@fayz-ai/plugin-financial 0.1.7 → 0.8.0

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 (72) hide show
  1. package/README.md +2 -0
  2. package/dist/FinancialContext.d.ts +2 -0
  3. package/dist/FinancialContext.d.ts.map +1 -1
  4. package/dist/FinancialPage.d.ts.map +1 -1
  5. package/dist/data/index.d.ts +1 -0
  6. package/dist/data/index.d.ts.map +1 -1
  7. package/dist/data/mock.d.ts +15 -0
  8. package/dist/data/mock.d.ts.map +1 -1
  9. package/dist/data/supabase.d.ts.map +1 -1
  10. package/dist/data/tables.d.ts +11 -0
  11. package/dist/data/tables.d.ts.map +1 -0
  12. package/dist/data/types.d.ts +2 -1
  13. package/dist/data/types.d.ts.map +1 -1
  14. package/dist/index.cjs +1594 -252
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.d.ts +13 -1
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +1456 -131
  19. package/dist/index.js.map +1 -1
  20. package/dist/locales/en.d.ts.map +1 -1
  21. package/dist/locales/pt-BR.d.ts.map +1 -1
  22. package/dist/migrations/index.d.ts +16 -0
  23. package/dist/migrations/index.d.ts.map +1 -0
  24. package/dist/quick-add.d.ts +13 -0
  25. package/dist/quick-add.d.ts.map +1 -0
  26. package/dist/registries.d.ts.map +1 -1
  27. package/dist/store.d.ts +3 -1
  28. package/dist/store.d.ts.map +1 -1
  29. package/dist/types.d.ts +42 -0
  30. package/dist/types.d.ts.map +1 -1
  31. package/dist/views/CardsView.d.ts.map +1 -1
  32. package/dist/views/InvoiceListView.d.ts.map +1 -1
  33. package/dist/views/QuickTransactionForm.d.ts +16 -0
  34. package/dist/views/QuickTransactionForm.d.ts.map +1 -0
  35. package/dist/views/SummaryView.d.ts +5 -4
  36. package/dist/views/SummaryView.d.ts.map +1 -1
  37. package/dist/views/dashboardWidgets.d.ts.map +1 -1
  38. package/package.json +7 -4
  39. package/src/FinancialContext.tsx +2 -0
  40. package/src/FinancialPage.tsx +86 -7
  41. package/src/README.md +5 -5
  42. package/src/contract.test.ts +1 -1
  43. package/src/data/capability.test.ts +45 -0
  44. package/src/data/index.ts +1 -0
  45. package/src/data/mock.ts +50 -10
  46. package/src/data/supabase.ts +95 -49
  47. package/src/data/tables.ts +12 -0
  48. package/src/data/types.ts +2 -1
  49. package/src/index.ts +24 -4
  50. package/src/locales/en.ts +75 -0
  51. package/src/locales/pt-BR.ts +75 -0
  52. package/src/migrations/000_plg_rename.sql +30 -0
  53. package/src/migrations/001_financial_base.sql +30 -30
  54. package/src/migrations/002_chart_of_accounts.sql +10 -10
  55. package/src/migrations/003_card_brands.sql +4 -4
  56. package/src/migrations/004_order_to_cash.sql +28 -28
  57. package/src/migrations/005_seed_defaults.sql +6 -6
  58. package/src/migrations/006_rls_policies.sql +5 -5
  59. package/src/migrations/{006_extract_fee_amount.sql → 006b_extract_fee_amount.sql} +4 -4
  60. package/src/migrations/007_reconciliation.sql +9 -9
  61. package/src/migrations/007b_movement_payment_method_type.sql +6 -0
  62. package/src/migrations/008_split_payment_movements.sql +5 -5
  63. package/src/migrations/index.ts +544 -0
  64. package/src/quick-add.ts +58 -0
  65. package/src/registries.ts +8 -7
  66. package/src/store.ts +88 -0
  67. package/src/types.ts +45 -0
  68. package/src/views/CardsView.tsx +186 -50
  69. package/src/views/InvoiceListView.tsx +60 -3
  70. package/src/views/QuickTransactionForm.tsx +537 -0
  71. package/src/views/SummaryView.tsx +197 -6
  72. package/src/views/dashboardWidgets.tsx +251 -5
package/src/data/mock.ts CHANGED
@@ -2,7 +2,7 @@ import type { FinancialDataProvider } from './types'
2
2
  import type {
3
3
  Invoice, InvoiceItem, FinancialMovement, BankAccount,
4
4
  CashSession, PaymentMethod, PaymentMethodType, ChartOfAccountsNode,
5
- CardTransaction,
5
+ CostCenter, CardTransaction,
6
6
  CreateInvoiceInput, PayMovementInput, CreateTransferInput, TransferResult,
7
7
  OpenCashSessionInput, CloseCashSessionInput, CreateBankAccountInput,
8
8
  InvoiceQuery, MovementQuery, StatementQuery,
@@ -62,13 +62,15 @@ interface MockStore {
62
62
  paymentMethods: PaymentMethod[]
63
63
  paymentMethodTypes: PaymentMethodType[]
64
64
  chartOfAccounts: ChartOfAccountsNode[]
65
+ costCenters: CostCenter[]
65
66
  cardTransactions: CardTransaction[]
66
67
  }
67
68
 
68
- function createStore(seedPaymentMethodTypes?: Array<{ name: string; transactionType?: string }>): MockStore {
69
+ function createStore(options?: MockFinancialProviderOptions): MockStore {
69
70
  const tenantId = 'mock-tenant'
71
+ const seed = options?.seed
70
72
 
71
- const defaultPMTypes: PaymentMethodType[] = (seedPaymentMethodTypes ?? [
73
+ const defaultPMTypes: PaymentMethodType[] = (options?.paymentMethodTypes ?? [
72
74
  { name: 'Cash', transactionType: 'cash' },
73
75
  { name: 'Credit Card', transactionType: 'credit_card' },
74
76
  { name: 'Debit Card', transactionType: 'debit_card' },
@@ -109,14 +111,18 @@ function createStore(seedPaymentMethodTypes?: Array<{ name: string; transactionT
109
111
  }
110
112
 
111
113
  return {
112
- invoices: [],
113
- invoiceItems: [],
114
- movements: [],
115
- bankAccounts: [cashAccount, bankAccount],
114
+ // Seeded data (when provided) makes the module render as if the user already
115
+ // has accounts + transactions — a populated Summary/Statements instead of the
116
+ // empty onboarding state. Seed arrays are copied so the caller's data is not mutated.
117
+ invoices: seed?.invoices ? [...seed.invoices] : [],
118
+ invoiceItems: seed?.invoiceItems ? [...seed.invoiceItems] : [],
119
+ movements: seed?.movements ? [...seed.movements] : [],
120
+ bankAccounts: seed?.bankAccounts ? [...seed.bankAccounts] : [cashAccount, bankAccount],
116
121
  cashSessions: [],
117
- paymentMethods: [],
122
+ paymentMethods: seed?.paymentMethods ? [...seed.paymentMethods] : [],
118
123
  paymentMethodTypes: defaultPMTypes,
119
124
  chartOfAccounts: [],
125
+ costCenters: [],
120
126
  cardTransactions: [],
121
127
  }
122
128
  }
@@ -125,12 +131,27 @@ function createStore(seedPaymentMethodTypes?: Array<{ name: string; transactionT
125
131
  // Mock provider factory
126
132
  // ---------------------------------------------------------------------------
127
133
 
134
+ /**
135
+ * Pre-populated store data. Any omitted collection stays empty (except
136
+ * bankAccounts, which fall back to the two default mock accounts when absent).
137
+ * Pass full domain objects — the mock treats them as the existing ledger.
138
+ */
139
+ export interface MockFinancialSeed {
140
+ bankAccounts?: BankAccount[]
141
+ invoices?: Invoice[]
142
+ invoiceItems?: InvoiceItem[]
143
+ movements?: FinancialMovement[]
144
+ paymentMethods?: PaymentMethod[]
145
+ }
146
+
128
147
  export interface MockFinancialProviderOptions {
129
148
  paymentMethodTypes?: Array<{ name: string; transactionType?: string }>
149
+ /** Pre-seed the in-memory store so the module renders populated (not the empty state). */
150
+ seed?: MockFinancialSeed
130
151
  }
131
152
 
132
153
  export function createMockFinancialProvider(options?: MockFinancialProviderOptions): FinancialDataProvider {
133
- const store = createStore(options?.paymentMethodTypes)
154
+ const store = createStore(options)
134
155
  const tenantId = 'mock-tenant'
135
156
 
136
157
  function recalcInvoiceStatus(invoice: Invoice): void {
@@ -154,6 +175,14 @@ export function createMockFinancialProvider(options?: MockFinancialProviderOptio
154
175
  if (query.direction) results = results.filter((i) => i.direction === query.direction)
155
176
  if (query.status) results = results.filter((i) => matchesStatus(i.status, query.status as InvoiceStatus | InvoiceStatus[]))
156
177
  if (query.contactId) results = results.filter((i) => i.contactId === query.contactId)
178
+ if (query.accountId) {
179
+ const invoiceIds = new Set(store.invoiceItems.filter((item) => item.accountId === query.accountId).map((item) => item.invoiceId))
180
+ results = results.filter((invoice) => invoiceIds.has(invoice.id))
181
+ }
182
+ if (query.costCenterId) {
183
+ const invoiceIds = new Set(store.invoiceItems.filter((item) => item.costCenterId === query.costCenterId).map((item) => item.invoiceId))
184
+ results = results.filter((invoice) => invoiceIds.has(invoice.id))
185
+ }
157
186
  if (query.dateRange) results = results.filter((i) => matchesDateRange(i.invoiceDate, query.dateRange))
158
187
  if (query.search) {
159
188
  const s = query.search.toLowerCase()
@@ -511,6 +540,10 @@ export function createMockFinancialProvider(options?: MockFinancialProviderOptio
511
540
  return store.chartOfAccounts.filter((n) => n.isActive)
512
541
  },
513
542
 
543
+ async getCostCenters(): Promise<CostCenter[]> {
544
+ return store.costCenters.filter((c) => c.isActive)
545
+ },
546
+
514
547
  // --- Card Transactions ---
515
548
  async getCardTransactions(dateRange?: DateRange): Promise<CardTransaction[]> {
516
549
  let results = [...store.cardTransactions]
@@ -627,7 +660,14 @@ export function createMockFinancialProvider(options?: MockFinancialProviderOptio
627
660
  )
628
661
 
629
662
  const todayStr = today()
630
- const monthStart = todayStr.slice(0, 7) + '-01'
663
+ // "Monthly" cash flow uses a trailing 30-day window (rolling month), not
664
+ // calendar month-to-date. Month-to-date collapses to near-empty in the
665
+ // first days of a month — on the 1st it's a single day — so the cash-flow
666
+ // chart and Fluxo Mensal KPI read empty even with a full month of realized
667
+ // activity. A rolling window stays representative on every day of the month.
668
+ const monthAgo = new Date()
669
+ monthAgo.setDate(monthAgo.getDate() - 30)
670
+ const monthStart = monthAgo.toISOString().slice(0, 10)
631
671
  const monthEnd = todayStr
632
672
 
633
673
  const monthMovements = store.movements.filter((m) =>
@@ -2,7 +2,7 @@ import type { FinancialDataProvider } from './types'
2
2
  import type {
3
3
  Invoice, InvoiceItem, FinancialMovement, BankAccount,
4
4
  CashSession, PaymentMethod, PaymentMethodType, ChartOfAccountsNode,
5
- CardTransaction,
5
+ CostCenter, CardTransaction,
6
6
  CreateInvoiceInput, PayMovementInput, CreateTransferInput, TransferResult,
7
7
  OpenCashSessionInput, CloseCashSessionInput, CreateBankAccountInput,
8
8
  InvoiceQuery, MovementQuery, StatementQuery,
@@ -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[],
@@ -116,10 +119,28 @@ function round2(n: number): number {
116
119
  return Math.round(n * 100) / 100
117
120
  }
118
121
 
122
+ async function findOrderIdsByItemMetadata(
123
+ core: any,
124
+ filters: { accountId?: string; costCenterId?: string },
125
+ ): Promise<string[] | undefined> {
126
+ if (!filters.accountId && !filters.costCenterId) return undefined
127
+
128
+ let qb = core.from('order_items').select('order_id')
129
+ if (filters.accountId) qb = qb.filter('metadata->>accountId', 'eq', filters.accountId)
130
+ if (filters.costCenterId) qb = qb.filter('metadata->>costCenterId', 'eq', filters.costCenterId)
131
+
132
+ const { data } = await qb
133
+ const ids: string[] = []
134
+ for (const row of (data ?? []) as Array<{ order_id?: unknown }>) {
135
+ if (typeof row.order_id === 'string' && row.order_id.length > 0) ids.push(row.order_id)
136
+ }
137
+ return Array.from(new Set(ids))
138
+ }
139
+
119
140
  /** Card/MDR fee percent for a configured payment method (0 when none / not a fee method). */
120
141
  async function getMethodFeePct(pub: any, paymentMethodId?: string): Promise<number> {
121
142
  if (!paymentMethodId) return 0
122
- 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()
123
144
  return Number(data?.discount_value) || 0
124
145
  }
125
146
 
@@ -151,6 +172,14 @@ export function createSupabaseFinancialProvider(): FinancialDataProvider {
151
172
  const statuses = Array.isArray(query.status) ? query.status : [query.status]
152
173
  qb = qb.in('status', statuses)
153
174
  }
175
+ const itemScopedOrderIds = await findOrderIdsByItemMetadata(core, {
176
+ accountId: query.accountId,
177
+ costCenterId: query.costCenterId,
178
+ })
179
+ if (itemScopedOrderIds) {
180
+ if (itemScopedOrderIds.length === 0) return { data: [], total: 0 }
181
+ qb = qb.in('id', itemScopedOrderIds)
182
+ }
154
183
  if (query.contactId) qb = qb.eq('party_id', query.contactId)
155
184
  if (query.search) qb = qb.ilike('notes', `%${query.search}%`)
156
185
  if (query.dateRange) qb = qb.gte('created_at', query.dateRange.from).lte('created_at', query.dateRange.to)
@@ -194,7 +223,7 @@ export function createSupabaseFinancialProvider(): FinancialDataProvider {
194
223
  const meta = data.metadata ?? {}
195
224
  // Paid amount + status are DERIVED from the ledger view (single source of truth).
196
225
  const balance = (await fetchInvoiceBalances(pub, [id])).get(id)
197
- const { data: movs } = await pub.from('financial_movements')
226
+ const { data: movs } = await pub.from(T.movements)
198
227
  .select('paid_amount')
199
228
  .eq('invoice_id', id)
200
229
  .eq('movement_kind', 'bill')
@@ -224,6 +253,8 @@ export function createSupabaseFinancialProvider(): FinancialDataProvider {
224
253
  unitPrice: i.unitPrice ?? 0, totalAmount: i.total ?? 0,
225
254
  discount: i.discount ?? 0, surcharge: 0,
226
255
  referenceId: i.productId ?? i.serviceId,
256
+ accountId: i.metadata?.accountId,
257
+ costCenterId: i.metadata?.costCenterId,
227
258
  createdAt: i.createdAt,
228
259
  } as InvoiceItem
229
260
  })
@@ -263,14 +294,19 @@ export function createSupabaseFinancialProvider(): FinancialDataProvider {
263
294
  discount: item.discount ?? 0,
264
295
  total: item.quantity * item.unitPrice - (item.discount ?? 0) + (item.surcharge ?? 0),
265
296
  sort_order: i,
266
- metadata: { itemKind: item.itemKind, referenceId: item.referenceId },
297
+ metadata: {
298
+ itemKind: item.itemKind,
299
+ referenceId: item.referenceId,
300
+ accountId: item.accountId,
301
+ costCenterId: item.costCenterId,
302
+ },
267
303
  }))
268
304
  )
269
305
  }
270
306
 
271
307
  // Create installment movements
272
308
  if (order && input.installments.length > 0) {
273
- await pub.from('financial_movements').insert(
309
+ await pub.from(T.movements).insert(
274
310
  input.installments.map((inst, i) => ({
275
311
  tenant_id: tenantId, invoice_id: order.id,
276
312
  direction: input.direction, movement_kind: inst.movementKind,
@@ -302,13 +338,13 @@ export function createSupabaseFinancialProvider(): FinancialDataProvider {
302
338
  async cancelInvoice(id: string): Promise<void> {
303
339
  const { core, pub } = getClients()
304
340
  await core.from('orders').update({ status: 'cancelled' }).eq('id', id)
305
- await pub.from('financial_movements').update({ status: 'cancelled' }).eq('invoice_id', id).eq('status', 'pending')
341
+ await pub.from(T.movements).update({ status: 'cancelled' }).eq('invoice_id', id).eq('status', 'pending')
306
342
  },
307
343
 
308
344
  // --- Movements (public.financial_movements) ---
309
345
  async getMovements(query: MovementQuery): Promise<PaginatedResult<FinancialMovement>> {
310
346
  const { core, pub } = getClients()
311
- let qb = pub.from('financial_movements').select('*', { count: 'exact' })
347
+ let qb = pub.from(T.movements).select('*', { count: 'exact' })
312
348
  if (query.direction) qb = qb.eq('direction', query.direction)
313
349
  if (query.status) {
314
350
  const statuses = Array.isArray(query.status) ? query.status : [query.status]
@@ -326,7 +362,7 @@ export function createSupabaseFinancialProvider(): FinancialDataProvider {
326
362
 
327
363
  async payMovement(input: PayMovementInput): Promise<FinancialMovement> {
328
364
  const { core, pub } = getClients()
329
- const { data: mov } = await pub.from('financial_movements').select('*').eq('id', input.movementId).single()
365
+ const { data: mov } = await pub.from(T.movements).select('*').eq('id', input.movementId).single()
330
366
  if (!mov) throw new Error('Movement not found')
331
367
  const tenantId = mov.tenant_id ?? getTenantId()
332
368
 
@@ -356,11 +392,11 @@ export function createSupabaseFinancialProvider(): FinancialDataProvider {
356
392
  card_installments: input.cardInstallments ?? null,
357
393
  notes: input.notes ?? null,
358
394
  }
359
- let { data, error } = await pub.from('financial_movements').insert(paymentRow).select().single()
395
+ let { data, error } = await pub.from(T.movements).insert(paymentRow).select().single()
360
396
  // Graceful fallback if the fee_amount migration hasn't been applied yet.
361
397
  if (error && /fee_amount/i.test(error.message ?? '')) {
362
398
  const { fee_amount, ...rest } = paymentRow
363
- ;({ data, error } = await pub.from('financial_movements').insert(rest).select().single())
399
+ ;({ data, error } = await pub.from(T.movements).insert(rest).select().single())
364
400
  }
365
401
  if (error) throw error
366
402
 
@@ -371,13 +407,13 @@ export function createSupabaseFinancialProvider(): FinancialDataProvider {
371
407
  const billStatus = newPaid >= mov.amount ? 'paid' : 'partial'
372
408
  // Clear the bill's cash fields so it stops being a cash event (the payment row is now
373
409
  // the cash event) — prevents double-counting legacy in-place-paid bills.
374
- await pub.from('financial_movements')
410
+ await pub.from(T.movements)
375
411
  .update({ paid_amount: newPaid, status: billStatus, payment_date: null, bank_account_id: null })
376
412
  .eq('id', mov.id)
377
413
 
378
414
  // 3. Update invoice (order) status based on its bills
379
415
  if (mov.invoice_id) {
380
- const { data: allMovs } = await pub.from('financial_movements')
416
+ const { data: allMovs } = await pub.from(T.movements)
381
417
  .select('status, amount, paid_amount')
382
418
  .eq('invoice_id', mov.invoice_id)
383
419
  .eq('movement_kind', 'bill')
@@ -390,7 +426,7 @@ export function createSupabaseFinancialProvider(): FinancialDataProvider {
390
426
  const { data: order } = await core.from('orders').select('metadata').eq('id', mov.invoice_id).single()
391
427
  const meta = (order?.metadata as any) ?? {}
392
428
  const invoiceStage = allPaid ? 'paid' : anyPaid ? 'partial' : 'invoiced'
393
- // Only update financial order status. Agenda status lives on saas_core.bookings.
429
+ // Only update financial order status. Agenda status lives on public.appointments.
394
430
  await core.from('orders').update({
395
431
  status: invoiceStage,
396
432
  metadata: { ...meta, paidAmount: totalPaid },
@@ -402,7 +438,7 @@ export function createSupabaseFinancialProvider(): FinancialDataProvider {
402
438
 
403
439
  async cancelMovement(id: string): Promise<void> {
404
440
  const { core, pub } = getClients()
405
- await pub.from('financial_movements').update({ status: 'cancelled' }).eq('id', id)
441
+ await pub.from(T.movements).update({ status: 'cancelled' }).eq('id', id)
406
442
  },
407
443
 
408
444
  // --- Transfers (between accounts) ---
@@ -439,11 +475,11 @@ export function createSupabaseFinancialProvider(): FinancialDataProvider {
439
475
  metadata: { transferId, transferRole: 'in', counterAccountId: input.fromAccountId },
440
476
  },
441
477
  ]
442
- let { data, error } = await pub.from('financial_movements').insert(rowsToInsert).select('id, direction')
478
+ let { data, error } = await pub.from(T.movements).insert(rowsToInsert).select('id, direction')
443
479
  // Graceful fallback if the fee_amount migration hasn't been applied yet.
444
480
  if (error && /fee_amount/i.test(error.message ?? '')) {
445
481
  const stripped = rowsToInsert.map(({ fee_amount, ...rest }) => rest)
446
- ;({ data, error } = await pub.from('financial_movements').insert(stripped).select('id, direction'))
482
+ ;({ data, error } = await pub.from(T.movements).insert(stripped).select('id, direction'))
447
483
  }
448
484
  if (error) throw error
449
485
  const rows = (data ?? []) as Array<{ id: string; direction: string }>
@@ -455,14 +491,14 @@ export function createSupabaseFinancialProvider(): FinancialDataProvider {
455
491
  // --- Bank Accounts ---
456
492
  async getBankAccounts(): Promise<BankAccount[]> {
457
493
  const { core, pub } = getClients()
458
- const { data } = await pub.from('bank_accounts').select('*').eq('is_active', true).order('name')
494
+ const { data } = await pub.from(T.bankAccounts).select('*').eq('is_active', true).order('name')
459
495
  return (data ?? []).map((r: any) => snakeToCamel(r) as unknown as BankAccount)
460
496
  },
461
497
 
462
498
  async createBankAccount(input: CreateBankAccountInput): Promise<BankAccount> {
463
499
  const { core, pub } = getClients()
464
500
  const tenantId = getTenantId()
465
- const { data } = await pub.from('bank_accounts').insert({
501
+ const { data } = await pub.from(T.bankAccounts).insert({
466
502
  ...camelToSnake(input as any), tenant_id: tenantId, current_balance: input.initialBalance ?? 0,
467
503
  }).select().single()
468
504
  return snakeToCamel(data!) as unknown as BankAccount
@@ -472,14 +508,14 @@ export function createSupabaseFinancialProvider(): FinancialDataProvider {
472
508
  const { core, pub } = getClients()
473
509
  const row = camelToSnake(partial as any)
474
510
  delete row.id; delete row.tenant_id
475
- const { data } = await pub.from('bank_accounts').update(row).eq('id', id).select().single()
511
+ const { data } = await pub.from(T.bankAccounts).update(row).eq('id', id).select().single()
476
512
  return snakeToCamel(data!) as unknown as BankAccount
477
513
  },
478
514
 
479
515
  // --- Cash Sessions ---
480
516
  async getCashSessions(bankAccountId?: string): Promise<CashSession[]> {
481
517
  const { core, pub } = getClients()
482
- let qb = pub.from('cash_register_sessions').select('*')
518
+ let qb = pub.from(T.cashRegisterSessions).select('*')
483
519
  if (bankAccountId) qb = qb.eq('bank_account_id', bankAccountId)
484
520
  const { data } = await qb.order('opened_at', { ascending: false })
485
521
  return (data ?? []).map((r: any) => snakeToCamel(r) as unknown as CashSession)
@@ -487,14 +523,14 @@ export function createSupabaseFinancialProvider(): FinancialDataProvider {
487
523
 
488
524
  async getOpenSession(bankAccountId: string): Promise<CashSession | null> {
489
525
  const { core, pub } = getClients()
490
- const { data } = await pub.from('cash_register_sessions').select('*').eq('bank_account_id', bankAccountId).eq('status', 'open').single()
526
+ const { data } = await pub.from(T.cashRegisterSessions).select('*').eq('bank_account_id', bankAccountId).eq('status', 'open').single()
491
527
  return data ? snakeToCamel(data) as unknown as CashSession : null
492
528
  },
493
529
 
494
530
  async openCashSession(input: OpenCashSessionInput): Promise<CashSession> {
495
531
  const { core, pub } = getClients()
496
532
  const tenantId = getTenantId()
497
- const { data } = await pub.from('cash_register_sessions').insert({
533
+ const { data } = await pub.from(T.cashRegisterSessions).insert({
498
534
  tenant_id: tenantId, bank_account_id: input.bankAccountId,
499
535
  status: 'open', opening_balance: input.openingBalance,
500
536
  opened_by_user_id: input.openedByUserId, opened_by_name: input.openedByName,
@@ -505,7 +541,7 @@ export function createSupabaseFinancialProvider(): FinancialDataProvider {
505
541
 
506
542
  async closeCashSession(input: CloseCashSessionInput): Promise<CashSession> {
507
543
  const { core, pub } = getClients()
508
- const { data } = await pub.from('cash_register_sessions').update({
544
+ const { data } = await pub.from(T.cashRegisterSessions).update({
509
545
  status: 'closed', closing_balance: input.closingBalance,
510
546
  closed_at: new Date().toISOString(),
511
547
  closed_by_user_id: input.closedByUserId, closed_by_name: input.closedByName,
@@ -516,8 +552,8 @@ export function createSupabaseFinancialProvider(): FinancialDataProvider {
516
552
 
517
553
  async getCashSessionSummary(sessionId: string): Promise<CashSessionSummary> {
518
554
  const { core, pub } = getClients()
519
- const session = (await pub.from('cash_register_sessions').select('*').eq('id', sessionId).single()).data
520
- const { data: movs } = await pub.from('financial_movements').select('direction, paid_amount').eq('cash_session_id', sessionId).eq('status', 'paid')
555
+ const session = (await pub.from(T.cashRegisterSessions).select('*').eq('id', sessionId).single()).data
556
+ const { data: movs } = await pub.from(T.movements).select('direction, paid_amount').eq('cash_session_id', sessionId).eq('status', 'paid')
521
557
  const movements = movs ?? []
522
558
  return {
523
559
  session: snakeToCamel(session!) as unknown as CashSession,
@@ -530,23 +566,29 @@ export function createSupabaseFinancialProvider(): FinancialDataProvider {
530
566
  // --- Payment Methods ---
531
567
  async getPaymentMethods(): Promise<PaymentMethod[]> {
532
568
  const { core, pub } = getClients()
533
- const { data } = await pub.from('payment_methods').select('*').eq('is_active', true).order('name')
569
+ const { data } = await pub.from(T.paymentMethods).select('*').eq('is_active', true).order('name')
534
570
  return (data ?? []).map((r: any) => snakeToCamel(r) as unknown as PaymentMethod)
535
571
  },
536
572
 
537
573
  async getPaymentMethodTypes(): Promise<PaymentMethodType[]> {
538
574
  const { core, pub } = getClients()
539
- const { data } = await pub.from('payment_method_types').select('*').eq('is_active', true).order('name')
575
+ const { data } = await pub.from(T.paymentMethodTypes).select('*').eq('is_active', true).order('name')
540
576
  return (data ?? []).map((r: any) => snakeToCamel(r) as unknown as PaymentMethodType)
541
577
  },
542
578
 
543
579
  // --- Chart of Accounts ---
544
580
  async getChartOfAccounts(): Promise<ChartOfAccountsNode[]> {
545
581
  const { core, pub } = getClients()
546
- const { data } = await pub.from('chart_of_accounts').select('*').eq('is_active', true).order('code')
582
+ const { data } = await pub.from(T.chartOfAccounts).select('*').eq('is_active', true).order('code')
547
583
  return (data ?? []).map((r: any) => snakeToCamel(r) as unknown as ChartOfAccountsNode)
548
584
  },
549
585
 
586
+ async getCostCenters(): Promise<CostCenter[]> {
587
+ const { core, pub } = getClients()
588
+ const { data } = await pub.from(T.costCenters).select('*').eq('is_active', true).order('code')
589
+ return (data ?? []).map((r: any) => snakeToCamel(r) as unknown as CostCenter)
590
+ },
591
+
550
592
  // --- Card Transactions ---
551
593
  async getCardTransactions(dateRange?: DateRange): Promise<CardTransaction[]> {
552
594
  const { core, pub } = getClients()
@@ -565,7 +607,7 @@ export function createSupabaseFinancialProvider(): FinancialDataProvider {
565
607
 
566
608
  // 1. Resolve in-scope accounts. No bankAccountId => consolidated: ALL realized cash
567
609
  // (including movements with no bank_account_id, e.g. cash/card payments not tied to an account).
568
- const { data: acctRows } = await pub.from('bank_accounts').select('id, name, initial_balance, is_active')
610
+ const { data: acctRows } = await pub.from(T.bankAccounts).select('id, name, initial_balance, is_active')
569
611
  const allAccounts = (acctRows ?? []) as any[]
570
612
  const consolidated = !query.bankAccountId
571
613
  const scopeAccounts = consolidated
@@ -587,7 +629,7 @@ export function createSupabaseFinancialProvider(): FinancialDataProvider {
587
629
 
588
630
  // 2. Opening balance = Σ initial_balance + prior net (payment_date < from).
589
631
  // Select '*' so this still works before the fee_amount migration lands (fee → 0).
590
- const { data: priorRows } = await scopeFilter(pub.from('financial_movements')
632
+ const { data: priorRows } = await scopeFilter(pub.from(T.movements)
591
633
  .select('*')
592
634
  .neq('status', 'cancelled')
593
635
  .lt('payment_date', query.dateRange.from))
@@ -595,7 +637,7 @@ export function createSupabaseFinancialProvider(): FinancialDataProvider {
595
637
  const openingBalance = round2(initialSum + priorNet)
596
638
 
597
639
  // 3. In-range realized rows.
598
- const { data: movs } = await scopeFilter(pub.from('financial_movements')
640
+ const { data: movs } = await scopeFilter(pub.from(T.movements)
599
641
  .select('*')
600
642
  .neq('status', 'cancelled')
601
643
  .gte('payment_date', query.dateRange.from).lte('payment_date', query.dateRange.to)
@@ -648,7 +690,7 @@ export function createSupabaseFinancialProvider(): FinancialDataProvider {
648
690
  // extract row can link to the internal transaction it was conciliated with.
649
691
  const matchedIds = [...new Set(entries.map((e) => e.movement.matchedMovementId).filter(Boolean))] as string[]
650
692
  if (matchedIds.length) {
651
- const { data: matched } = await pub.from('financial_movements').select('id, invoice_id, notes').in('id', matchedIds)
693
+ const { data: matched } = await pub.from(T.movements).select('id, invoice_id, notes').in('id', matchedIds)
652
694
  const matchedById = new Map<string, any>((matched ?? []).map((m: any) => [m.id, m]))
653
695
  const matchedInvoiceIds = [...new Set((matched ?? []).map((m: any) => m.invoice_id).filter(Boolean))] as string[]
654
696
  const invById = new Map<string, any>()
@@ -672,7 +714,7 @@ export function createSupabaseFinancialProvider(): FinancialDataProvider {
672
714
  }
673
715
 
674
716
  // 6. Account current balance (ledger as-of-today).
675
- const { data: todayRows } = await scopeFilter(pub.from('financial_movements')
717
+ const { data: todayRows } = await scopeFilter(pub.from(T.movements)
676
718
  .select('*')
677
719
  .neq('status', 'cancelled')
678
720
  .lte('payment_date', today))
@@ -696,10 +738,10 @@ export function createSupabaseFinancialProvider(): FinancialDataProvider {
696
738
  // Sweep overdue on first load (background, non-blocking)
697
739
  sweepOverdue()
698
740
  const { core, pub } = getClients()
699
- const { data: accounts } = await pub.from('bank_accounts').select('current_balance').eq('is_active', true)
741
+ const { data: accounts } = await pub.from(T.bankAccounts).select('current_balance').eq('is_active', true)
700
742
  const totalBalance = (accounts ?? []).reduce((s: number, a: any) => s + (a.current_balance ?? 0), 0)
701
743
 
702
- const { data: movs } = await pub.from('financial_movements').select('direction, movement_kind, amount, paid_amount, status, due_date, payment_date')
744
+ const { data: movs } = await pub.from(T.movements).select('direction, movement_kind, amount, paid_amount, status, due_date, payment_date')
703
745
  const allMovs = movs ?? []
704
746
  const unpaid = allMovs.filter((m: any) => ['pending', 'partial', 'overdue'].includes(m.status))
705
747
  const today = new Date().toISOString().slice(0, 10)
@@ -709,7 +751,11 @@ export function createSupabaseFinancialProvider(): FinancialDataProvider {
709
751
  const overdueCredit = unpaidCredit.filter((m: any) => m.status === 'overdue' || m.due_date < today)
710
752
  const overdueDebit = unpaidDebit.filter((m: any) => m.status === 'overdue' || m.due_date < today)
711
753
 
712
- const monthStart = today.slice(0, 7) + '-01'
754
+ // Monthly cash flow = a trailing 30-day rolling window (not calendar
755
+ // month-to-date, which reads empty in the first days of a month).
756
+ const monthAgo = new Date()
757
+ monthAgo.setDate(monthAgo.getDate() - 30)
758
+ const monthStart = monthAgo.toISOString().slice(0, 10)
713
759
  // Monthly cash flow = payment cash events (one row per payment), never bills.
714
760
  const monthPaid = allMovs.filter((m: any) => m.movement_kind === 'payment' && m.payment_date >= monthStart)
715
761
 
@@ -735,7 +781,7 @@ export function createSupabaseFinancialProvider(): FinancialDataProvider {
735
781
  const tenantId = getTenantId()
736
782
  let imported = 0, duplicates = 0
737
783
  for (const line of input.lines) {
738
- const { error } = await pub.from('financial_movements').insert({
784
+ const { error } = await pub.from(T.movements).insert({
739
785
  tenant_id: tenantId,
740
786
  direction: line.direction,
741
787
  movement_kind: 'payment',
@@ -760,7 +806,7 @@ export function createSupabaseFinancialProvider(): FinancialDataProvider {
760
806
 
761
807
  async getUnreconciled(query) {
762
808
  const { pub } = getClients()
763
- let qb = pub.from('financial_movements').select('*')
809
+ let qb = pub.from(T.movements).select('*')
764
810
  .not('external_source', 'is', null)
765
811
  .is('reconciled_at', null)
766
812
  if (query?.bankAccountId) qb = qb.eq('bank_account_id', query.bankAccountId)
@@ -771,13 +817,13 @@ export function createSupabaseFinancialProvider(): FinancialDataProvider {
771
817
 
772
818
  async suggestReconciliation(bankMovementId) {
773
819
  const { core, pub } = getClients()
774
- const { data: line } = await pub.from('financial_movements').select('*').eq('id', bankMovementId).single()
820
+ const { data: line } = await pub.from(T.movements).select('*').eq('id', bankMovementId).single()
775
821
  if (!line) return []
776
822
  const amount = Number(line.amount) || 0
777
823
  const lineDate = line.payment_date ?? line.due_date
778
824
  // Candidates: app-native (non-imported), same direction, not reconciled, amount within ±2%.
779
825
  const tol = Math.max(0.01, round2(amount * 0.02))
780
- const { data } = await pub.from('financial_movements').select('*')
826
+ const { data } = await pub.from(T.movements).select('*')
781
827
  .is('external_source', null)
782
828
  .is('reconciled_at', null)
783
829
  .eq('direction', line.direction)
@@ -814,22 +860,22 @@ export function createSupabaseFinancialProvider(): FinancialDataProvider {
814
860
  async reconcileMovement(input) {
815
861
  const { pub } = getClients()
816
862
  const now = new Date().toISOString()
817
- await pub.from('financial_movements').update({
863
+ await pub.from(T.movements).update({
818
864
  reconciled_at: now,
819
865
  matched_movement_id: input.matchedMovementId ?? null,
820
866
  }).eq('id', input.bankMovementId)
821
867
  if (input.matchedMovementId) {
822
868
  // The internal movement is now settled+reconciled by the bank line.
823
- await pub.from('financial_movements').update({ reconciled_at: now }).eq('id', input.matchedMovementId)
869
+ await pub.from(T.movements).update({ reconciled_at: now }).eq('id', input.matchedMovementId)
824
870
  }
825
871
  },
826
872
 
827
873
  async unreconcileMovement(bankMovementId) {
828
874
  const { pub } = getClients()
829
- const { data: line } = await pub.from('financial_movements').select('matched_movement_id').eq('id', bankMovementId).single()
830
- await pub.from('financial_movements').update({ reconciled_at: null, matched_movement_id: null }).eq('id', bankMovementId)
875
+ const { data: line } = await pub.from(T.movements).select('matched_movement_id').eq('id', bankMovementId).single()
876
+ await pub.from(T.movements).update({ reconciled_at: null, matched_movement_id: null }).eq('id', bankMovementId)
831
877
  if (line?.matched_movement_id) {
832
- await pub.from('financial_movements').update({ reconciled_at: null }).eq('id', line.matched_movement_id)
878
+ await pub.from(T.movements).update({ reconciled_at: null }).eq('id', line.matched_movement_id)
833
879
  }
834
880
  },
835
881
  }
@@ -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/data/types.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type {
2
2
  Invoice, InvoiceItem, FinancialMovement, BankAccount,
3
3
  CashSession, PaymentMethod, PaymentMethodType, ChartOfAccountsNode,
4
- CardTransaction,
4
+ CostCenter, CardTransaction,
5
5
  CreateInvoiceInput, PayMovementInput, CreateTransferInput, TransferResult,
6
6
  OpenCashSessionInput, CloseCashSessionInput, CreateBankAccountInput,
7
7
  InvoiceQuery, MovementQuery, StatementQuery,
@@ -46,6 +46,7 @@ export interface FinancialDataProvider {
46
46
 
47
47
  // --- Chart of Accounts ---
48
48
  getChartOfAccounts(): Promise<ChartOfAccountsNode[]>
49
+ getCostCenters(): Promise<CostCenter[]>
49
50
 
50
51
  // --- Card Transactions ---
51
52
  getCardTransactions(dateRange?: DateRange): Promise<CardTransaction[]>