@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.
Files changed (95) hide show
  1. package/README.md +3 -3
  2. package/dist/components/PersonStatementWidget.d.ts +1 -1
  3. package/dist/components/PersonStatementWidget.d.ts.map +1 -1
  4. package/dist/{FinancialContext.d.ts → context.d.ts} +4 -2
  5. package/dist/context.d.ts.map +1 -0
  6. package/dist/data/registries.d.ts.map +1 -0
  7. package/dist/index.d.ts +13 -4
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +1426 -1426
  10. package/dist/index.js.map +1 -1
  11. package/dist/integrations/stripe/connectorDef.d.ts +14 -0
  12. package/dist/integrations/stripe/connectorDef.d.ts.map +1 -0
  13. package/dist/integrations/stripe/data.d.ts +43 -0
  14. package/dist/integrations/stripe/data.d.ts.map +1 -0
  15. package/dist/lib/message-events.d.ts +12 -0
  16. package/dist/lib/message-events.d.ts.map +1 -0
  17. package/dist/{quick-add.d.ts → lib/quick-add.d.ts} +1 -1
  18. package/dist/lib/quick-add.d.ts.map +1 -0
  19. package/dist/locales/en.d.ts.map +1 -1
  20. package/dist/locales/pt-BR.d.ts.map +1 -1
  21. package/dist/{FinancialPage.d.ts → views/FinancialPage.d.ts} +3 -3
  22. package/dist/views/FinancialPage.d.ts.map +1 -0
  23. package/dist/views/InvoiceDetailView.d.ts.map +1 -1
  24. package/dist/views/InvoiceFormView.d.ts.map +1 -1
  25. package/dist/views/InvoiceListView.d.ts.map +1 -1
  26. package/dist/views/PayablesView.d.ts +1 -1
  27. package/dist/views/PayablesView.d.ts.map +1 -1
  28. package/dist/views/ReceivablesView.d.ts +1 -1
  29. package/dist/views/ReceivablesView.d.ts.map +1 -1
  30. package/dist/views/StatementsView.d.ts.map +1 -1
  31. package/dist/views/dashboardWidgets.d.ts +1 -1
  32. package/dist/views/dashboardWidgets.d.ts.map +1 -1
  33. package/package.json +12 -14
  34. package/dist/FinancialContext.d.ts.map +0 -1
  35. package/dist/FinancialPage.d.ts.map +0 -1
  36. package/dist/contract.test.d.ts +0 -2
  37. package/dist/contract.test.d.ts.map +0 -1
  38. package/dist/quick-add.d.ts.map +0 -1
  39. package/dist/registries.d.ts.map +0 -1
  40. package/src/FinancialContext.tsx +0 -59
  41. package/src/FinancialPage.tsx +0 -413
  42. package/src/README.md +0 -155
  43. package/src/components/FinancialGeneralSettings.tsx +0 -39
  44. package/src/components/FinancialOnboarding.tsx +0 -87
  45. package/src/components/FinancialSettings.tsx +0 -31
  46. package/src/components/PaymentModal.tsx +0 -369
  47. package/src/components/PersonStatementWidget.tsx +0 -133
  48. package/src/components/TransferModal.tsx +0 -150
  49. package/src/contract.test.ts +0 -58
  50. package/src/data/capability.test.ts +0 -135
  51. package/src/data/index.ts +0 -3
  52. package/src/data/mock.ts +0 -842
  53. package/src/data/supabase.ts +0 -968
  54. package/src/data/tables.ts +0 -13
  55. package/src/data/types.ts +0 -86
  56. package/src/index.ts +0 -487
  57. package/src/lib/tenant.ts +0 -3
  58. package/src/locales/en.ts +0 -493
  59. package/src/locales/index.ts +0 -7
  60. package/src/locales/pt-BR.ts +0 -493
  61. package/src/migrations/000_plg_rename.sql +0 -30
  62. package/src/migrations/001_financial_base.sql +0 -109
  63. package/src/migrations/002_chart_of_accounts.sql +0 -28
  64. package/src/migrations/003_card_brands.sql +0 -12
  65. package/src/migrations/004_order_to_cash.sql +0 -178
  66. package/src/migrations/005_seed_defaults.sql +0 -38
  67. package/src/migrations/006_rls_policies.sql +0 -37
  68. package/src/migrations/006b_extract_fee_amount.sql +0 -20
  69. package/src/migrations/007_reconciliation.sql +0 -32
  70. package/src/migrations/007b_movement_payment_method_type.sql +0 -6
  71. package/src/migrations/008_split_payment_movements.sql +0 -29
  72. package/src/migrations/009_agent_rpcs.sql +0 -165
  73. package/src/migrations/010_invoice_internal.sql +0 -133
  74. package/src/migrations/011_nfe.sql +0 -58
  75. package/src/migrations/index.ts +0 -923
  76. package/src/quick-add.ts +0 -58
  77. package/src/registries.ts +0 -308
  78. package/src/store.ts +0 -329
  79. package/src/types.ts +0 -592
  80. package/src/views/CardsView.tsx +0 -213
  81. package/src/views/CashRegistersView.tsx +0 -597
  82. package/src/views/CommissionsView.tsx +0 -18
  83. package/src/views/InvoiceDetailView.tsx +0 -616
  84. package/src/views/InvoiceFormView.tsx +0 -945
  85. package/src/views/InvoiceListView.tsx +0 -323
  86. package/src/views/NotasFiscaisView.tsx +0 -209
  87. package/src/views/PayablesView.tsx +0 -48
  88. package/src/views/QuickTransactionForm.tsx +0 -548
  89. package/src/views/ReceivablesView.tsx +0 -48
  90. package/src/views/ReconciliationView.tsx +0 -211
  91. package/src/views/StatementsView.tsx +0 -335
  92. package/src/views/SummaryView.tsx +0 -203
  93. package/src/views/dashboardWidgets.tsx +0 -437
  94. package/src/views/navReferrer.ts +0 -24
  95. /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
- }