@fayz-ai/plugin-financial 0.1.2 → 0.1.4
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/LICENSE +21 -0
- package/dist/FinancialContext.d.ts +2 -0
- package/dist/FinancialContext.d.ts.map +1 -1
- package/dist/FinancialPage.d.ts.map +1 -1
- package/dist/components/PaymentModal.d.ts.map +1 -1
- package/dist/components/PersonStatementWidget.d.ts +18 -0
- package/dist/components/PersonStatementWidget.d.ts.map +1 -0
- package/dist/components/TransferModal.d.ts +7 -0
- package/dist/components/TransferModal.d.ts.map +1 -0
- package/dist/contract.test.d.ts +2 -0
- package/dist/contract.test.d.ts.map +1 -0
- package/dist/data/capability.test.d.ts +2 -0
- package/dist/data/capability.test.d.ts.map +1 -0
- package/dist/data/mock.d.ts.map +1 -1
- package/dist/data/supabase.d.ts.map +1 -1
- package/dist/data/types.d.ts +13 -2
- package/dist/data/types.d.ts.map +1 -1
- package/dist/index.cjs +1529 -263
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1437 -171
- package/dist/index.js.map +1 -1
- package/dist/locales/en.d.ts.map +1 -1
- package/dist/locales/pt-BR.d.ts.map +1 -1
- package/dist/registries.d.ts.map +1 -1
- package/dist/store.d.ts +4 -2
- package/dist/store.d.ts.map +1 -1
- package/dist/types.d.ts +111 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/views/InvoiceDetailView.d.ts +3 -1
- package/dist/views/InvoiceDetailView.d.ts.map +1 -1
- package/dist/views/InvoiceFormView.d.ts.map +1 -1
- package/dist/views/PayablesView.d.ts +5 -1
- package/dist/views/PayablesView.d.ts.map +1 -1
- package/dist/views/ReceivablesView.d.ts +5 -1
- package/dist/views/ReceivablesView.d.ts.map +1 -1
- package/dist/views/ReconciliationView.d.ts +3 -0
- package/dist/views/ReconciliationView.d.ts.map +1 -0
- package/dist/views/StatementsView.d.ts +3 -1
- package/dist/views/StatementsView.d.ts.map +1 -1
- package/dist/views/navReferrer.d.ts +5 -0
- package/dist/views/navReferrer.d.ts.map +1 -0
- package/package.json +8 -5
- package/src/FinancialContext.tsx +2 -0
- package/src/FinancialPage.tsx +32 -17
- package/src/components/PaymentModal.tsx +58 -11
- package/src/components/PersonStatementWidget.tsx +133 -0
- package/src/components/TransferModal.tsx +150 -0
- package/src/contract.test.ts +58 -0
- package/src/data/capability.test.ts +90 -0
- package/src/data/mock.ts +265 -52
- package/src/data/supabase.ts +359 -30
- package/src/data/types.ts +22 -3
- package/src/index.ts +28 -2
- package/src/locales/en.ts +76 -0
- package/src/locales/pt-BR.ts +76 -0
- package/src/migrations/006_extract_fee_amount.sql +20 -0
- package/src/migrations/006_rls_policies.sql +37 -0
- package/src/migrations/007_reconciliation.sql +32 -0
- package/src/migrations/008_split_payment_movements.sql +29 -0
- package/src/registries.ts +3 -1
- package/src/store.ts +27 -7
- package/src/types.ts +126 -1
- package/src/views/InvoiceDetailView.tsx +70 -51
- package/src/views/InvoiceFormView.tsx +55 -20
- package/src/views/PayablesView.tsx +13 -2
- package/src/views/ReceivablesView.tsx +13 -2
- package/src/views/ReconciliationView.tsx +211 -0
- package/src/views/StatementsView.tsx +241 -29
- package/src/views/navReferrer.ts +24 -0
package/src/data/mock.ts
CHANGED
|
@@ -3,10 +3,10 @@ import type {
|
|
|
3
3
|
Invoice, InvoiceItem, FinancialMovement, BankAccount,
|
|
4
4
|
CashSession, PaymentMethod, PaymentMethodType, ChartOfAccountsNode,
|
|
5
5
|
CardTransaction,
|
|
6
|
-
CreateInvoiceInput, PayMovementInput,
|
|
6
|
+
CreateInvoiceInput, PayMovementInput, CreateTransferInput, TransferResult,
|
|
7
7
|
OpenCashSessionInput, CloseCashSessionInput, CreateBankAccountInput,
|
|
8
8
|
InvoiceQuery, MovementQuery, StatementQuery,
|
|
9
|
-
PaginatedResult, FinancialSummary, StatementEntry, CashSessionSummary,
|
|
9
|
+
PaginatedResult, FinancialSummary, StatementEntry, StatementResult, CashSessionSummary,
|
|
10
10
|
DateRange, InvoiceStatus, MovementStatus,
|
|
11
11
|
} from '../types'
|
|
12
12
|
|
|
@@ -27,6 +27,10 @@ function today(): string {
|
|
|
27
27
|
return new Date().toISOString().slice(0, 10)
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
function round2(n: number): number {
|
|
31
|
+
return Math.round(n * 100) / 100
|
|
32
|
+
}
|
|
33
|
+
|
|
30
34
|
function matchesDateRange(dateStr: string | undefined, range?: DateRange): boolean {
|
|
31
35
|
if (!range || !dateStr) return true
|
|
32
36
|
return dateStr >= range.from && dateStr <= range.to
|
|
@@ -281,43 +285,72 @@ export function createMockFinancialProvider(options?: MockFinancialProviderOptio
|
|
|
281
285
|
},
|
|
282
286
|
|
|
283
287
|
async payMovement(input: PayMovementInput): Promise<FinancialMovement> {
|
|
284
|
-
const
|
|
285
|
-
if (!
|
|
286
|
-
|
|
287
|
-
movement.paidAmount += input.amount
|
|
288
|
-
movement.paymentDate = input.paymentDate
|
|
289
|
-
movement.paymentMethodId = input.paymentMethodId ?? movement.paymentMethodId
|
|
290
|
-
movement.bankAccountId = input.bankAccountId ?? movement.bankAccountId
|
|
291
|
-
movement.cashSessionId = input.cashSessionId ?? movement.cashSessionId
|
|
292
|
-
movement.cardBrand = input.cardBrand
|
|
293
|
-
movement.cardInstallments = input.cardInstallments
|
|
294
|
-
movement.status = movement.paidAmount >= movement.amount ? 'paid' : 'partial'
|
|
295
|
-
movement.updatedAt = now()
|
|
288
|
+
const bill = store.movements.find((m) => m.id === input.movementId)
|
|
289
|
+
if (!bill) throw new Error(`Movement ${input.movementId} not found`)
|
|
296
290
|
|
|
297
|
-
//
|
|
298
|
-
|
|
299
|
-
|
|
291
|
+
// Net card settlement: derive the processing/MDR fee from the payment method (credits only).
|
|
292
|
+
const method = input.paymentMethodId ? store.paymentMethods.find((pm) => pm.id === input.paymentMethodId) : undefined
|
|
293
|
+
const feePct = bill.direction === 'credit' ? (method?.discountValue ?? 0) : 0
|
|
294
|
+
const thisFee = round2(input.amount * (feePct / 100))
|
|
295
|
+
|
|
296
|
+
// 1. Record the cash event as its OWN payment movement (one row per payment).
|
|
297
|
+
const payment: FinancialMovement = {
|
|
298
|
+
id: uid(),
|
|
299
|
+
invoiceId: bill.invoiceId,
|
|
300
|
+
direction: bill.direction,
|
|
301
|
+
movementKind: 'payment',
|
|
302
|
+
amount: input.amount,
|
|
303
|
+
paidAmount: input.amount,
|
|
304
|
+
feeAmount: thisFee,
|
|
305
|
+
status: 'paid',
|
|
306
|
+
dueDate: bill.dueDate,
|
|
307
|
+
paymentDate: input.paymentDate,
|
|
308
|
+
installmentNumber: bill.installmentNumber,
|
|
309
|
+
paymentMethodId: input.paymentMethodId,
|
|
310
|
+
paymentMethodTypeId: input.paymentMethodTypeId,
|
|
311
|
+
bankAccountId: input.bankAccountId,
|
|
312
|
+
cashSessionId: input.cashSessionId,
|
|
313
|
+
cardBrand: input.cardBrand,
|
|
314
|
+
cardInstallments: input.cardInstallments,
|
|
315
|
+
notes: input.notes,
|
|
316
|
+
tenantId,
|
|
317
|
+
createdAt: now(),
|
|
318
|
+
updatedAt: now(),
|
|
319
|
+
}
|
|
320
|
+
store.movements.push(payment)
|
|
321
|
+
|
|
322
|
+
// 2. Update the bill (obligation) cache: cumulative paid + status. Clear its cash
|
|
323
|
+
// fields so it stops being a cash event (the payment row is now the cash event).
|
|
324
|
+
bill.paidAmount = round2(bill.paidAmount + input.amount)
|
|
325
|
+
bill.status = bill.paidAmount >= bill.amount ? 'paid' : 'partial'
|
|
326
|
+
bill.paymentDate = undefined
|
|
327
|
+
bill.bankAccountId = undefined
|
|
328
|
+
bill.updatedAt = now()
|
|
329
|
+
|
|
330
|
+
// Update bank account balance by the NET cash impact (credit nets the fee).
|
|
331
|
+
if (input.bankAccountId) {
|
|
332
|
+
const account = store.bankAccounts.find((a) => a.id === input.bankAccountId)
|
|
300
333
|
if (account) {
|
|
301
|
-
if (
|
|
302
|
-
account.currentBalance
|
|
334
|
+
if (bill.direction === 'credit') {
|
|
335
|
+
account.currentBalance = round2(account.currentBalance + (input.amount - thisFee))
|
|
303
336
|
} else {
|
|
304
|
-
account.currentBalance
|
|
337
|
+
account.currentBalance = round2(account.currentBalance - input.amount)
|
|
305
338
|
}
|
|
306
339
|
account.updatedAt = now()
|
|
307
340
|
}
|
|
308
341
|
}
|
|
309
342
|
|
|
310
|
-
// Recalculate invoice status
|
|
311
|
-
if (
|
|
312
|
-
const invoice = store.invoices.find((i) => i.id ===
|
|
343
|
+
// 3. Recalculate invoice status from its bills
|
|
344
|
+
if (bill.invoiceId) {
|
|
345
|
+
const invoice = store.invoices.find((i) => i.id === bill.invoiceId)
|
|
313
346
|
if (invoice) {
|
|
314
|
-
const
|
|
315
|
-
invoice.paidAmount =
|
|
347
|
+
const bills = store.movements.filter((m) => m.invoiceId === invoice.id && m.movementKind === 'bill')
|
|
348
|
+
invoice.paidAmount = bills.reduce((sum, m) => sum + m.paidAmount, 0)
|
|
316
349
|
recalcInvoiceStatus(invoice)
|
|
317
350
|
}
|
|
318
351
|
}
|
|
319
352
|
|
|
320
|
-
return
|
|
353
|
+
return payment
|
|
321
354
|
},
|
|
322
355
|
|
|
323
356
|
async cancelMovement(id: string): Promise<void> {
|
|
@@ -327,6 +360,44 @@ export function createMockFinancialProvider(options?: MockFinancialProviderOptio
|
|
|
327
360
|
movement.updatedAt = now()
|
|
328
361
|
},
|
|
329
362
|
|
|
363
|
+
// --- Transfers (between accounts) ---
|
|
364
|
+
async createTransfer(input: CreateTransferInput): Promise<TransferResult> {
|
|
365
|
+
if (!input.fromAccountId || !input.toAccountId || input.fromAccountId === input.toAccountId) {
|
|
366
|
+
throw new Error('Source and destination accounts must differ')
|
|
367
|
+
}
|
|
368
|
+
if (!(input.amount > 0)) throw new Error('Amount must be greater than zero')
|
|
369
|
+
const transferId = uid()
|
|
370
|
+
const debitMovementId = uid()
|
|
371
|
+
const creditMovementId = uid()
|
|
372
|
+
const base = {
|
|
373
|
+
movementKind: 'transfer' as const,
|
|
374
|
+
amount: input.amount,
|
|
375
|
+
paidAmount: input.amount,
|
|
376
|
+
feeAmount: 0,
|
|
377
|
+
status: 'paid' as const,
|
|
378
|
+
dueDate: input.date,
|
|
379
|
+
paymentDate: input.date,
|
|
380
|
+
notes: input.notes,
|
|
381
|
+
tenantId,
|
|
382
|
+
createdAt: now(),
|
|
383
|
+
updatedAt: now(),
|
|
384
|
+
}
|
|
385
|
+
store.movements.push({
|
|
386
|
+
...base, id: debitMovementId, direction: 'debit', bankAccountId: input.fromAccountId,
|
|
387
|
+
metadata: { transferId, transferRole: 'out', counterAccountId: input.toAccountId },
|
|
388
|
+
} as FinancialMovement)
|
|
389
|
+
store.movements.push({
|
|
390
|
+
...base, id: creditMovementId, direction: 'credit', bankAccountId: input.toAccountId,
|
|
391
|
+
metadata: { transferId, transferRole: 'in', counterAccountId: input.fromAccountId },
|
|
392
|
+
} as FinancialMovement)
|
|
393
|
+
// Reflect in stored balances.
|
|
394
|
+
const from = store.bankAccounts.find((a) => a.id === input.fromAccountId)
|
|
395
|
+
const to = store.bankAccounts.find((a) => a.id === input.toAccountId)
|
|
396
|
+
if (from) { from.currentBalance = round2(from.currentBalance - input.amount); from.updatedAt = now() }
|
|
397
|
+
if (to) { to.currentBalance = round2(to.currentBalance + input.amount); to.updatedAt = now() }
|
|
398
|
+
return { transferId, debitMovementId, creditMovementId }
|
|
399
|
+
},
|
|
400
|
+
|
|
330
401
|
// --- Bank Accounts ---
|
|
331
402
|
async getBankAccounts(): Promise<BankAccount[]> {
|
|
332
403
|
return store.bankAccounts.filter((a) => a.isActive)
|
|
@@ -447,35 +518,99 @@ export function createMockFinancialProvider(options?: MockFinancialProviderOptio
|
|
|
447
518
|
return results.sort((a, b) => a.expectedDate.localeCompare(b.expectedDate))
|
|
448
519
|
},
|
|
449
520
|
|
|
450
|
-
// --- Statements ---
|
|
451
|
-
async getStatement(query: StatementQuery): Promise<
|
|
452
|
-
const
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
const
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
521
|
+
// --- Statements (ERP extract: opening balance + realized cash + per-row net/fee) ---
|
|
522
|
+
async getStatement(query: StatementQuery): Promise<StatementResult> {
|
|
523
|
+
const todayStr = today()
|
|
524
|
+
// A cash event = any non-cancelled movement with a payment_date (payment, transfer,
|
|
525
|
+
// or a legacy in-place-paid bill). Bills lose their payment_date once split.
|
|
526
|
+
const isRealized = (m: FinancialMovement) => m.status !== 'cancelled' && !!m.paymentDate
|
|
527
|
+
const consolidated = !query.bankAccountId
|
|
528
|
+
const scopeAccounts = consolidated
|
|
529
|
+
? store.bankAccounts.filter((a) => a.isActive)
|
|
530
|
+
: store.bankAccounts.filter((a) => a.id === query.bankAccountId)
|
|
531
|
+
const accountIds = new Set(scopeAccounts.map((a) => a.id))
|
|
532
|
+
const accountNameById = new Map(store.bankAccounts.map((a) => [a.id, a.name]))
|
|
533
|
+
const initialSum = scopeAccounts.reduce((s, a) => s + a.initialBalance, 0)
|
|
534
|
+
// Only bail when a specific account was asked for and not found.
|
|
535
|
+
if (!consolidated && accountIds.size === 0) {
|
|
536
|
+
return { entries: [], openingBalance: 0, closingBalance: 0, totalCredits: 0, totalDebits: 0, totalFees: 0, net: 0, accountId: query.bankAccountId }
|
|
537
|
+
}
|
|
465
538
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
539
|
+
const netOf = (m: FinancialMovement) => m.direction === 'credit'
|
|
540
|
+
? m.paidAmount - (m.feeAmount ?? 0)
|
|
541
|
+
: -m.paidAmount
|
|
542
|
+
|
|
543
|
+
// Consolidated => all realized movements (incl. those with no bank account).
|
|
544
|
+
const inScope = store.movements.filter((m) =>
|
|
545
|
+
isRealized(m) && (consolidated || (m.bankAccountId && accountIds.has(m.bankAccountId))))
|
|
546
|
+
|
|
547
|
+
const openingBalance = round2(initialSum + inScope
|
|
548
|
+
.filter((m) => (m.paymentDate ?? '') < query.dateRange.from)
|
|
549
|
+
.reduce((s, m) => s + netOf(m), 0))
|
|
550
|
+
|
|
551
|
+
const rangeMovs = inScope
|
|
552
|
+
.filter((m) => matchesDateRange(m.paymentDate, query.dateRange))
|
|
553
|
+
.sort((a, b) => (a.paymentDate ?? '').localeCompare(b.paymentDate ?? '') || a.createdAt.localeCompare(b.createdAt))
|
|
554
|
+
|
|
555
|
+
let balance = openingBalance
|
|
556
|
+
let totalCredits = 0, totalDebits = 0, totalFees = 0
|
|
557
|
+
const entries: StatementEntry[] = rangeMovs.map((movement) => {
|
|
558
|
+
const gross = movement.paidAmount
|
|
559
|
+
const isCredit = movement.direction === 'credit'
|
|
560
|
+
const fee = isCredit ? (movement.feeAmount ?? 0) : 0
|
|
561
|
+
const net = isCredit ? round2(gross - fee) : gross
|
|
562
|
+
const isTransfer = movement.movementKind === 'transfer'
|
|
563
|
+
const meta = (movement.metadata ?? {}) as any
|
|
564
|
+
const entryKind: StatementEntry['entryKind'] = isTransfer
|
|
565
|
+
? (meta.transferRole === 'in' ? 'transfer-in' : 'transfer-out')
|
|
566
|
+
: 'movement'
|
|
567
|
+
const counterAccountId = isTransfer ? meta.counterAccountId : undefined
|
|
568
|
+
balance = round2(isCredit ? balance + net : balance - net)
|
|
569
|
+
if (!(consolidated && isTransfer)) {
|
|
570
|
+
if (isCredit) totalCredits = round2(totalCredits + net)
|
|
571
|
+
else totalDebits = round2(totalDebits + net)
|
|
572
|
+
totalFees = round2(totalFees + fee)
|
|
573
|
+
}
|
|
574
|
+
return {
|
|
575
|
+
movement,
|
|
576
|
+
invoice: movement.invoiceId ? store.invoices.find((i) => i.id === movement.invoiceId) : undefined,
|
|
577
|
+
runningBalance: balance,
|
|
578
|
+
gross, fee, net, entryKind,
|
|
579
|
+
counterAccountId,
|
|
580
|
+
counterAccountName: counterAccountId ? accountNameById.get(counterAccountId) : undefined,
|
|
581
|
+
} as StatementEntry
|
|
582
|
+
})
|
|
583
|
+
|
|
584
|
+
// Link reconciled bank lines to the internal movement they were matched to.
|
|
585
|
+
for (const e of entries) {
|
|
586
|
+
const mid = e.movement.matchedMovementId
|
|
587
|
+
const mm = mid ? store.movements.find((m) => m.id === mid) : undefined
|
|
588
|
+
if (!mm) continue
|
|
589
|
+
const inv = mm.invoiceId ? store.invoices.find((i) => i.id === mm.invoiceId) : undefined
|
|
590
|
+
e.reconciledWith = {
|
|
591
|
+
movementId: mm.id,
|
|
592
|
+
description: mm.notes ?? undefined,
|
|
593
|
+
invoiceId: mm.invoiceId,
|
|
594
|
+
fiscalNumber: inv?.fiscalNumber,
|
|
595
|
+
contactName: inv?.contactName,
|
|
471
596
|
}
|
|
472
|
-
const invoice = movement.invoiceId
|
|
473
|
-
? store.invoices.find((i) => i.id === movement.invoiceId)
|
|
474
|
-
: undefined
|
|
475
|
-
entries.push({ movement, invoice, runningBalance })
|
|
476
597
|
}
|
|
477
598
|
|
|
478
|
-
|
|
599
|
+
const accountCurrentBalance = round2(initialSum + inScope
|
|
600
|
+
.filter((m) => (m.paymentDate ?? '') <= todayStr)
|
|
601
|
+
.reduce((s, m) => s + netOf(m), 0))
|
|
602
|
+
|
|
603
|
+
return {
|
|
604
|
+
entries,
|
|
605
|
+
openingBalance,
|
|
606
|
+
closingBalance: balance,
|
|
607
|
+
totalCredits,
|
|
608
|
+
totalDebits,
|
|
609
|
+
totalFees,
|
|
610
|
+
net: round2(totalCredits - totalDebits),
|
|
611
|
+
accountId: query.bankAccountId,
|
|
612
|
+
accountCurrentBalance,
|
|
613
|
+
}
|
|
479
614
|
},
|
|
480
615
|
|
|
481
616
|
// --- Summary ---
|
|
@@ -496,7 +631,7 @@ export function createMockFinancialProvider(options?: MockFinancialProviderOptio
|
|
|
496
631
|
const monthEnd = todayStr
|
|
497
632
|
|
|
498
633
|
const monthMovements = store.movements.filter((m) =>
|
|
499
|
-
m.
|
|
634
|
+
m.movementKind === 'payment' && matchesDateRange(m.paymentDate, dateRange ?? { from: monthStart, to: monthEnd })
|
|
500
635
|
)
|
|
501
636
|
|
|
502
637
|
const overdueReceivable = pendingReceivable.filter((m) => m.dueDate < todayStr)
|
|
@@ -514,6 +649,84 @@ export function createMockFinancialProvider(options?: MockFinancialProviderOptio
|
|
|
514
649
|
overduePayableAmount: overduePayable.reduce((s, m) => s + (m.amount - m.paidAmount), 0),
|
|
515
650
|
}
|
|
516
651
|
},
|
|
652
|
+
|
|
653
|
+
// --- Reconciliation (conciliação) ---
|
|
654
|
+
async importBankTransactions(input) {
|
|
655
|
+
let imported = 0, duplicates = 0
|
|
656
|
+
for (const line of input.lines) {
|
|
657
|
+
const dup = store.movements.some((m) => m.externalSource === line.externalSource && m.externalId === line.externalId)
|
|
658
|
+
if (dup) { duplicates++; continue }
|
|
659
|
+
store.movements.push({
|
|
660
|
+
id: uid(),
|
|
661
|
+
direction: line.direction,
|
|
662
|
+
movementKind: 'payment',
|
|
663
|
+
amount: line.amount,
|
|
664
|
+
paidAmount: line.amount,
|
|
665
|
+
status: 'paid',
|
|
666
|
+
dueDate: line.date,
|
|
667
|
+
paymentDate: line.date,
|
|
668
|
+
bankAccountId: line.bankAccountId ?? input.bankAccountId,
|
|
669
|
+
notes: line.description,
|
|
670
|
+
externalId: line.externalId,
|
|
671
|
+
externalSource: line.externalSource,
|
|
672
|
+
tenantId,
|
|
673
|
+
createdAt: now(),
|
|
674
|
+
updatedAt: now(),
|
|
675
|
+
} as FinancialMovement)
|
|
676
|
+
imported++
|
|
677
|
+
}
|
|
678
|
+
return { imported, duplicates }
|
|
679
|
+
},
|
|
680
|
+
|
|
681
|
+
async getUnreconciled(query) {
|
|
682
|
+
let results = store.movements.filter((m) => m.externalSource && !m.reconciledAt)
|
|
683
|
+
if (query?.bankAccountId) results = results.filter((m) => m.bankAccountId === query.bankAccountId)
|
|
684
|
+
if (query?.dateRange) results = results.filter((m) => matchesDateRange(m.paymentDate, query.dateRange))
|
|
685
|
+
return results.sort((a, b) => (b.paymentDate ?? '').localeCompare(a.paymentDate ?? ''))
|
|
686
|
+
},
|
|
687
|
+
|
|
688
|
+
async suggestReconciliation(bankMovementId) {
|
|
689
|
+
const line = store.movements.find((m) => m.id === bankMovementId)
|
|
690
|
+
if (!line) return []
|
|
691
|
+
const amount = line.amount
|
|
692
|
+
const tol = Math.max(0.01, round2(amount * 0.02))
|
|
693
|
+
const lineDate = line.paymentDate ?? line.dueDate
|
|
694
|
+
return store.movements
|
|
695
|
+
.filter((m) => !m.externalSource && !m.reconciledAt && m.direction === line.direction && m.id !== bankMovementId && Math.abs(m.amount - amount) <= tol)
|
|
696
|
+
.map((m) => {
|
|
697
|
+
const amtScore = amount > 0 ? Math.max(0, 1 - Math.abs(m.amount - amount) / amount) : 0
|
|
698
|
+
const refDate = m.dueDate ?? m.paymentDate
|
|
699
|
+
const days = lineDate && refDate ? Math.abs((new Date(lineDate).getTime() - new Date(refDate).getTime()) / 86400000) : 30
|
|
700
|
+
const dateScore = Math.max(0, 1 - days / 30)
|
|
701
|
+
return {
|
|
702
|
+
movement: m,
|
|
703
|
+
invoice: m.invoiceId ? store.invoices.find((i) => i.id === m.invoiceId) : undefined,
|
|
704
|
+
score: round2(0.7 * amtScore + 0.3 * dateScore),
|
|
705
|
+
}
|
|
706
|
+
})
|
|
707
|
+
.sort((a, b) => b.score - a.score)
|
|
708
|
+
.slice(0, 25)
|
|
709
|
+
},
|
|
710
|
+
|
|
711
|
+
async reconcileMovement(input) {
|
|
712
|
+
const line = store.movements.find((m) => m.id === input.bankMovementId)
|
|
713
|
+
if (line) { line.reconciledAt = now(); line.matchedMovementId = input.matchedMovementId; line.updatedAt = now() }
|
|
714
|
+
if (input.matchedMovementId) {
|
|
715
|
+
const internal = store.movements.find((m) => m.id === input.matchedMovementId)
|
|
716
|
+
if (internal) { internal.reconciledAt = now(); internal.updatedAt = now() }
|
|
717
|
+
}
|
|
718
|
+
},
|
|
719
|
+
|
|
720
|
+
async unreconcileMovement(bankMovementId) {
|
|
721
|
+
const line = store.movements.find((m) => m.id === bankMovementId)
|
|
722
|
+
if (!line) return
|
|
723
|
+
const matchedId = line.matchedMovementId
|
|
724
|
+
line.reconciledAt = undefined; line.matchedMovementId = undefined; line.updatedAt = now()
|
|
725
|
+
if (matchedId) {
|
|
726
|
+
const internal = store.movements.find((m) => m.id === matchedId)
|
|
727
|
+
if (internal) { internal.reconciledAt = undefined; internal.updatedAt = now() }
|
|
728
|
+
}
|
|
729
|
+
},
|
|
517
730
|
}
|
|
518
731
|
|
|
519
732
|
return provider
|