@fayz-ai/plugin-financial 0.2.0 → 0.8.1

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 (53) hide show
  1. package/README.md +2 -0
  2. package/dist/FinancialContext.d.ts +6 -0
  3. package/dist/FinancialContext.d.ts.map +1 -1
  4. package/dist/FinancialPage.d.ts.map +1 -1
  5. package/dist/components/FinancialGeneralSettings.d.ts.map +1 -1
  6. package/dist/data/supabase.d.ts.map +1 -1
  7. package/dist/data/tables.d.ts +11 -0
  8. package/dist/data/tables.d.ts.map +1 -0
  9. package/dist/index.cjs +345 -81
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.d.ts +31 -0
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +347 -84
  14. package/dist/index.js.map +1 -1
  15. package/dist/locales/en.d.ts.map +1 -1
  16. package/dist/locales/pt-BR.d.ts.map +1 -1
  17. package/dist/migrations/index.d.ts +18 -0
  18. package/dist/migrations/index.d.ts.map +1 -0
  19. package/dist/registries.d.ts.map +1 -1
  20. package/dist/views/CardsView.d.ts.map +1 -1
  21. package/dist/views/InvoiceFormView.d.ts.map +1 -1
  22. package/dist/views/InvoiceListView.d.ts.map +1 -1
  23. package/dist/views/QuickTransactionForm.d.ts.map +1 -1
  24. package/package.json +10 -4
  25. package/src/FinancialContext.tsx +6 -0
  26. package/src/FinancialPage.tsx +56 -12
  27. package/src/README.md +5 -5
  28. package/src/components/FinancialGeneralSettings.tsx +21 -8
  29. package/src/contract.test.ts +1 -1
  30. package/src/data/supabase.ts +56 -48
  31. package/src/data/tables.ts +12 -0
  32. package/src/index.ts +128 -0
  33. package/src/locales/en.ts +10 -0
  34. package/src/locales/pt-BR.ts +10 -0
  35. package/src/migrations/000_plg_rename.sql +30 -0
  36. package/src/migrations/001_financial_base.sql +30 -30
  37. package/src/migrations/002_chart_of_accounts.sql +10 -10
  38. package/src/migrations/003_card_brands.sql +4 -4
  39. package/src/migrations/004_order_to_cash.sql +43 -29
  40. package/src/migrations/005_seed_defaults.sql +6 -6
  41. package/src/migrations/006_rls_policies.sql +5 -5
  42. package/src/migrations/{006_extract_fee_amount.sql → 006b_extract_fee_amount.sql} +4 -4
  43. package/src/migrations/007_reconciliation.sql +9 -9
  44. package/src/migrations/007b_movement_payment_method_type.sql +6 -0
  45. package/src/migrations/008_split_payment_movements.sql +5 -5
  46. package/src/migrations/009_agent_rpcs.sql +165 -0
  47. package/src/migrations/010_invoice_internal.sql +133 -0
  48. package/src/migrations/index.ts +862 -0
  49. package/src/registries.ts +8 -7
  50. package/src/views/CardsView.tsx +12 -9
  51. package/src/views/InvoiceFormView.tsx +10 -0
  52. package/src/views/InvoiceListView.tsx +98 -9
  53. package/src/views/QuickTransactionForm.tsx +11 -0
@@ -11,6 +11,7 @@ import type {
11
11
  } from '../types'
12
12
  import { getSupabaseClientOptional, getActiveTenantId } from '@fayz-ai/core'
13
13
  import { getFinancialTenantId } from '../lib/tenant'
14
+ import { T } from './tables'
14
15
 
15
16
  function getTenantId(): string | undefined {
16
17
  // Local override wins; else use the app's active tenant so writes pass RLS.
@@ -37,7 +38,9 @@ function camelToSnake(obj: Record<string, unknown>): Record<string, unknown> {
37
38
  function getClients() {
38
39
  const supabase = getSupabaseClientOptional() as any
39
40
  if (!supabase) throw new Error('Supabase not initialized')
40
- return { core: supabase.schema('saas_core'), pub: supabase }
41
+ // Core tables now live in PUBLIC (legacy dedicated core schema is gone). Both
42
+ // handles are the single public client; `core` retained for call-site readability.
43
+ return { core: supabase, pub: supabase }
41
44
  }
42
45
 
43
46
  /** Map order stage to InvoiceStatus for the financial UI */
@@ -64,7 +67,7 @@ async function sweepOverdue() {
64
67
  const today = new Date().toISOString().slice(0, 10)
65
68
 
66
69
  // Mark overdue movements
67
- const { data: overdueMovs } = await pub.from('financial_movements')
70
+ const { data: overdueMovs } = await pub.from(T.movements)
68
71
  .update({ status: 'overdue' })
69
72
  .in('status', ['pending', 'partial'])
70
73
  .lt('due_date', today)
@@ -74,7 +77,7 @@ async function sweepOverdue() {
74
77
  if (overdueMovs && overdueMovs.length > 0) {
75
78
  const invoiceIds = [...new Set(overdueMovs.map((m: any) => m.invoice_id).filter(Boolean))]
76
79
  for (const invId of invoiceIds) {
77
- const { data: allMovs } = await pub.from('financial_movements')
80
+ const { data: allMovs } = await pub.from(T.movements)
78
81
  .select('status')
79
82
  .eq('invoice_id', invId)
80
83
  .eq('movement_kind', 'bill')
@@ -95,7 +98,7 @@ async function sweepOverdue() {
95
98
  // S2 (data-model refactor): paid amount, balance and status are DERIVED from the
96
99
  // append-only ledger via v_invoice_balances — the single source of truth — not
97
100
  // from stored order.stage / metadata.paidAmount (which drift). See
98
- // docs/data-model-refactor.md.
101
+ // docs/DATA-MODEL.md.
99
102
  async function fetchInvoiceBalances(
100
103
  pub: any,
101
104
  ids: string[],
@@ -137,7 +140,7 @@ async function findOrderIdsByItemMetadata(
137
140
  /** Card/MDR fee percent for a configured payment method (0 when none / not a fee method). */
138
141
  async function getMethodFeePct(pub: any, paymentMethodId?: string): Promise<number> {
139
142
  if (!paymentMethodId) return 0
140
- const { data } = await pub.from('payment_methods').select('discount_value').eq('id', paymentMethodId).single()
143
+ const { data } = await pub.from(T.paymentMethods).select('discount_value').eq('id', paymentMethodId).single()
141
144
  return Number(data?.discount_value) || 0
142
145
  }
143
146
 
@@ -179,7 +182,12 @@ export function createSupabaseFinancialProvider(): FinancialDataProvider {
179
182
  }
180
183
  if (query.contactId) qb = qb.eq('party_id', query.contactId)
181
184
  if (query.search) qb = qb.ilike('notes', `%${query.search}%`)
182
- if (query.dateRange) qb = qb.gte('created_at', query.dateRange.from).lte('created_at', query.dateRange.to)
185
+ // `to` is a plain date from the UI filter — widen it to end-of-day so the
186
+ // last day of the range isn't cut off at midnight.
187
+ if (query.dateRange) {
188
+ const to = query.dateRange.to.length === 10 ? `${query.dateRange.to}T23:59:59.999` : query.dateRange.to
189
+ qb = qb.gte('created_at', query.dateRange.from).lte('created_at', to)
190
+ }
183
191
  const page = query.page ?? 1
184
192
  const pageSize = query.pageSize ?? 50
185
193
  qb = qb.range((page - 1) * pageSize, page * pageSize - 1).order('created_at', { ascending: false })
@@ -220,7 +228,7 @@ export function createSupabaseFinancialProvider(): FinancialDataProvider {
220
228
  const meta = data.metadata ?? {}
221
229
  // Paid amount + status are DERIVED from the ledger view (single source of truth).
222
230
  const balance = (await fetchInvoiceBalances(pub, [id])).get(id)
223
- const { data: movs } = await pub.from('financial_movements')
231
+ const { data: movs } = await pub.from(T.movements)
224
232
  .select('paid_amount')
225
233
  .eq('invoice_id', id)
226
234
  .eq('movement_kind', 'bill')
@@ -303,7 +311,7 @@ export function createSupabaseFinancialProvider(): FinancialDataProvider {
303
311
 
304
312
  // Create installment movements
305
313
  if (order && input.installments.length > 0) {
306
- await pub.from('financial_movements').insert(
314
+ await pub.from(T.movements).insert(
307
315
  input.installments.map((inst, i) => ({
308
316
  tenant_id: tenantId, invoice_id: order.id,
309
317
  direction: input.direction, movement_kind: inst.movementKind,
@@ -335,13 +343,13 @@ export function createSupabaseFinancialProvider(): FinancialDataProvider {
335
343
  async cancelInvoice(id: string): Promise<void> {
336
344
  const { core, pub } = getClients()
337
345
  await core.from('orders').update({ status: 'cancelled' }).eq('id', id)
338
- await pub.from('financial_movements').update({ status: 'cancelled' }).eq('invoice_id', id).eq('status', 'pending')
346
+ await pub.from(T.movements).update({ status: 'cancelled' }).eq('invoice_id', id).eq('status', 'pending')
339
347
  },
340
348
 
341
349
  // --- Movements (public.financial_movements) ---
342
350
  async getMovements(query: MovementQuery): Promise<PaginatedResult<FinancialMovement>> {
343
351
  const { core, pub } = getClients()
344
- let qb = pub.from('financial_movements').select('*', { count: 'exact' })
352
+ let qb = pub.from(T.movements).select('*', { count: 'exact' })
345
353
  if (query.direction) qb = qb.eq('direction', query.direction)
346
354
  if (query.status) {
347
355
  const statuses = Array.isArray(query.status) ? query.status : [query.status]
@@ -359,7 +367,7 @@ export function createSupabaseFinancialProvider(): FinancialDataProvider {
359
367
 
360
368
  async payMovement(input: PayMovementInput): Promise<FinancialMovement> {
361
369
  const { core, pub } = getClients()
362
- const { data: mov } = await pub.from('financial_movements').select('*').eq('id', input.movementId).single()
370
+ const { data: mov } = await pub.from(T.movements).select('*').eq('id', input.movementId).single()
363
371
  if (!mov) throw new Error('Movement not found')
364
372
  const tenantId = mov.tenant_id ?? getTenantId()
365
373
 
@@ -389,11 +397,11 @@ export function createSupabaseFinancialProvider(): FinancialDataProvider {
389
397
  card_installments: input.cardInstallments ?? null,
390
398
  notes: input.notes ?? null,
391
399
  }
392
- let { data, error } = await pub.from('financial_movements').insert(paymentRow).select().single()
400
+ let { data, error } = await pub.from(T.movements).insert(paymentRow).select().single()
393
401
  // Graceful fallback if the fee_amount migration hasn't been applied yet.
394
402
  if (error && /fee_amount/i.test(error.message ?? '')) {
395
403
  const { fee_amount, ...rest } = paymentRow
396
- ;({ data, error } = await pub.from('financial_movements').insert(rest).select().single())
404
+ ;({ data, error } = await pub.from(T.movements).insert(rest).select().single())
397
405
  }
398
406
  if (error) throw error
399
407
 
@@ -404,13 +412,13 @@ export function createSupabaseFinancialProvider(): FinancialDataProvider {
404
412
  const billStatus = newPaid >= mov.amount ? 'paid' : 'partial'
405
413
  // Clear the bill's cash fields so it stops being a cash event (the payment row is now
406
414
  // the cash event) — prevents double-counting legacy in-place-paid bills.
407
- await pub.from('financial_movements')
415
+ await pub.from(T.movements)
408
416
  .update({ paid_amount: newPaid, status: billStatus, payment_date: null, bank_account_id: null })
409
417
  .eq('id', mov.id)
410
418
 
411
419
  // 3. Update invoice (order) status based on its bills
412
420
  if (mov.invoice_id) {
413
- const { data: allMovs } = await pub.from('financial_movements')
421
+ const { data: allMovs } = await pub.from(T.movements)
414
422
  .select('status, amount, paid_amount')
415
423
  .eq('invoice_id', mov.invoice_id)
416
424
  .eq('movement_kind', 'bill')
@@ -423,7 +431,7 @@ export function createSupabaseFinancialProvider(): FinancialDataProvider {
423
431
  const { data: order } = await core.from('orders').select('metadata').eq('id', mov.invoice_id).single()
424
432
  const meta = (order?.metadata as any) ?? {}
425
433
  const invoiceStage = allPaid ? 'paid' : anyPaid ? 'partial' : 'invoiced'
426
- // Only update financial order status. Agenda status lives on saas_core.bookings.
434
+ // Only update financial order status. Agenda status lives on public.appointments.
427
435
  await core.from('orders').update({
428
436
  status: invoiceStage,
429
437
  metadata: { ...meta, paidAmount: totalPaid },
@@ -435,7 +443,7 @@ export function createSupabaseFinancialProvider(): FinancialDataProvider {
435
443
 
436
444
  async cancelMovement(id: string): Promise<void> {
437
445
  const { core, pub } = getClients()
438
- await pub.from('financial_movements').update({ status: 'cancelled' }).eq('id', id)
446
+ await pub.from(T.movements).update({ status: 'cancelled' }).eq('id', id)
439
447
  },
440
448
 
441
449
  // --- Transfers (between accounts) ---
@@ -472,11 +480,11 @@ export function createSupabaseFinancialProvider(): FinancialDataProvider {
472
480
  metadata: { transferId, transferRole: 'in', counterAccountId: input.fromAccountId },
473
481
  },
474
482
  ]
475
- let { data, error } = await pub.from('financial_movements').insert(rowsToInsert).select('id, direction')
483
+ let { data, error } = await pub.from(T.movements).insert(rowsToInsert).select('id, direction')
476
484
  // Graceful fallback if the fee_amount migration hasn't been applied yet.
477
485
  if (error && /fee_amount/i.test(error.message ?? '')) {
478
486
  const stripped = rowsToInsert.map(({ fee_amount, ...rest }) => rest)
479
- ;({ data, error } = await pub.from('financial_movements').insert(stripped).select('id, direction'))
487
+ ;({ data, error } = await pub.from(T.movements).insert(stripped).select('id, direction'))
480
488
  }
481
489
  if (error) throw error
482
490
  const rows = (data ?? []) as Array<{ id: string; direction: string }>
@@ -488,14 +496,14 @@ export function createSupabaseFinancialProvider(): FinancialDataProvider {
488
496
  // --- Bank Accounts ---
489
497
  async getBankAccounts(): Promise<BankAccount[]> {
490
498
  const { core, pub } = getClients()
491
- const { data } = await pub.from('bank_accounts').select('*').eq('is_active', true).order('name')
499
+ const { data } = await pub.from(T.bankAccounts).select('*').eq('is_active', true).order('name')
492
500
  return (data ?? []).map((r: any) => snakeToCamel(r) as unknown as BankAccount)
493
501
  },
494
502
 
495
503
  async createBankAccount(input: CreateBankAccountInput): Promise<BankAccount> {
496
504
  const { core, pub } = getClients()
497
505
  const tenantId = getTenantId()
498
- const { data } = await pub.from('bank_accounts').insert({
506
+ const { data } = await pub.from(T.bankAccounts).insert({
499
507
  ...camelToSnake(input as any), tenant_id: tenantId, current_balance: input.initialBalance ?? 0,
500
508
  }).select().single()
501
509
  return snakeToCamel(data!) as unknown as BankAccount
@@ -505,14 +513,14 @@ export function createSupabaseFinancialProvider(): FinancialDataProvider {
505
513
  const { core, pub } = getClients()
506
514
  const row = camelToSnake(partial as any)
507
515
  delete row.id; delete row.tenant_id
508
- const { data } = await pub.from('bank_accounts').update(row).eq('id', id).select().single()
516
+ const { data } = await pub.from(T.bankAccounts).update(row).eq('id', id).select().single()
509
517
  return snakeToCamel(data!) as unknown as BankAccount
510
518
  },
511
519
 
512
520
  // --- Cash Sessions ---
513
521
  async getCashSessions(bankAccountId?: string): Promise<CashSession[]> {
514
522
  const { core, pub } = getClients()
515
- let qb = pub.from('cash_register_sessions').select('*')
523
+ let qb = pub.from(T.cashRegisterSessions).select('*')
516
524
  if (bankAccountId) qb = qb.eq('bank_account_id', bankAccountId)
517
525
  const { data } = await qb.order('opened_at', { ascending: false })
518
526
  return (data ?? []).map((r: any) => snakeToCamel(r) as unknown as CashSession)
@@ -520,14 +528,14 @@ export function createSupabaseFinancialProvider(): FinancialDataProvider {
520
528
 
521
529
  async getOpenSession(bankAccountId: string): Promise<CashSession | null> {
522
530
  const { core, pub } = getClients()
523
- const { data } = await pub.from('cash_register_sessions').select('*').eq('bank_account_id', bankAccountId).eq('status', 'open').single()
531
+ const { data } = await pub.from(T.cashRegisterSessions).select('*').eq('bank_account_id', bankAccountId).eq('status', 'open').single()
524
532
  return data ? snakeToCamel(data) as unknown as CashSession : null
525
533
  },
526
534
 
527
535
  async openCashSession(input: OpenCashSessionInput): Promise<CashSession> {
528
536
  const { core, pub } = getClients()
529
537
  const tenantId = getTenantId()
530
- const { data } = await pub.from('cash_register_sessions').insert({
538
+ const { data } = await pub.from(T.cashRegisterSessions).insert({
531
539
  tenant_id: tenantId, bank_account_id: input.bankAccountId,
532
540
  status: 'open', opening_balance: input.openingBalance,
533
541
  opened_by_user_id: input.openedByUserId, opened_by_name: input.openedByName,
@@ -538,7 +546,7 @@ export function createSupabaseFinancialProvider(): FinancialDataProvider {
538
546
 
539
547
  async closeCashSession(input: CloseCashSessionInput): Promise<CashSession> {
540
548
  const { core, pub } = getClients()
541
- const { data } = await pub.from('cash_register_sessions').update({
549
+ const { data } = await pub.from(T.cashRegisterSessions).update({
542
550
  status: 'closed', closing_balance: input.closingBalance,
543
551
  closed_at: new Date().toISOString(),
544
552
  closed_by_user_id: input.closedByUserId, closed_by_name: input.closedByName,
@@ -549,8 +557,8 @@ export function createSupabaseFinancialProvider(): FinancialDataProvider {
549
557
 
550
558
  async getCashSessionSummary(sessionId: string): Promise<CashSessionSummary> {
551
559
  const { core, pub } = getClients()
552
- const session = (await pub.from('cash_register_sessions').select('*').eq('id', sessionId).single()).data
553
- const { data: movs } = await pub.from('financial_movements').select('direction, paid_amount').eq('cash_session_id', sessionId).eq('status', 'paid')
560
+ const session = (await pub.from(T.cashRegisterSessions).select('*').eq('id', sessionId).single()).data
561
+ const { data: movs } = await pub.from(T.movements).select('direction, paid_amount').eq('cash_session_id', sessionId).eq('status', 'paid')
554
562
  const movements = movs ?? []
555
563
  return {
556
564
  session: snakeToCamel(session!) as unknown as CashSession,
@@ -563,26 +571,26 @@ export function createSupabaseFinancialProvider(): FinancialDataProvider {
563
571
  // --- Payment Methods ---
564
572
  async getPaymentMethods(): Promise<PaymentMethod[]> {
565
573
  const { core, pub } = getClients()
566
- const { data } = await pub.from('payment_methods').select('*').eq('is_active', true).order('name')
574
+ const { data } = await pub.from(T.paymentMethods).select('*').eq('is_active', true).order('name')
567
575
  return (data ?? []).map((r: any) => snakeToCamel(r) as unknown as PaymentMethod)
568
576
  },
569
577
 
570
578
  async getPaymentMethodTypes(): Promise<PaymentMethodType[]> {
571
579
  const { core, pub } = getClients()
572
- const { data } = await pub.from('payment_method_types').select('*').eq('is_active', true).order('name')
580
+ const { data } = await pub.from(T.paymentMethodTypes).select('*').eq('is_active', true).order('name')
573
581
  return (data ?? []).map((r: any) => snakeToCamel(r) as unknown as PaymentMethodType)
574
582
  },
575
583
 
576
584
  // --- Chart of Accounts ---
577
585
  async getChartOfAccounts(): Promise<ChartOfAccountsNode[]> {
578
586
  const { core, pub } = getClients()
579
- const { data } = await pub.from('chart_of_accounts').select('*').eq('is_active', true).order('code')
587
+ const { data } = await pub.from(T.chartOfAccounts).select('*').eq('is_active', true).order('code')
580
588
  return (data ?? []).map((r: any) => snakeToCamel(r) as unknown as ChartOfAccountsNode)
581
589
  },
582
590
 
583
591
  async getCostCenters(): Promise<CostCenter[]> {
584
592
  const { core, pub } = getClients()
585
- const { data } = await pub.from('cost_centers').select('*').eq('is_active', true).order('code')
593
+ const { data } = await pub.from(T.costCenters).select('*').eq('is_active', true).order('code')
586
594
  return (data ?? []).map((r: any) => snakeToCamel(r) as unknown as CostCenter)
587
595
  },
588
596
 
@@ -604,7 +612,7 @@ export function createSupabaseFinancialProvider(): FinancialDataProvider {
604
612
 
605
613
  // 1. Resolve in-scope accounts. No bankAccountId => consolidated: ALL realized cash
606
614
  // (including movements with no bank_account_id, e.g. cash/card payments not tied to an account).
607
- const { data: acctRows } = await pub.from('bank_accounts').select('id, name, initial_balance, is_active')
615
+ const { data: acctRows } = await pub.from(T.bankAccounts).select('id, name, initial_balance, is_active')
608
616
  const allAccounts = (acctRows ?? []) as any[]
609
617
  const consolidated = !query.bankAccountId
610
618
  const scopeAccounts = consolidated
@@ -626,7 +634,7 @@ export function createSupabaseFinancialProvider(): FinancialDataProvider {
626
634
 
627
635
  // 2. Opening balance = Σ initial_balance + prior net (payment_date < from).
628
636
  // Select '*' so this still works before the fee_amount migration lands (fee → 0).
629
- const { data: priorRows } = await scopeFilter(pub.from('financial_movements')
637
+ const { data: priorRows } = await scopeFilter(pub.from(T.movements)
630
638
  .select('*')
631
639
  .neq('status', 'cancelled')
632
640
  .lt('payment_date', query.dateRange.from))
@@ -634,7 +642,7 @@ export function createSupabaseFinancialProvider(): FinancialDataProvider {
634
642
  const openingBalance = round2(initialSum + priorNet)
635
643
 
636
644
  // 3. In-range realized rows.
637
- const { data: movs } = await scopeFilter(pub.from('financial_movements')
645
+ const { data: movs } = await scopeFilter(pub.from(T.movements)
638
646
  .select('*')
639
647
  .neq('status', 'cancelled')
640
648
  .gte('payment_date', query.dateRange.from).lte('payment_date', query.dateRange.to)
@@ -687,7 +695,7 @@ export function createSupabaseFinancialProvider(): FinancialDataProvider {
687
695
  // extract row can link to the internal transaction it was conciliated with.
688
696
  const matchedIds = [...new Set(entries.map((e) => e.movement.matchedMovementId).filter(Boolean))] as string[]
689
697
  if (matchedIds.length) {
690
- const { data: matched } = await pub.from('financial_movements').select('id, invoice_id, notes').in('id', matchedIds)
698
+ const { data: matched } = await pub.from(T.movements).select('id, invoice_id, notes').in('id', matchedIds)
691
699
  const matchedById = new Map<string, any>((matched ?? []).map((m: any) => [m.id, m]))
692
700
  const matchedInvoiceIds = [...new Set((matched ?? []).map((m: any) => m.invoice_id).filter(Boolean))] as string[]
693
701
  const invById = new Map<string, any>()
@@ -711,7 +719,7 @@ export function createSupabaseFinancialProvider(): FinancialDataProvider {
711
719
  }
712
720
 
713
721
  // 6. Account current balance (ledger as-of-today).
714
- const { data: todayRows } = await scopeFilter(pub.from('financial_movements')
722
+ const { data: todayRows } = await scopeFilter(pub.from(T.movements)
715
723
  .select('*')
716
724
  .neq('status', 'cancelled')
717
725
  .lte('payment_date', today))
@@ -735,10 +743,10 @@ export function createSupabaseFinancialProvider(): FinancialDataProvider {
735
743
  // Sweep overdue on first load (background, non-blocking)
736
744
  sweepOverdue()
737
745
  const { core, pub } = getClients()
738
- const { data: accounts } = await pub.from('bank_accounts').select('current_balance').eq('is_active', true)
746
+ const { data: accounts } = await pub.from(T.bankAccounts).select('current_balance').eq('is_active', true)
739
747
  const totalBalance = (accounts ?? []).reduce((s: number, a: any) => s + (a.current_balance ?? 0), 0)
740
748
 
741
- const { data: movs } = await pub.from('financial_movements').select('direction, movement_kind, amount, paid_amount, status, due_date, payment_date')
749
+ const { data: movs } = await pub.from(T.movements).select('direction, movement_kind, amount, paid_amount, status, due_date, payment_date')
742
750
  const allMovs = movs ?? []
743
751
  const unpaid = allMovs.filter((m: any) => ['pending', 'partial', 'overdue'].includes(m.status))
744
752
  const today = new Date().toISOString().slice(0, 10)
@@ -778,7 +786,7 @@ export function createSupabaseFinancialProvider(): FinancialDataProvider {
778
786
  const tenantId = getTenantId()
779
787
  let imported = 0, duplicates = 0
780
788
  for (const line of input.lines) {
781
- const { error } = await pub.from('financial_movements').insert({
789
+ const { error } = await pub.from(T.movements).insert({
782
790
  tenant_id: tenantId,
783
791
  direction: line.direction,
784
792
  movement_kind: 'payment',
@@ -803,7 +811,7 @@ export function createSupabaseFinancialProvider(): FinancialDataProvider {
803
811
 
804
812
  async getUnreconciled(query) {
805
813
  const { pub } = getClients()
806
- let qb = pub.from('financial_movements').select('*')
814
+ let qb = pub.from(T.movements).select('*')
807
815
  .not('external_source', 'is', null)
808
816
  .is('reconciled_at', null)
809
817
  if (query?.bankAccountId) qb = qb.eq('bank_account_id', query.bankAccountId)
@@ -814,13 +822,13 @@ export function createSupabaseFinancialProvider(): FinancialDataProvider {
814
822
 
815
823
  async suggestReconciliation(bankMovementId) {
816
824
  const { core, pub } = getClients()
817
- const { data: line } = await pub.from('financial_movements').select('*').eq('id', bankMovementId).single()
825
+ const { data: line } = await pub.from(T.movements).select('*').eq('id', bankMovementId).single()
818
826
  if (!line) return []
819
827
  const amount = Number(line.amount) || 0
820
828
  const lineDate = line.payment_date ?? line.due_date
821
829
  // Candidates: app-native (non-imported), same direction, not reconciled, amount within ±2%.
822
830
  const tol = Math.max(0.01, round2(amount * 0.02))
823
- const { data } = await pub.from('financial_movements').select('*')
831
+ const { data } = await pub.from(T.movements).select('*')
824
832
  .is('external_source', null)
825
833
  .is('reconciled_at', null)
826
834
  .eq('direction', line.direction)
@@ -857,22 +865,22 @@ export function createSupabaseFinancialProvider(): FinancialDataProvider {
857
865
  async reconcileMovement(input) {
858
866
  const { pub } = getClients()
859
867
  const now = new Date().toISOString()
860
- await pub.from('financial_movements').update({
868
+ await pub.from(T.movements).update({
861
869
  reconciled_at: now,
862
870
  matched_movement_id: input.matchedMovementId ?? null,
863
871
  }).eq('id', input.bankMovementId)
864
872
  if (input.matchedMovementId) {
865
873
  // The internal movement is now settled+reconciled by the bank line.
866
- await pub.from('financial_movements').update({ reconciled_at: now }).eq('id', input.matchedMovementId)
874
+ await pub.from(T.movements).update({ reconciled_at: now }).eq('id', input.matchedMovementId)
867
875
  }
868
876
  },
869
877
 
870
878
  async unreconcileMovement(bankMovementId) {
871
879
  const { pub } = getClients()
872
- const { data: line } = await pub.from('financial_movements').select('matched_movement_id').eq('id', bankMovementId).single()
873
- await pub.from('financial_movements').update({ reconciled_at: null, matched_movement_id: null }).eq('id', bankMovementId)
880
+ const { data: line } = await pub.from(T.movements).select('matched_movement_id').eq('id', bankMovementId).single()
881
+ await pub.from(T.movements).update({ reconciled_at: null, matched_movement_id: null }).eq('id', bankMovementId)
874
882
  if (line?.matched_movement_id) {
875
- await pub.from('financial_movements').update({ reconciled_at: null }).eq('id', line.matched_movement_id)
883
+ await pub.from(T.movements).update({ reconciled_at: null }).eq('id', line.matched_movement_id)
876
884
  }
877
885
  },
878
886
  }
@@ -0,0 +1,12 @@
1
+ // Central physical-table-name registry for plugin-financial. Providers import T so
2
+ // the plg_financial_* strings live in one place.
3
+ export const T = {
4
+ movements: 'plg_financial_movements',
5
+ chartOfAccounts: 'plg_financial_chart_of_accounts',
6
+ bankAccounts: 'plg_financial_bank_accounts',
7
+ cardBrands: 'plg_financial_card_brands',
8
+ costCenters: 'plg_financial_cost_centers',
9
+ cashRegisterSessions: 'plg_financial_cash_register_sessions',
10
+ paymentMethods: 'plg_financial_payment_methods',
11
+ paymentMethodTypes: 'plg_financial_payment_method_types',
12
+ } as const
package/src/index.ts CHANGED
@@ -121,6 +121,22 @@ export interface FinancialPluginOptions {
121
121
 
122
122
  /** Callback when user clicks a booking link on an invoice. Receives the order ID. */
123
123
  onBookingClick?: (orderId: string) => void
124
+
125
+ /**
126
+ * Which internal-nav links this APP gates, as `nav item id → feature id`.
127
+ * Empty by default: the plugin ships no opinion about what is paid — packaging
128
+ * is the app's decision, declared alongside its `permissions.ts` (RBAC) and
129
+ * `billing.ts` (plan entitlements). An id absent from this map is never hidden
130
+ * and never gated, in the nav OR in the content.
131
+ *
132
+ * {@link FINANCIAL_NAV_FEATURES} is an opt-in preset of the `fin_*` ids used
133
+ * by the reference apps — spread the entries you actually want:
134
+ *
135
+ * ```ts
136
+ * navFeatureMap: { reconciliation: FINANCIAL_NAV_FEATURES.reconciliation }
137
+ * ```
138
+ */
139
+ navFeatureMap?: Record<string, string>
124
140
  }
125
141
 
126
142
  // ---------------------------------------------------------------------------
@@ -150,6 +166,23 @@ const DEFAULT_LABELS: FinancialPluginLabels = {
150
166
 
151
167
  const DEFAULT_CURRENCY = { code: 'BRL', locale: 'pt-BR', symbol: 'R$' }
152
168
 
169
+ /**
170
+ * OPT-IN preset of feature ids for the financial sub-modules — a naming
171
+ * convention offered to apps, NOT a default. Nothing here is applied unless an
172
+ * app passes the entries it wants through `options.navFeatureMap`, because what
173
+ * is paid is a product decision that belongs to the app (its `permissions.ts` +
174
+ * `billing.ts`), never to the plugin.
175
+ */
176
+ export const FINANCIAL_NAV_FEATURES = {
177
+ reconciliation: 'fin_reconciliation',
178
+ cards: 'fin_cards',
179
+ commissions: 'fin_commissions',
180
+ statements: 'fin_statements',
181
+ receivables: 'fin_receivables',
182
+ payables: 'fin_payables',
183
+ 'cash-registers': 'fin_cash_registers',
184
+ } as const satisfies Record<string, string>
185
+
153
186
  const DEFAULT_ITEM_TYPES: ItemTypeOption[] = [
154
187
  { value: 'service', label: 'Service', icon: 'Briefcase' },
155
188
  { value: 'product', label: 'Product', icon: 'Package' },
@@ -186,6 +219,7 @@ function resolveConfig(options?: FinancialPluginOptions): ResolvedFinancialConfi
186
219
  entityLookups: options?.entityLookups ?? {},
187
220
  contactLookup: options?.contactLookup,
188
221
  onBookingClick: options?.onBookingClick,
222
+ navFeatureMap: { ...options?.navFeatureMap },
189
223
  }
190
224
  }
191
225
 
@@ -225,6 +259,71 @@ export function createFinancialPlugin(options?: FinancialPluginOptions): PluginM
225
259
  ...(config.modules.payables ? [{ id: 'financial.payables', label: config.labels.payables, group: config.labels.pageTitle }] : []),
226
260
  ...(config.modules.commissions ? [{ id: 'financial.commissions', label: config.labels.commissions, group: config.labels.pageTitle }] : []),
227
261
  ],
262
+ // Recurring monthly quota — counts financial movement rows created this
263
+ // month (invoices write one per installment; quick transactions one, or two
264
+ // for a transfer).
265
+ declaredLimits: [
266
+ { key: 'movements_month', label: 'Financial movements this month', table: 'plg_financial_movements', period: 'month' },
267
+ ],
268
+ queryEntities: [
269
+ {
270
+ key: 'financial:invoices',
271
+ entity: {
272
+ name: 'Invoice',
273
+ namePlural: 'Invoices (receipts REC- / payables PAG-)',
274
+ icon: 'Receipt',
275
+ permission: { feature: 'financial', action: 'read' },
276
+ fields: [
277
+ { key: 'referenceNumber', label: 'Reference (REC-/PAG-)', type: 'text', searchable: true },
278
+ { key: 'flow', label: 'Flow (receivable/payable)', type: 'text' },
279
+ { key: 'amount', label: 'Amount', type: 'number' },
280
+ { key: 'paid', label: 'Paid', type: 'number' },
281
+ { key: 'balance', label: 'Balance', type: 'number' },
282
+ { key: 'status', label: 'Status', type: 'text' },
283
+ { key: 'createdAt', label: 'Created at', type: 'text' },
284
+ ],
285
+ data: {
286
+ table: 'v_invoice_balances',
287
+ tenantScoped: true,
288
+ searchColumns: ['reference_number'],
289
+ },
290
+ },
291
+ },
292
+ {
293
+ key: 'financial:movements',
294
+ entity: {
295
+ name: 'Financial movement',
296
+ namePlural: 'Financial movements (installment ledger)',
297
+ icon: 'Banknote',
298
+ permission: { feature: 'financial', action: 'read' },
299
+ fields: [
300
+ { key: 'direction', label: 'Direction', type: 'text' },
301
+ { key: 'movementKind', label: 'Kind (bill/payment)', type: 'text' },
302
+ { key: 'amount', label: 'Amount', type: 'number' },
303
+ { key: 'paidAmount', label: 'Paid amount', type: 'number' },
304
+ { key: 'status', label: 'Status', type: 'text' },
305
+ { key: 'dueDate', label: 'Due date', type: 'text' },
306
+ { key: 'paymentDate', label: 'Payment date', type: 'text' },
307
+ { key: 'installmentNumber', label: 'Installment', type: 'number' },
308
+ { key: 'cardInstallments', label: 'Card installments', type: 'number' },
309
+ { key: 'createdAt', label: 'Created at', type: 'text' },
310
+ ],
311
+ data: {
312
+ table: 'plg_financial_movements',
313
+ tenantScoped: true,
314
+ },
315
+ },
316
+ },
317
+ ],
318
+ declaredRpcs: [
319
+ {
320
+ name: 'agent_financial_mark_payment_received',
321
+ kind: 'write' as const,
322
+ description:
323
+ 'Guarded settle: invoices the order via fn_invoice_from_order (installments for credit card) and marks the receivable paid, audited with the acting user.',
324
+ audits: true,
325
+ },
326
+ ],
228
327
  navigation: [
229
328
  {
230
329
  section: options?.navSection ?? 'main',
@@ -278,6 +377,7 @@ export function createFinancialPlugin(options?: FinancialPluginOptions): PluginM
278
377
  description: 'Creates a new invoice/payable for a contact.',
279
378
  icon: 'FileText',
280
379
  mode: 'persist' as const,
380
+ limitKey: 'movements_month',
281
381
  category: 'Finance',
282
382
  parameters: {
283
383
  type: 'object' as const,
@@ -293,6 +393,34 @@ export function createFinancialPlugin(options?: FinancialPluginOptions): PluginM
293
393
  ],
294
394
  permission: { feature: 'financial', action: 'create' as const },
295
395
  },
396
+ {
397
+ id: 'financial.mark-payment-received',
398
+ name: 'markPaymentReceived',
399
+ description:
400
+ 'Marks an order/appointment as paid: invoices it (with installments for credit card, e.g. "cartão 2x") and settles the receivable. Resolve the booking or order id via search tools first.',
401
+ icon: 'BadgeDollarSign',
402
+ mode: 'persist' as const,
403
+ execution: { plane: 'server' as const, kind: 'rpc' as const, rpc: 'agent_financial_mark_payment_received' },
404
+ category: 'Finance',
405
+ parameters: {
406
+ type: 'object' as const,
407
+ properties: {
408
+ booking_id: { type: 'string' as const, description: 'Appointment id (alternative to order_id)' },
409
+ order_id: { type: 'string' as const, description: 'Order id' },
410
+ method: {
411
+ type: 'string' as const,
412
+ description: 'Payment method',
413
+ enum: ['cash', 'pix', 'credit_card', 'debit_card', 'transfer'],
414
+ },
415
+ installments: { type: 'number' as const, description: 'Credit-card installments (1-12), e.g. 2 for "cartão 2x"' },
416
+ },
417
+ required: ['method'],
418
+ },
419
+ suggestions: [
420
+ { label: 'Marcar o atendimento como pago no cartão em 2x' },
421
+ ],
422
+ permission: { feature: 'financial.receivables', action: 'update' as const },
423
+ },
296
424
  {
297
425
  id: 'financial.list-payables',
298
426
  name: 'listPayables',
package/src/locales/en.ts CHANGED
@@ -142,6 +142,16 @@ export const en: Record<string, string> = {
142
142
  "financial.invoice.filterAllAccounts": "All accounts",
143
143
  "financial.invoice.filterCostCenter": "Cost center",
144
144
  "financial.invoice.filterAllCostCenters": "All cost centers",
145
+ "financial.invoice.filterPeriod": "Period",
146
+ "financial.invoice.filterAllPeriods": "All time",
147
+ "financial.invoice.filterThisMonth": "This month",
148
+ "financial.invoice.filterLastMonth": "Last month",
149
+ "financial.invoice.filterLast30": "Last 30 days",
150
+ "financial.invoice.filterThisYear": "This year",
151
+ "financial.invoice.filterCustom": "Custom",
152
+ "financial.invoice.filterFrom": "From",
153
+ "financial.invoice.filterTo": "To",
154
+ "financial.invoice.filterClear": "Clear",
145
155
  "financial.reconciliation.title": "Reconciliation",
146
156
  "financial.reconciliation.subtitle": "Match imported bank lines to your movements",
147
157
  "financial.reconciliation.pending": "{{count}} to reconcile",
@@ -142,6 +142,16 @@ export const ptBR: Record<string, string> = {
142
142
  "financial.invoice.filterAllAccounts": "Todos os planos",
143
143
  "financial.invoice.filterCostCenter": "Centro de custo",
144
144
  "financial.invoice.filterAllCostCenters": "Todos os centros",
145
+ "financial.invoice.filterPeriod": "Período",
146
+ "financial.invoice.filterAllPeriods": "Todo o período",
147
+ "financial.invoice.filterThisMonth": "Este mês",
148
+ "financial.invoice.filterLastMonth": "Mês passado",
149
+ "financial.invoice.filterLast30": "Últimos 30 dias",
150
+ "financial.invoice.filterThisYear": "Este ano",
151
+ "financial.invoice.filterCustom": "Personalizado",
152
+ "financial.invoice.filterFrom": "De",
153
+ "financial.invoice.filterTo": "Até",
154
+ "financial.invoice.filterClear": "Limpar",
145
155
  "financial.reconciliation.title": "Conciliação",
146
156
  "financial.reconciliation.subtitle": "Concilie as linhas importadas do banco com seus lançamentos",
147
157
  "financial.reconciliation.pending": "{{count}} para conciliar",
@@ -0,0 +1,30 @@
1
+ -- 000_plg_rename.sql — rename legacy financial tables to plg_financial_* for pools
2
+ -- provisioned before the industry-pool rename. Guarded: fires only when the legacy
3
+ -- name exists and the target does not, so fresh pools skip every branch.
4
+ DO $$
5
+ BEGIN
6
+ IF to_regclass('public.financial_movements') IS NOT NULL AND to_regclass('public.plg_financial_movements') IS NULL THEN
7
+ ALTER TABLE public.financial_movements RENAME TO plg_financial_movements;
8
+ END IF;
9
+ IF to_regclass('public.chart_of_accounts') IS NOT NULL AND to_regclass('public.plg_financial_chart_of_accounts') IS NULL THEN
10
+ ALTER TABLE public.chart_of_accounts RENAME TO plg_financial_chart_of_accounts;
11
+ END IF;
12
+ IF to_regclass('public.bank_accounts') IS NOT NULL AND to_regclass('public.plg_financial_bank_accounts') IS NULL THEN
13
+ ALTER TABLE public.bank_accounts RENAME TO plg_financial_bank_accounts;
14
+ END IF;
15
+ IF to_regclass('public.card_brands') IS NOT NULL AND to_regclass('public.plg_financial_card_brands') IS NULL THEN
16
+ ALTER TABLE public.card_brands RENAME TO plg_financial_card_brands;
17
+ END IF;
18
+ IF to_regclass('public.cost_centers') IS NOT NULL AND to_regclass('public.plg_financial_cost_centers') IS NULL THEN
19
+ ALTER TABLE public.cost_centers RENAME TO plg_financial_cost_centers;
20
+ END IF;
21
+ IF to_regclass('public.cash_register_sessions') IS NOT NULL AND to_regclass('public.plg_financial_cash_register_sessions') IS NULL THEN
22
+ ALTER TABLE public.cash_register_sessions RENAME TO plg_financial_cash_register_sessions;
23
+ END IF;
24
+ IF to_regclass('public.payment_methods') IS NOT NULL AND to_regclass('public.plg_financial_payment_methods') IS NULL THEN
25
+ ALTER TABLE public.payment_methods RENAME TO plg_financial_payment_methods;
26
+ END IF;
27
+ IF to_regclass('public.payment_method_types') IS NOT NULL AND to_regclass('public.plg_financial_payment_method_types') IS NULL THEN
28
+ ALTER TABLE public.payment_method_types RENAME TO plg_financial_payment_method_types;
29
+ END IF;
30
+ END $$;