@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
package/src/data/mock.ts
DELETED
|
@@ -1,842 +0,0 @@
|
|
|
1
|
-
import type { FinancialDataProvider } from './types'
|
|
2
|
-
import type {
|
|
3
|
-
Invoice, InvoiceItem, FinancialMovement, BankAccount,
|
|
4
|
-
CashSession, PaymentMethod, PaymentMethodType, ChartOfAccountsNode,
|
|
5
|
-
CostCenter, CardTransaction,
|
|
6
|
-
CreateInvoiceInput, PayMovementInput, CreateTransferInput, TransferResult,
|
|
7
|
-
OpenCashSessionInput, CloseCashSessionInput, CreateBankAccountInput,
|
|
8
|
-
InvoiceQuery, MovementQuery, StatementQuery,
|
|
9
|
-
PaginatedResult, FinancialSummary, StatementEntry, StatementResult, CashSessionSummary,
|
|
10
|
-
DateRange, InvoiceStatus, MovementStatus,
|
|
11
|
-
NotaFiscal, CreateNotaFiscalInput, NotaFiscalQuery,
|
|
12
|
-
} from '../types'
|
|
13
|
-
|
|
14
|
-
// ---------------------------------------------------------------------------
|
|
15
|
-
// Helpers
|
|
16
|
-
// ---------------------------------------------------------------------------
|
|
17
|
-
|
|
18
|
-
let nextId = 1
|
|
19
|
-
function uid(): string {
|
|
20
|
-
return String(nextId++)
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function now(): string {
|
|
24
|
-
return new Date().toISOString()
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function today(): string {
|
|
28
|
-
return new Date().toISOString().slice(0, 10)
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function round2(n: number): number {
|
|
32
|
-
return Math.round(n * 100) / 100
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function matchesDateRange(dateStr: string | undefined, range?: DateRange): boolean {
|
|
36
|
-
if (!range || !dateStr) return true
|
|
37
|
-
return dateStr >= range.from && dateStr <= range.to
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function matchesStatus<T extends string>(status: T, filter?: T | T[]): boolean {
|
|
41
|
-
if (!filter) return true
|
|
42
|
-
if (Array.isArray(filter)) return filter.includes(status)
|
|
43
|
-
return status === filter
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function paginate<T>(items: T[], page?: number, pageSize?: number): PaginatedResult<T> {
|
|
47
|
-
const p = page ?? 1
|
|
48
|
-
const ps = pageSize ?? 50
|
|
49
|
-
const start = (p - 1) * ps
|
|
50
|
-
return { data: items.slice(start, start + ps), total: items.length }
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// ---------------------------------------------------------------------------
|
|
54
|
-
// In-memory store
|
|
55
|
-
// ---------------------------------------------------------------------------
|
|
56
|
-
|
|
57
|
-
interface MockStore {
|
|
58
|
-
invoices: Invoice[]
|
|
59
|
-
invoiceItems: InvoiceItem[]
|
|
60
|
-
movements: FinancialMovement[]
|
|
61
|
-
bankAccounts: BankAccount[]
|
|
62
|
-
cashSessions: CashSession[]
|
|
63
|
-
paymentMethods: PaymentMethod[]
|
|
64
|
-
paymentMethodTypes: PaymentMethodType[]
|
|
65
|
-
chartOfAccounts: ChartOfAccountsNode[]
|
|
66
|
-
costCenters: CostCenter[]
|
|
67
|
-
cardTransactions: CardTransaction[]
|
|
68
|
-
notas: NotaFiscal[]
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function createStore(options?: MockFinancialProviderOptions): MockStore {
|
|
72
|
-
const tenantId = 'mock-tenant'
|
|
73
|
-
const seed = options?.seed
|
|
74
|
-
|
|
75
|
-
const defaultPMTypes: PaymentMethodType[] = (options?.paymentMethodTypes ?? [
|
|
76
|
-
{ name: 'Cash', transactionType: 'cash' },
|
|
77
|
-
{ name: 'Credit Card', transactionType: 'credit_card' },
|
|
78
|
-
{ name: 'Debit Card', transactionType: 'debit_card' },
|
|
79
|
-
{ name: 'PIX', transactionType: 'pix' },
|
|
80
|
-
{ name: 'Bank Transfer', transactionType: 'transfer' },
|
|
81
|
-
]).map((t) => ({
|
|
82
|
-
id: uid(),
|
|
83
|
-
name: t.name,
|
|
84
|
-
transactionType: t.transactionType,
|
|
85
|
-
isActive: true,
|
|
86
|
-
tenantId,
|
|
87
|
-
createdAt: now(),
|
|
88
|
-
}))
|
|
89
|
-
|
|
90
|
-
const cashAccount: BankAccount = {
|
|
91
|
-
id: uid(),
|
|
92
|
-
name: 'Main Cash Register',
|
|
93
|
-
accountType: 'cash_register',
|
|
94
|
-
currentBalance: 0,
|
|
95
|
-
initialBalance: 0,
|
|
96
|
-
isActive: true,
|
|
97
|
-
tenantId,
|
|
98
|
-
createdAt: now(),
|
|
99
|
-
updatedAt: now(),
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const bankAccount: BankAccount = {
|
|
103
|
-
id: uid(),
|
|
104
|
-
name: 'Business Checking',
|
|
105
|
-
accountType: 'bank_account',
|
|
106
|
-
bankName: 'Main Bank',
|
|
107
|
-
currentBalance: 0,
|
|
108
|
-
initialBalance: 0,
|
|
109
|
-
isActive: true,
|
|
110
|
-
tenantId,
|
|
111
|
-
createdAt: now(),
|
|
112
|
-
updatedAt: now(),
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
return {
|
|
116
|
-
// Seeded data (when provided) makes the module render as if the user already
|
|
117
|
-
// has accounts + transactions — a populated Summary/Statements instead of the
|
|
118
|
-
// empty onboarding state. Seed arrays are copied so the caller's data is not mutated.
|
|
119
|
-
invoices: seed?.invoices ? [...seed.invoices] : [],
|
|
120
|
-
invoiceItems: seed?.invoiceItems ? [...seed.invoiceItems] : [],
|
|
121
|
-
movements: seed?.movements ? [...seed.movements] : [],
|
|
122
|
-
bankAccounts: seed?.bankAccounts ? [...seed.bankAccounts] : [cashAccount, bankAccount],
|
|
123
|
-
cashSessions: [],
|
|
124
|
-
paymentMethods: seed?.paymentMethods ? [...seed.paymentMethods] : [],
|
|
125
|
-
paymentMethodTypes: defaultPMTypes,
|
|
126
|
-
chartOfAccounts: [],
|
|
127
|
-
costCenters: [],
|
|
128
|
-
cardTransactions: [],
|
|
129
|
-
notas: [],
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// ---------------------------------------------------------------------------
|
|
134
|
-
// Mock provider factory
|
|
135
|
-
// ---------------------------------------------------------------------------
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Pre-populated store data. Any omitted collection stays empty (except
|
|
139
|
-
* bankAccounts, which fall back to the two default mock accounts when absent).
|
|
140
|
-
* Pass full domain objects — the mock treats them as the existing ledger.
|
|
141
|
-
*/
|
|
142
|
-
export interface MockFinancialSeed {
|
|
143
|
-
bankAccounts?: BankAccount[]
|
|
144
|
-
invoices?: Invoice[]
|
|
145
|
-
invoiceItems?: InvoiceItem[]
|
|
146
|
-
movements?: FinancialMovement[]
|
|
147
|
-
paymentMethods?: PaymentMethod[]
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
export interface MockFinancialProviderOptions {
|
|
151
|
-
paymentMethodTypes?: Array<{ name: string; transactionType?: string }>
|
|
152
|
-
/** Pre-seed the in-memory store so the module renders populated (not the empty state). */
|
|
153
|
-
seed?: MockFinancialSeed
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
export function createMockFinancialProvider(options?: MockFinancialProviderOptions): FinancialDataProvider {
|
|
157
|
-
const store = createStore(options)
|
|
158
|
-
const tenantId = 'mock-tenant'
|
|
159
|
-
|
|
160
|
-
function recalcInvoiceStatus(invoice: Invoice): void {
|
|
161
|
-
if (invoice.paidAmount >= invoice.totalAmount) {
|
|
162
|
-
invoice.status = 'paid'
|
|
163
|
-
} else if (invoice.paidAmount > 0) {
|
|
164
|
-
invoice.status = 'partial'
|
|
165
|
-
} else {
|
|
166
|
-
// Check if any movement is overdue
|
|
167
|
-
const movements = store.movements.filter((m) => m.invoiceId === invoice.id && m.movementKind === 'bill')
|
|
168
|
-
const hasOverdue = movements.some((m) => m.status === 'overdue' || (m.status === 'pending' && m.dueDate < today()))
|
|
169
|
-
invoice.status = hasOverdue ? 'overdue' : 'open'
|
|
170
|
-
}
|
|
171
|
-
invoice.updatedAt = now()
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const provider: FinancialDataProvider = {
|
|
175
|
-
// --- Invoices ---
|
|
176
|
-
async getInvoices(query: InvoiceQuery): Promise<PaginatedResult<Invoice>> {
|
|
177
|
-
let results = [...store.invoices]
|
|
178
|
-
if (query.direction) results = results.filter((i) => i.direction === query.direction)
|
|
179
|
-
if (query.status) results = results.filter((i) => matchesStatus(i.status, query.status as InvoiceStatus | InvoiceStatus[]))
|
|
180
|
-
if (query.contactId) results = results.filter((i) => i.contactId === query.contactId)
|
|
181
|
-
if (query.accountId) {
|
|
182
|
-
const invoiceIds = new Set(store.invoiceItems.filter((item) => item.accountId === query.accountId).map((item) => item.invoiceId))
|
|
183
|
-
results = results.filter((invoice) => invoiceIds.has(invoice.id))
|
|
184
|
-
}
|
|
185
|
-
if (query.costCenterId) {
|
|
186
|
-
const invoiceIds = new Set(store.invoiceItems.filter((item) => item.costCenterId === query.costCenterId).map((item) => item.invoiceId))
|
|
187
|
-
results = results.filter((invoice) => invoiceIds.has(invoice.id))
|
|
188
|
-
}
|
|
189
|
-
if (query.dateRange) results = results.filter((i) => matchesDateRange(i.invoiceDate, query.dateRange))
|
|
190
|
-
if (query.search) {
|
|
191
|
-
const s = query.search.toLowerCase()
|
|
192
|
-
results = results.filter((i) =>
|
|
193
|
-
i.contactName?.toLowerCase().includes(s) ||
|
|
194
|
-
i.observations?.toLowerCase().includes(s) ||
|
|
195
|
-
i.fiscalNumber?.toLowerCase().includes(s)
|
|
196
|
-
)
|
|
197
|
-
}
|
|
198
|
-
results.sort((a, b) => b.createdAt.localeCompare(a.createdAt))
|
|
199
|
-
return paginate(results, query.page, query.pageSize)
|
|
200
|
-
},
|
|
201
|
-
|
|
202
|
-
async getInvoiceById(id: string): Promise<Invoice | null> {
|
|
203
|
-
return store.invoices.find((i) => i.id === id) ?? null
|
|
204
|
-
},
|
|
205
|
-
|
|
206
|
-
async getInvoiceItems(invoiceId: string): Promise<InvoiceItem[]> {
|
|
207
|
-
return store.invoiceItems.filter((i) => i.invoiceId === invoiceId)
|
|
208
|
-
},
|
|
209
|
-
|
|
210
|
-
async createInvoice(input: CreateInvoiceInput): Promise<Invoice> {
|
|
211
|
-
const invoiceId = uid()
|
|
212
|
-
const totalAmount = input.items.reduce((sum, item) => {
|
|
213
|
-
const itemTotal = item.quantity * item.unitPrice - (item.discount ?? 0) + (item.surcharge ?? 0)
|
|
214
|
-
return sum + itemTotal
|
|
215
|
-
}, 0)
|
|
216
|
-
|
|
217
|
-
const itemsSummary = input.items.map((i) => i.description).filter(Boolean).join(', ')
|
|
218
|
-
|
|
219
|
-
const invoice: Invoice = {
|
|
220
|
-
id: invoiceId,
|
|
221
|
-
direction: input.direction,
|
|
222
|
-
invoiceDate: input.invoiceDate,
|
|
223
|
-
fiscalNumber: input.fiscalNumber,
|
|
224
|
-
totalAmount,
|
|
225
|
-
paidAmount: 0,
|
|
226
|
-
status: 'open',
|
|
227
|
-
totalInstallments: input.installments.length || 1,
|
|
228
|
-
contactId: input.contactId,
|
|
229
|
-
contactName: input.contactName,
|
|
230
|
-
cashSessionId: input.cashSessionId,
|
|
231
|
-
unitId: input.unitId,
|
|
232
|
-
observations: input.observations,
|
|
233
|
-
itemsSummary: itemsSummary || undefined,
|
|
234
|
-
metadata: input.metadata,
|
|
235
|
-
tenantId,
|
|
236
|
-
createdAt: now(),
|
|
237
|
-
updatedAt: now(),
|
|
238
|
-
}
|
|
239
|
-
store.invoices.push(invoice)
|
|
240
|
-
|
|
241
|
-
// Create items
|
|
242
|
-
for (const itemInput of input.items) {
|
|
243
|
-
const itemTotal = itemInput.quantity * itemInput.unitPrice - (itemInput.discount ?? 0) + (itemInput.surcharge ?? 0)
|
|
244
|
-
const item: InvoiceItem = {
|
|
245
|
-
id: uid(),
|
|
246
|
-
invoiceId,
|
|
247
|
-
itemKind: itemInput.itemKind,
|
|
248
|
-
referenceId: itemInput.referenceId,
|
|
249
|
-
description: itemInput.description,
|
|
250
|
-
quantity: itemInput.quantity,
|
|
251
|
-
unitPrice: itemInput.unitPrice,
|
|
252
|
-
totalAmount: itemTotal,
|
|
253
|
-
discount: itemInput.discount ?? 0,
|
|
254
|
-
surcharge: itemInput.surcharge ?? 0,
|
|
255
|
-
accountId: itemInput.accountId,
|
|
256
|
-
costCenterId: itemInput.costCenterId,
|
|
257
|
-
metadata: itemInput.metadata,
|
|
258
|
-
createdAt: now(),
|
|
259
|
-
}
|
|
260
|
-
store.invoiceItems.push(item)
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// Create installment movements
|
|
264
|
-
for (const movInput of input.installments) {
|
|
265
|
-
const movement: FinancialMovement = {
|
|
266
|
-
id: uid(),
|
|
267
|
-
invoiceId,
|
|
268
|
-
direction: input.direction,
|
|
269
|
-
movementKind: movInput.movementKind,
|
|
270
|
-
amount: movInput.amount,
|
|
271
|
-
paidAmount: 0,
|
|
272
|
-
status: 'pending',
|
|
273
|
-
dueDate: movInput.dueDate,
|
|
274
|
-
installmentNumber: movInput.installmentNumber,
|
|
275
|
-
paymentMethodId: movInput.paymentMethodId,
|
|
276
|
-
bankAccountId: movInput.bankAccountId,
|
|
277
|
-
notes: movInput.notes,
|
|
278
|
-
metadata: movInput.metadata,
|
|
279
|
-
tenantId,
|
|
280
|
-
createdAt: now(),
|
|
281
|
-
updatedAt: now(),
|
|
282
|
-
}
|
|
283
|
-
store.movements.push(movement)
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
return invoice
|
|
287
|
-
},
|
|
288
|
-
|
|
289
|
-
async updateInvoice(id: string, data: Partial<Invoice>): Promise<Invoice> {
|
|
290
|
-
const invoice = store.invoices.find((i) => i.id === id)
|
|
291
|
-
if (!invoice) throw new Error(`Invoice ${id} not found`)
|
|
292
|
-
Object.assign(invoice, data, { updatedAt: now() })
|
|
293
|
-
return invoice
|
|
294
|
-
},
|
|
295
|
-
|
|
296
|
-
async cancelInvoice(id: string): Promise<void> {
|
|
297
|
-
const invoice = store.invoices.find((i) => i.id === id)
|
|
298
|
-
if (!invoice) throw new Error(`Invoice ${id} not found`)
|
|
299
|
-
invoice.status = 'cancelled'
|
|
300
|
-
invoice.updatedAt = now()
|
|
301
|
-
// Cancel all pending movements
|
|
302
|
-
store.movements
|
|
303
|
-
.filter((m) => m.invoiceId === id && m.status === 'pending')
|
|
304
|
-
.forEach((m) => { m.status = 'cancelled'; m.updatedAt = now() })
|
|
305
|
-
},
|
|
306
|
-
|
|
307
|
-
// --- Movements ---
|
|
308
|
-
async getMovements(query: MovementQuery): Promise<PaginatedResult<FinancialMovement>> {
|
|
309
|
-
let results = [...store.movements]
|
|
310
|
-
if (query.direction) results = results.filter((m) => m.direction === query.direction)
|
|
311
|
-
if (query.status) results = results.filter((m) => matchesStatus(m.status, query.status as MovementStatus | MovementStatus[]))
|
|
312
|
-
if (query.bankAccountId) results = results.filter((m) => m.bankAccountId === query.bankAccountId)
|
|
313
|
-
if (query.cashSessionId) results = results.filter((m) => m.cashSessionId === query.cashSessionId)
|
|
314
|
-
if (query.dateRange) results = results.filter((m) => matchesDateRange(m.dueDate, query.dateRange))
|
|
315
|
-
results.sort((a, b) => a.dueDate.localeCompare(b.dueDate))
|
|
316
|
-
return paginate(results, query.page, query.pageSize)
|
|
317
|
-
},
|
|
318
|
-
|
|
319
|
-
async payMovement(input: PayMovementInput): Promise<FinancialMovement> {
|
|
320
|
-
const bill = store.movements.find((m) => m.id === input.movementId)
|
|
321
|
-
if (!bill) throw new Error(`Movement ${input.movementId} not found`)
|
|
322
|
-
|
|
323
|
-
// Net card settlement: derive the processing/MDR fee from the payment method (credits only).
|
|
324
|
-
const method = input.paymentMethodId ? store.paymentMethods.find((pm) => pm.id === input.paymentMethodId) : undefined
|
|
325
|
-
const feePct = bill.direction === 'credit' ? (method?.discountValue ?? 0) : 0
|
|
326
|
-
const thisFee = round2(input.amount * (feePct / 100))
|
|
327
|
-
|
|
328
|
-
// 1. Record the cash event as its OWN payment movement (one row per payment).
|
|
329
|
-
const payment: FinancialMovement = {
|
|
330
|
-
id: uid(),
|
|
331
|
-
invoiceId: bill.invoiceId,
|
|
332
|
-
direction: bill.direction,
|
|
333
|
-
movementKind: 'payment',
|
|
334
|
-
amount: input.amount,
|
|
335
|
-
paidAmount: input.amount,
|
|
336
|
-
feeAmount: thisFee,
|
|
337
|
-
status: 'paid',
|
|
338
|
-
dueDate: bill.dueDate,
|
|
339
|
-
paymentDate: input.paymentDate,
|
|
340
|
-
installmentNumber: bill.installmentNumber,
|
|
341
|
-
paymentMethodId: input.paymentMethodId,
|
|
342
|
-
paymentMethodTypeId: input.paymentMethodTypeId,
|
|
343
|
-
bankAccountId: input.bankAccountId,
|
|
344
|
-
cashSessionId: input.cashSessionId,
|
|
345
|
-
cardBrand: input.cardBrand,
|
|
346
|
-
cardInstallments: input.cardInstallments,
|
|
347
|
-
notes: input.notes,
|
|
348
|
-
tenantId,
|
|
349
|
-
createdAt: now(),
|
|
350
|
-
updatedAt: now(),
|
|
351
|
-
}
|
|
352
|
-
store.movements.push(payment)
|
|
353
|
-
|
|
354
|
-
// 2. Update the bill (obligation) cache: cumulative paid + status. Clear its cash
|
|
355
|
-
// fields so it stops being a cash event (the payment row is now the cash event).
|
|
356
|
-
bill.paidAmount = round2(bill.paidAmount + input.amount)
|
|
357
|
-
bill.status = bill.paidAmount >= bill.amount ? 'paid' : 'partial'
|
|
358
|
-
bill.paymentDate = undefined
|
|
359
|
-
bill.bankAccountId = undefined
|
|
360
|
-
bill.updatedAt = now()
|
|
361
|
-
|
|
362
|
-
// Update bank account balance by the NET cash impact (credit nets the fee).
|
|
363
|
-
if (input.bankAccountId) {
|
|
364
|
-
const account = store.bankAccounts.find((a) => a.id === input.bankAccountId)
|
|
365
|
-
if (account) {
|
|
366
|
-
if (bill.direction === 'credit') {
|
|
367
|
-
account.currentBalance = round2(account.currentBalance + (input.amount - thisFee))
|
|
368
|
-
} else {
|
|
369
|
-
account.currentBalance = round2(account.currentBalance - input.amount)
|
|
370
|
-
}
|
|
371
|
-
account.updatedAt = now()
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
// 3. Recalculate invoice status from its bills
|
|
376
|
-
if (bill.invoiceId) {
|
|
377
|
-
const invoice = store.invoices.find((i) => i.id === bill.invoiceId)
|
|
378
|
-
if (invoice) {
|
|
379
|
-
const bills = store.movements.filter((m) => m.invoiceId === invoice.id && m.movementKind === 'bill')
|
|
380
|
-
invoice.paidAmount = bills.reduce((sum, m) => sum + m.paidAmount, 0)
|
|
381
|
-
recalcInvoiceStatus(invoice)
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
return payment
|
|
386
|
-
},
|
|
387
|
-
|
|
388
|
-
async cancelMovement(id: string): Promise<void> {
|
|
389
|
-
const movement = store.movements.find((m) => m.id === id)
|
|
390
|
-
if (!movement) throw new Error(`Movement ${id} not found`)
|
|
391
|
-
movement.status = 'cancelled'
|
|
392
|
-
movement.updatedAt = now()
|
|
393
|
-
},
|
|
394
|
-
|
|
395
|
-
// --- Transfers (between accounts) ---
|
|
396
|
-
async createTransfer(input: CreateTransferInput): Promise<TransferResult> {
|
|
397
|
-
if (!input.fromAccountId || !input.toAccountId || input.fromAccountId === input.toAccountId) {
|
|
398
|
-
throw new Error('Source and destination accounts must differ')
|
|
399
|
-
}
|
|
400
|
-
if (!(input.amount > 0)) throw new Error('Amount must be greater than zero')
|
|
401
|
-
const transferId = uid()
|
|
402
|
-
const debitMovementId = uid()
|
|
403
|
-
const creditMovementId = uid()
|
|
404
|
-
const base = {
|
|
405
|
-
movementKind: 'transfer' as const,
|
|
406
|
-
amount: input.amount,
|
|
407
|
-
paidAmount: input.amount,
|
|
408
|
-
feeAmount: 0,
|
|
409
|
-
status: 'paid' as const,
|
|
410
|
-
dueDate: input.date,
|
|
411
|
-
paymentDate: input.date,
|
|
412
|
-
notes: input.notes,
|
|
413
|
-
tenantId,
|
|
414
|
-
createdAt: now(),
|
|
415
|
-
updatedAt: now(),
|
|
416
|
-
}
|
|
417
|
-
store.movements.push({
|
|
418
|
-
...base, id: debitMovementId, direction: 'debit', bankAccountId: input.fromAccountId,
|
|
419
|
-
metadata: { transferId, transferRole: 'out', counterAccountId: input.toAccountId },
|
|
420
|
-
} as FinancialMovement)
|
|
421
|
-
store.movements.push({
|
|
422
|
-
...base, id: creditMovementId, direction: 'credit', bankAccountId: input.toAccountId,
|
|
423
|
-
metadata: { transferId, transferRole: 'in', counterAccountId: input.fromAccountId },
|
|
424
|
-
} as FinancialMovement)
|
|
425
|
-
// Reflect in stored balances.
|
|
426
|
-
const from = store.bankAccounts.find((a) => a.id === input.fromAccountId)
|
|
427
|
-
const to = store.bankAccounts.find((a) => a.id === input.toAccountId)
|
|
428
|
-
if (from) { from.currentBalance = round2(from.currentBalance - input.amount); from.updatedAt = now() }
|
|
429
|
-
if (to) { to.currentBalance = round2(to.currentBalance + input.amount); to.updatedAt = now() }
|
|
430
|
-
return { transferId, debitMovementId, creditMovementId }
|
|
431
|
-
},
|
|
432
|
-
|
|
433
|
-
// --- Bank Accounts ---
|
|
434
|
-
async getBankAccounts(): Promise<BankAccount[]> {
|
|
435
|
-
return store.bankAccounts.filter((a) => a.isActive)
|
|
436
|
-
},
|
|
437
|
-
|
|
438
|
-
async createBankAccount(data: CreateBankAccountInput): Promise<BankAccount> {
|
|
439
|
-
const account: BankAccount = {
|
|
440
|
-
id: uid(),
|
|
441
|
-
name: data.name,
|
|
442
|
-
accountType: data.accountType,
|
|
443
|
-
bankName: data.bankName,
|
|
444
|
-
accountNumber: data.accountNumber,
|
|
445
|
-
agencyNumber: data.agencyNumber,
|
|
446
|
-
currentBalance: data.initialBalance ?? 0,
|
|
447
|
-
initialBalance: data.initialBalance ?? 0,
|
|
448
|
-
creditLimit: data.creditLimit,
|
|
449
|
-
dueDay: data.dueDay,
|
|
450
|
-
closingDay: data.closingDay,
|
|
451
|
-
isActive: true,
|
|
452
|
-
unitId: data.unitId,
|
|
453
|
-
tenantId,
|
|
454
|
-
createdAt: now(),
|
|
455
|
-
updatedAt: now(),
|
|
456
|
-
}
|
|
457
|
-
store.bankAccounts.push(account)
|
|
458
|
-
return account
|
|
459
|
-
},
|
|
460
|
-
|
|
461
|
-
async updateBankAccount(id: string, data: Partial<BankAccount>): Promise<BankAccount> {
|
|
462
|
-
const account = store.bankAccounts.find((a) => a.id === id)
|
|
463
|
-
if (!account) throw new Error(`Bank account ${id} not found`)
|
|
464
|
-
Object.assign(account, data, { updatedAt: now() })
|
|
465
|
-
return account
|
|
466
|
-
},
|
|
467
|
-
|
|
468
|
-
// --- Cash Sessions ---
|
|
469
|
-
async getCashSessions(bankAccountId?: string): Promise<CashSession[]> {
|
|
470
|
-
let results = [...store.cashSessions]
|
|
471
|
-
if (bankAccountId) results = results.filter((s) => s.bankAccountId === bankAccountId)
|
|
472
|
-
return results.sort((a, b) => b.openedAt.localeCompare(a.openedAt))
|
|
473
|
-
},
|
|
474
|
-
|
|
475
|
-
async getOpenSession(bankAccountId: string): Promise<CashSession | null> {
|
|
476
|
-
return store.cashSessions.find((s) => s.bankAccountId === bankAccountId && s.status === 'open') ?? null
|
|
477
|
-
},
|
|
478
|
-
|
|
479
|
-
async openCashSession(input: OpenCashSessionInput): Promise<CashSession> {
|
|
480
|
-
const account = store.bankAccounts.find((a) => a.id === input.bankAccountId)
|
|
481
|
-
const session: CashSession = {
|
|
482
|
-
id: uid(),
|
|
483
|
-
bankAccountId: input.bankAccountId,
|
|
484
|
-
bankAccountName: account?.name,
|
|
485
|
-
status: 'open',
|
|
486
|
-
openedAt: now(),
|
|
487
|
-
openedByUserId: input.openedByUserId,
|
|
488
|
-
openedByName: input.openedByName,
|
|
489
|
-
openingBalance: input.openingBalance,
|
|
490
|
-
unitId: input.unitId,
|
|
491
|
-
tenantId,
|
|
492
|
-
createdAt: now(),
|
|
493
|
-
}
|
|
494
|
-
store.cashSessions.push(session)
|
|
495
|
-
return session
|
|
496
|
-
},
|
|
497
|
-
|
|
498
|
-
async closeCashSession(input: CloseCashSessionInput): Promise<CashSession> {
|
|
499
|
-
const session = store.cashSessions.find((s) => s.id === input.sessionId)
|
|
500
|
-
if (!session) throw new Error(`Cash session ${input.sessionId} not found`)
|
|
501
|
-
|
|
502
|
-
// Calculate expected balance
|
|
503
|
-
const sessionMovements = store.movements.filter((m) => m.cashSessionId === session.id && m.status === 'paid')
|
|
504
|
-
const inflow = sessionMovements.filter((m) => m.direction === 'credit').reduce((s, m) => s + m.paidAmount, 0)
|
|
505
|
-
const outflow = sessionMovements.filter((m) => m.direction === 'debit').reduce((s, m) => s + m.paidAmount, 0)
|
|
506
|
-
const expectedBalance = session.openingBalance + inflow - outflow
|
|
507
|
-
|
|
508
|
-
session.status = 'closed'
|
|
509
|
-
session.closedAt = now()
|
|
510
|
-
session.closedByUserId = input.closedByUserId
|
|
511
|
-
session.closedByName = input.closedByName
|
|
512
|
-
session.closingBalance = input.closingBalance
|
|
513
|
-
session.expectedBalance = expectedBalance
|
|
514
|
-
session.difference = input.closingBalance - expectedBalance
|
|
515
|
-
session.notes = input.notes
|
|
516
|
-
|
|
517
|
-
return session
|
|
518
|
-
},
|
|
519
|
-
|
|
520
|
-
async getCashSessionSummary(sessionId: string): Promise<CashSessionSummary> {
|
|
521
|
-
const session = store.cashSessions.find((s) => s.id === sessionId)
|
|
522
|
-
if (!session) throw new Error(`Cash session ${sessionId} not found`)
|
|
523
|
-
const sessionMovements = store.movements.filter((m) => m.cashSessionId === sessionId && m.status === 'paid')
|
|
524
|
-
return {
|
|
525
|
-
session,
|
|
526
|
-
movementCount: sessionMovements.length,
|
|
527
|
-
totalInflow: sessionMovements.filter((m) => m.direction === 'credit').reduce((s, m) => s + m.paidAmount, 0),
|
|
528
|
-
totalOutflow: sessionMovements.filter((m) => m.direction === 'debit').reduce((s, m) => s + m.paidAmount, 0),
|
|
529
|
-
}
|
|
530
|
-
},
|
|
531
|
-
|
|
532
|
-
// --- Payment Methods ---
|
|
533
|
-
async getPaymentMethods(): Promise<PaymentMethod[]> {
|
|
534
|
-
return store.paymentMethods.filter((pm) => pm.isActive)
|
|
535
|
-
},
|
|
536
|
-
|
|
537
|
-
async getPaymentMethodTypes(): Promise<PaymentMethodType[]> {
|
|
538
|
-
return store.paymentMethodTypes.filter((t) => t.isActive)
|
|
539
|
-
},
|
|
540
|
-
|
|
541
|
-
// --- Chart of Accounts ---
|
|
542
|
-
async getChartOfAccounts(): Promise<ChartOfAccountsNode[]> {
|
|
543
|
-
return store.chartOfAccounts.filter((n) => n.isActive)
|
|
544
|
-
},
|
|
545
|
-
|
|
546
|
-
async getCostCenters(): Promise<CostCenter[]> {
|
|
547
|
-
return store.costCenters.filter((c) => c.isActive)
|
|
548
|
-
},
|
|
549
|
-
|
|
550
|
-
// --- Card Transactions ---
|
|
551
|
-
async getCardTransactions(dateRange?: DateRange): Promise<CardTransaction[]> {
|
|
552
|
-
let results = [...store.cardTransactions]
|
|
553
|
-
if (dateRange) results = results.filter((t) => matchesDateRange(t.expectedDate, dateRange))
|
|
554
|
-
return results.sort((a, b) => a.expectedDate.localeCompare(b.expectedDate))
|
|
555
|
-
},
|
|
556
|
-
|
|
557
|
-
// --- Statements (ERP extract: opening balance + realized cash + per-row net/fee) ---
|
|
558
|
-
async getStatement(query: StatementQuery): Promise<StatementResult> {
|
|
559
|
-
const todayStr = today()
|
|
560
|
-
// A cash event = any non-cancelled movement with a payment_date (payment, transfer,
|
|
561
|
-
// or a legacy in-place-paid bill). Bills lose their payment_date once split.
|
|
562
|
-
const isRealized = (m: FinancialMovement) => m.status !== 'cancelled' && !!m.paymentDate
|
|
563
|
-
const consolidated = !query.bankAccountId
|
|
564
|
-
const scopeAccounts = consolidated
|
|
565
|
-
? store.bankAccounts.filter((a) => a.isActive)
|
|
566
|
-
: store.bankAccounts.filter((a) => a.id === query.bankAccountId)
|
|
567
|
-
const accountIds = new Set(scopeAccounts.map((a) => a.id))
|
|
568
|
-
const accountNameById = new Map(store.bankAccounts.map((a) => [a.id, a.name]))
|
|
569
|
-
const initialSum = scopeAccounts.reduce((s, a) => s + a.initialBalance, 0)
|
|
570
|
-
// Only bail when a specific account was asked for and not found.
|
|
571
|
-
if (!consolidated && accountIds.size === 0) {
|
|
572
|
-
return { entries: [], openingBalance: 0, closingBalance: 0, totalCredits: 0, totalDebits: 0, totalFees: 0, net: 0, accountId: query.bankAccountId }
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
const netOf = (m: FinancialMovement) => m.direction === 'credit'
|
|
576
|
-
? m.paidAmount - (m.feeAmount ?? 0)
|
|
577
|
-
: -m.paidAmount
|
|
578
|
-
|
|
579
|
-
// Consolidated => all realized movements (incl. those with no bank account).
|
|
580
|
-
const inScope = store.movements.filter((m) =>
|
|
581
|
-
isRealized(m) && (consolidated || (m.bankAccountId && accountIds.has(m.bankAccountId))))
|
|
582
|
-
|
|
583
|
-
const openingBalance = round2(initialSum + inScope
|
|
584
|
-
.filter((m) => (m.paymentDate ?? '') < query.dateRange.from)
|
|
585
|
-
.reduce((s, m) => s + netOf(m), 0))
|
|
586
|
-
|
|
587
|
-
const rangeMovs = inScope
|
|
588
|
-
.filter((m) => matchesDateRange(m.paymentDate, query.dateRange))
|
|
589
|
-
.sort((a, b) => (a.paymentDate ?? '').localeCompare(b.paymentDate ?? '') || a.createdAt.localeCompare(b.createdAt))
|
|
590
|
-
|
|
591
|
-
let balance = openingBalance
|
|
592
|
-
let totalCredits = 0, totalDebits = 0, totalFees = 0
|
|
593
|
-
const entries: StatementEntry[] = rangeMovs.map((movement) => {
|
|
594
|
-
const gross = movement.paidAmount
|
|
595
|
-
const isCredit = movement.direction === 'credit'
|
|
596
|
-
const fee = isCredit ? (movement.feeAmount ?? 0) : 0
|
|
597
|
-
const net = isCredit ? round2(gross - fee) : gross
|
|
598
|
-
const isTransfer = movement.movementKind === 'transfer'
|
|
599
|
-
const meta = (movement.metadata ?? {}) as any
|
|
600
|
-
const entryKind: StatementEntry['entryKind'] = isTransfer
|
|
601
|
-
? (meta.transferRole === 'in' ? 'transfer-in' : 'transfer-out')
|
|
602
|
-
: 'movement'
|
|
603
|
-
const counterAccountId = isTransfer ? meta.counterAccountId : undefined
|
|
604
|
-
balance = round2(isCredit ? balance + net : balance - net)
|
|
605
|
-
if (!(consolidated && isTransfer)) {
|
|
606
|
-
if (isCredit) totalCredits = round2(totalCredits + net)
|
|
607
|
-
else totalDebits = round2(totalDebits + net)
|
|
608
|
-
totalFees = round2(totalFees + fee)
|
|
609
|
-
}
|
|
610
|
-
return {
|
|
611
|
-
movement,
|
|
612
|
-
invoice: movement.invoiceId ? store.invoices.find((i) => i.id === movement.invoiceId) : undefined,
|
|
613
|
-
runningBalance: balance,
|
|
614
|
-
gross, fee, net, entryKind,
|
|
615
|
-
counterAccountId,
|
|
616
|
-
counterAccountName: counterAccountId ? accountNameById.get(counterAccountId) : undefined,
|
|
617
|
-
} as StatementEntry
|
|
618
|
-
})
|
|
619
|
-
|
|
620
|
-
// Link reconciled bank lines to the internal movement they were matched to.
|
|
621
|
-
for (const e of entries) {
|
|
622
|
-
const mid = e.movement.matchedMovementId
|
|
623
|
-
const mm = mid ? store.movements.find((m) => m.id === mid) : undefined
|
|
624
|
-
if (!mm) continue
|
|
625
|
-
const inv = mm.invoiceId ? store.invoices.find((i) => i.id === mm.invoiceId) : undefined
|
|
626
|
-
e.reconciledWith = {
|
|
627
|
-
movementId: mm.id,
|
|
628
|
-
description: mm.notes ?? undefined,
|
|
629
|
-
invoiceId: mm.invoiceId,
|
|
630
|
-
fiscalNumber: inv?.fiscalNumber,
|
|
631
|
-
contactName: inv?.contactName,
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
const accountCurrentBalance = round2(initialSum + inScope
|
|
636
|
-
.filter((m) => (m.paymentDate ?? '') <= todayStr)
|
|
637
|
-
.reduce((s, m) => s + netOf(m), 0))
|
|
638
|
-
|
|
639
|
-
return {
|
|
640
|
-
entries,
|
|
641
|
-
openingBalance,
|
|
642
|
-
closingBalance: balance,
|
|
643
|
-
totalCredits,
|
|
644
|
-
totalDebits,
|
|
645
|
-
totalFees,
|
|
646
|
-
net: round2(totalCredits - totalDebits),
|
|
647
|
-
accountId: query.bankAccountId,
|
|
648
|
-
accountCurrentBalance,
|
|
649
|
-
}
|
|
650
|
-
},
|
|
651
|
-
|
|
652
|
-
// --- Summary ---
|
|
653
|
-
async getSummary(dateRange?: DateRange): Promise<FinancialSummary> {
|
|
654
|
-
const totalBalance = store.bankAccounts
|
|
655
|
-
.filter((a) => a.isActive)
|
|
656
|
-
.reduce((sum, a) => sum + a.currentBalance, 0)
|
|
657
|
-
|
|
658
|
-
const pendingReceivable = store.movements.filter((m) =>
|
|
659
|
-
m.direction === 'credit' && m.movementKind === 'bill' && ['pending', 'partial'].includes(m.status)
|
|
660
|
-
)
|
|
661
|
-
const pendingPayable = store.movements.filter((m) =>
|
|
662
|
-
m.direction === 'debit' && m.movementKind === 'bill' && ['pending', 'partial'].includes(m.status)
|
|
663
|
-
)
|
|
664
|
-
|
|
665
|
-
const todayStr = today()
|
|
666
|
-
// "Monthly" cash flow uses a trailing 30-day window (rolling month), not
|
|
667
|
-
// calendar month-to-date. Month-to-date collapses to near-empty in the
|
|
668
|
-
// first days of a month — on the 1st it's a single day — so the cash-flow
|
|
669
|
-
// chart and Fluxo Mensal KPI read empty even with a full month of realized
|
|
670
|
-
// activity. A rolling window stays representative on every day of the month.
|
|
671
|
-
const monthAgo = new Date()
|
|
672
|
-
monthAgo.setDate(monthAgo.getDate() - 30)
|
|
673
|
-
const monthStart = monthAgo.toISOString().slice(0, 10)
|
|
674
|
-
const monthEnd = todayStr
|
|
675
|
-
|
|
676
|
-
const monthMovements = store.movements.filter((m) =>
|
|
677
|
-
m.movementKind === 'payment' && matchesDateRange(m.paymentDate, dateRange ?? { from: monthStart, to: monthEnd })
|
|
678
|
-
)
|
|
679
|
-
|
|
680
|
-
const overdueReceivable = pendingReceivable.filter((m) => m.dueDate < todayStr)
|
|
681
|
-
const overduePayable = pendingPayable.filter((m) => m.dueDate < todayStr)
|
|
682
|
-
|
|
683
|
-
return {
|
|
684
|
-
totalBalance,
|
|
685
|
-
totalReceivable: pendingReceivable.reduce((s, m) => s + (m.amount - m.paidAmount), 0),
|
|
686
|
-
totalPayable: pendingPayable.reduce((s, m) => s + (m.amount - m.paidAmount), 0),
|
|
687
|
-
monthlyInflow: monthMovements.filter((m) => m.direction === 'credit').reduce((s, m) => s + m.paidAmount, 0),
|
|
688
|
-
monthlyOutflow: monthMovements.filter((m) => m.direction === 'debit').reduce((s, m) => s + m.paidAmount, 0),
|
|
689
|
-
overdueReceivableCount: overdueReceivable.length,
|
|
690
|
-
overdueReceivableAmount: overdueReceivable.reduce((s, m) => s + (m.amount - m.paidAmount), 0),
|
|
691
|
-
overduePayableCount: overduePayable.length,
|
|
692
|
-
overduePayableAmount: overduePayable.reduce((s, m) => s + (m.amount - m.paidAmount), 0),
|
|
693
|
-
}
|
|
694
|
-
},
|
|
695
|
-
|
|
696
|
-
// --- Reconciliation (conciliação) ---
|
|
697
|
-
async importBankTransactions(input) {
|
|
698
|
-
let imported = 0, duplicates = 0
|
|
699
|
-
for (const line of input.lines) {
|
|
700
|
-
const dup = store.movements.some((m) => m.externalSource === line.externalSource && m.externalId === line.externalId)
|
|
701
|
-
if (dup) { duplicates++; continue }
|
|
702
|
-
store.movements.push({
|
|
703
|
-
id: uid(),
|
|
704
|
-
direction: line.direction,
|
|
705
|
-
movementKind: 'payment',
|
|
706
|
-
amount: line.amount,
|
|
707
|
-
paidAmount: line.amount,
|
|
708
|
-
status: 'paid',
|
|
709
|
-
dueDate: line.date,
|
|
710
|
-
paymentDate: line.date,
|
|
711
|
-
bankAccountId: line.bankAccountId ?? input.bankAccountId,
|
|
712
|
-
notes: line.description,
|
|
713
|
-
externalId: line.externalId,
|
|
714
|
-
externalSource: line.externalSource,
|
|
715
|
-
tenantId,
|
|
716
|
-
createdAt: now(),
|
|
717
|
-
updatedAt: now(),
|
|
718
|
-
} as FinancialMovement)
|
|
719
|
-
imported++
|
|
720
|
-
}
|
|
721
|
-
return { imported, duplicates }
|
|
722
|
-
},
|
|
723
|
-
|
|
724
|
-
async getUnreconciled(query) {
|
|
725
|
-
let results = store.movements.filter((m) => m.externalSource && !m.reconciledAt)
|
|
726
|
-
if (query?.bankAccountId) results = results.filter((m) => m.bankAccountId === query.bankAccountId)
|
|
727
|
-
if (query?.dateRange) results = results.filter((m) => matchesDateRange(m.paymentDate, query.dateRange))
|
|
728
|
-
return results.sort((a, b) => (b.paymentDate ?? '').localeCompare(a.paymentDate ?? ''))
|
|
729
|
-
},
|
|
730
|
-
|
|
731
|
-
async suggestReconciliation(bankMovementId) {
|
|
732
|
-
const line = store.movements.find((m) => m.id === bankMovementId)
|
|
733
|
-
if (!line) return []
|
|
734
|
-
const amount = line.amount
|
|
735
|
-
const tol = Math.max(0.01, round2(amount * 0.02))
|
|
736
|
-
const lineDate = line.paymentDate ?? line.dueDate
|
|
737
|
-
return store.movements
|
|
738
|
-
.filter((m) => !m.externalSource && !m.reconciledAt && m.direction === line.direction && m.id !== bankMovementId && Math.abs(m.amount - amount) <= tol)
|
|
739
|
-
.map((m) => {
|
|
740
|
-
const amtScore = amount > 0 ? Math.max(0, 1 - Math.abs(m.amount - amount) / amount) : 0
|
|
741
|
-
const refDate = m.dueDate ?? m.paymentDate
|
|
742
|
-
const days = lineDate && refDate ? Math.abs((new Date(lineDate).getTime() - new Date(refDate).getTime()) / 86400000) : 30
|
|
743
|
-
const dateScore = Math.max(0, 1 - days / 30)
|
|
744
|
-
return {
|
|
745
|
-
movement: m,
|
|
746
|
-
invoice: m.invoiceId ? store.invoices.find((i) => i.id === m.invoiceId) : undefined,
|
|
747
|
-
score: round2(0.7 * amtScore + 0.3 * dateScore),
|
|
748
|
-
}
|
|
749
|
-
})
|
|
750
|
-
.sort((a, b) => b.score - a.score)
|
|
751
|
-
.slice(0, 25)
|
|
752
|
-
},
|
|
753
|
-
|
|
754
|
-
async reconcileMovement(input) {
|
|
755
|
-
const line = store.movements.find((m) => m.id === input.bankMovementId)
|
|
756
|
-
if (line) { line.reconciledAt = now(); line.matchedMovementId = input.matchedMovementId; line.updatedAt = now() }
|
|
757
|
-
if (input.matchedMovementId) {
|
|
758
|
-
const internal = store.movements.find((m) => m.id === input.matchedMovementId)
|
|
759
|
-
if (internal) { internal.reconciledAt = now(); internal.updatedAt = now() }
|
|
760
|
-
}
|
|
761
|
-
},
|
|
762
|
-
|
|
763
|
-
async unreconcileMovement(bankMovementId) {
|
|
764
|
-
const line = store.movements.find((m) => m.id === bankMovementId)
|
|
765
|
-
if (!line) return
|
|
766
|
-
const matchedId = line.matchedMovementId
|
|
767
|
-
line.reconciledAt = undefined; line.matchedMovementId = undefined; line.updatedAt = now()
|
|
768
|
-
if (matchedId) {
|
|
769
|
-
const internal = store.movements.find((m) => m.id === matchedId)
|
|
770
|
-
if (internal) { internal.reconciledAt = undefined; internal.updatedAt = now() }
|
|
771
|
-
}
|
|
772
|
-
},
|
|
773
|
-
|
|
774
|
-
// --- Notas fiscais (NF-e / NFC-e / NFS-e) ---
|
|
775
|
-
async getNotas(query: NotaFiscalQuery): Promise<PaginatedResult<NotaFiscal>> {
|
|
776
|
-
let list = [...store.notas].sort((a, b) => b.createdAt.localeCompare(a.createdAt))
|
|
777
|
-
if (query.status) list = list.filter((n) => matchesStatus(n.status, query.status))
|
|
778
|
-
if (query.model) list = list.filter((n) => n.model === query.model)
|
|
779
|
-
if (query.orderId) list = list.filter((n) => n.orderId === query.orderId)
|
|
780
|
-
if (query.search) {
|
|
781
|
-
const q = query.search.toLowerCase()
|
|
782
|
-
list = list.filter((n) => (n.number ?? '').toLowerCase().includes(q) || (n.contactName ?? '').toLowerCase().includes(q))
|
|
783
|
-
}
|
|
784
|
-
return paginate(list, query.page, query.pageSize)
|
|
785
|
-
},
|
|
786
|
-
|
|
787
|
-
async getNotaById(id: string): Promise<NotaFiscal | null> {
|
|
788
|
-
return store.notas.find((n) => n.id === id) ?? null
|
|
789
|
-
},
|
|
790
|
-
|
|
791
|
-
async createNota(input: CreateNotaFiscalInput): Promise<NotaFiscal> {
|
|
792
|
-
const nota: NotaFiscal = {
|
|
793
|
-
id: uid(),
|
|
794
|
-
tenantId: 'mock-tenant',
|
|
795
|
-
model: input.model ?? 'nfe',
|
|
796
|
-
status: 'draft',
|
|
797
|
-
number: null,
|
|
798
|
-
series: input.series ?? null,
|
|
799
|
-
orderId: input.orderId,
|
|
800
|
-
contactId: input.contactId,
|
|
801
|
-
contactName: input.contactName,
|
|
802
|
-
totalAmount: input.totalAmount,
|
|
803
|
-
observations: input.observations,
|
|
804
|
-
createdAt: now(),
|
|
805
|
-
updatedAt: now(),
|
|
806
|
-
}
|
|
807
|
-
store.notas.unshift(nota)
|
|
808
|
-
return nota
|
|
809
|
-
},
|
|
810
|
-
|
|
811
|
-
async updateNota(id: string, patch: Partial<NotaFiscal>): Promise<NotaFiscal> {
|
|
812
|
-
const nota = store.notas.find((n) => n.id === id)
|
|
813
|
-
if (!nota) throw new Error('Nota fiscal not found')
|
|
814
|
-
Object.assign(nota, patch, { updatedAt: now() })
|
|
815
|
-
return nota
|
|
816
|
-
},
|
|
817
|
-
|
|
818
|
-
async issueNota(id: string): Promise<NotaFiscal> {
|
|
819
|
-
const nota = store.notas.find((n) => n.id === id)
|
|
820
|
-
if (!nota) throw new Error('Nota fiscal not found')
|
|
821
|
-
const issuedCount = store.notas.filter((n) => n.status === 'issued').length
|
|
822
|
-
nota.status = 'issued'
|
|
823
|
-
nota.number = String(issuedCount + 1).padStart(6, '0')
|
|
824
|
-
nota.series = nota.series ?? '1'
|
|
825
|
-
nota.issuedAt = now()
|
|
826
|
-
nota.emissor = 'manual'
|
|
827
|
-
nota.updatedAt = now()
|
|
828
|
-
return nota
|
|
829
|
-
},
|
|
830
|
-
|
|
831
|
-
async cancelNota(id: string, reason?: string): Promise<NotaFiscal> {
|
|
832
|
-
const nota = store.notas.find((n) => n.id === id)
|
|
833
|
-
if (!nota) throw new Error('Nota fiscal not found')
|
|
834
|
-
nota.status = 'cancelled'
|
|
835
|
-
nota.errorMessage = reason
|
|
836
|
-
nota.updatedAt = now()
|
|
837
|
-
return nota
|
|
838
|
-
},
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
return provider
|
|
842
|
-
}
|