@fayz-ai/plugin-financial 0.1.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 (98) hide show
  1. package/dist/FinancialContext.d.ts +44 -0
  2. package/dist/FinancialContext.d.ts.map +1 -0
  3. package/dist/FinancialPage.d.ts +18 -0
  4. package/dist/FinancialPage.d.ts.map +1 -0
  5. package/dist/components/FinancialGeneralSettings.d.ts +3 -0
  6. package/dist/components/FinancialGeneralSettings.d.ts.map +1 -0
  7. package/dist/components/FinancialOnboarding.d.ts +5 -0
  8. package/dist/components/FinancialOnboarding.d.ts.map +1 -0
  9. package/dist/components/FinancialSettings.d.ts +8 -0
  10. package/dist/components/FinancialSettings.d.ts.map +1 -0
  11. package/dist/components/PaymentModal.d.ts +8 -0
  12. package/dist/components/PaymentModal.d.ts.map +1 -0
  13. package/dist/data/index.d.ts +3 -0
  14. package/dist/data/index.d.ts.map +1 -0
  15. package/dist/data/mock.d.ts +9 -0
  16. package/dist/data/mock.d.ts.map +1 -0
  17. package/dist/data/supabase.d.ts +3 -0
  18. package/dist/data/supabase.d.ts.map +1 -0
  19. package/dist/data/types.d.ts +27 -0
  20. package/dist/data/types.d.ts.map +1 -0
  21. package/dist/index.cjs +4937 -0
  22. package/dist/index.cjs.map +1 -0
  23. package/dist/index.d.ts +91 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +4930 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/lib/tenant.d.ts +3 -0
  28. package/dist/lib/tenant.d.ts.map +1 -0
  29. package/dist/locales/en.d.ts +2 -0
  30. package/dist/locales/en.d.ts.map +1 -0
  31. package/dist/locales/index.d.ts +2 -0
  32. package/dist/locales/index.d.ts.map +1 -0
  33. package/dist/locales/pt-BR.d.ts +2 -0
  34. package/dist/locales/pt-BR.d.ts.map +1 -0
  35. package/dist/registries.d.ts +3 -0
  36. package/dist/registries.d.ts.map +1 -0
  37. package/dist/store.d.ts +38 -0
  38. package/dist/store.d.ts.map +1 -0
  39. package/dist/types.d.ts +302 -0
  40. package/dist/types.d.ts.map +1 -0
  41. package/dist/views/CardsView.d.ts +3 -0
  42. package/dist/views/CardsView.d.ts.map +1 -0
  43. package/dist/views/CashRegistersView.d.ts +3 -0
  44. package/dist/views/CashRegistersView.d.ts.map +1 -0
  45. package/dist/views/CommissionsView.d.ts +3 -0
  46. package/dist/views/CommissionsView.d.ts.map +1 -0
  47. package/dist/views/InvoiceDetailView.d.ts +9 -0
  48. package/dist/views/InvoiceDetailView.d.ts.map +1 -0
  49. package/dist/views/InvoiceFormView.d.ts +13 -0
  50. package/dist/views/InvoiceFormView.d.ts.map +1 -0
  51. package/dist/views/InvoiceListView.d.ts +8 -0
  52. package/dist/views/InvoiceListView.d.ts.map +1 -0
  53. package/dist/views/PayablesView.d.ts +7 -0
  54. package/dist/views/PayablesView.d.ts.map +1 -0
  55. package/dist/views/ReceivablesView.d.ts +7 -0
  56. package/dist/views/ReceivablesView.d.ts.map +1 -0
  57. package/dist/views/StatementsView.d.ts +3 -0
  58. package/dist/views/StatementsView.d.ts.map +1 -0
  59. package/dist/views/SummaryView.d.ts +9 -0
  60. package/dist/views/SummaryView.d.ts.map +1 -0
  61. package/dist/views/dashboardWidgets.d.ts +11 -0
  62. package/dist/views/dashboardWidgets.d.ts.map +1 -0
  63. package/package.json +55 -0
  64. package/src/FinancialContext.tsx +47 -0
  65. package/src/FinancialPage.tsx +261 -0
  66. package/src/README.md +155 -0
  67. package/src/components/FinancialGeneralSettings.tsx +26 -0
  68. package/src/components/FinancialOnboarding.tsx +87 -0
  69. package/src/components/FinancialSettings.tsx +31 -0
  70. package/src/components/PaymentModal.tsx +322 -0
  71. package/src/data/index.ts +2 -0
  72. package/src/data/mock.ts +520 -0
  73. package/src/data/supabase.ts +509 -0
  74. package/src/data/types.ts +53 -0
  75. package/src/index.ts +307 -0
  76. package/src/lib/tenant.ts +3 -0
  77. package/src/locales/en.ts +331 -0
  78. package/src/locales/index.ts +7 -0
  79. package/src/locales/pt-BR.ts +331 -0
  80. package/src/migrations/001_financial_base.sql +109 -0
  81. package/src/migrations/002_chart_of_accounts.sql +28 -0
  82. package/src/migrations/003_card_brands.sql +12 -0
  83. package/src/migrations/004_order_to_cash.sql +164 -0
  84. package/src/migrations/005_seed_defaults.sql +38 -0
  85. package/src/registries.ts +305 -0
  86. package/src/store.ts +221 -0
  87. package/src/types.ts +359 -0
  88. package/src/views/CardsView.tsx +74 -0
  89. package/src/views/CashRegistersView.tsx +597 -0
  90. package/src/views/CommissionsView.tsx +18 -0
  91. package/src/views/InvoiceDetailView.tsx +595 -0
  92. package/src/views/InvoiceFormView.tsx +900 -0
  93. package/src/views/InvoiceListView.tsx +177 -0
  94. package/src/views/PayablesView.tsx +37 -0
  95. package/src/views/ReceivablesView.tsx +37 -0
  96. package/src/views/StatementsView.tsx +123 -0
  97. package/src/views/SummaryView.tsx +12 -0
  98. package/src/views/dashboardWidgets.tsx +191 -0
@@ -0,0 +1,509 @@
1
+ import type { FinancialDataProvider } from './types'
2
+ import type {
3
+ Invoice, InvoiceItem, FinancialMovement, BankAccount,
4
+ CashSession, PaymentMethod, PaymentMethodType, ChartOfAccountsNode,
5
+ CardTransaction,
6
+ CreateInvoiceInput, PayMovementInput,
7
+ OpenCashSessionInput, CloseCashSessionInput, CreateBankAccountInput,
8
+ InvoiceQuery, MovementQuery, StatementQuery,
9
+ PaginatedResult, FinancialSummary, StatementEntry, CashSessionSummary,
10
+ DateRange,
11
+ } from '../types'
12
+ import { getSupabaseClientOptional, getActiveTenantId } from '@fayz-ai/core'
13
+ import { getFinancialTenantId } from '../lib/tenant'
14
+
15
+ function getTenantId(): string | undefined {
16
+ // Local override wins; else use the app's active tenant so writes pass RLS.
17
+ return getFinancialTenantId() ?? getActiveTenantId()
18
+ }
19
+
20
+ function snakeToCamel(obj: Record<string, unknown>): Record<string, unknown> {
21
+ const result: Record<string, unknown> = {}
22
+ for (const [key, value] of Object.entries(obj)) {
23
+ result[key.replace(/_([a-z])/g, (_, c) => c.toUpperCase())] = value
24
+ }
25
+ return result
26
+ }
27
+
28
+ function camelToSnake(obj: Record<string, unknown>): Record<string, unknown> {
29
+ const result: Record<string, unknown> = {}
30
+ for (const [key, value] of Object.entries(obj)) {
31
+ if (key.startsWith('_')) continue
32
+ result[key.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`)] = value
33
+ }
34
+ return result
35
+ }
36
+
37
+ function getClients() {
38
+ const supabase = getSupabaseClientOptional() as any
39
+ if (!supabase) throw new Error('Supabase not initialized')
40
+ return { core: supabase.schema('saas_core'), pub: supabase }
41
+ }
42
+
43
+ /** Map order stage to InvoiceStatus for the financial UI */
44
+ function mapStageToInvoiceStatus(stage?: string): string {
45
+ switch (stage) {
46
+ case 'paid': return 'paid'
47
+ case 'partial': return 'partial'
48
+ case 'overdue': return 'overdue'
49
+ case 'cancelled': return 'cancelled'
50
+ case 'invoiced': return 'open'
51
+ case 'booked': return 'open'
52
+ default: return 'open'
53
+ }
54
+ }
55
+
56
+ let _overdueSwept = false
57
+
58
+ /** Batch-mark overdue: pending/partial movements past due → 'overdue', then update parent invoices */
59
+ async function sweepOverdue() {
60
+ if (_overdueSwept) return
61
+ _overdueSwept = true
62
+ try {
63
+ const { core, pub } = getClients()
64
+ const today = new Date().toISOString().slice(0, 10)
65
+
66
+ // Mark overdue movements
67
+ const { data: overdueMovs } = await pub.from('financial_movements')
68
+ .update({ status: 'overdue' })
69
+ .in('status', ['pending', 'partial'])
70
+ .lt('due_date', today)
71
+ .select('invoice_id')
72
+
73
+ // Collect unique invoice IDs and update their status
74
+ if (overdueMovs && overdueMovs.length > 0) {
75
+ const invoiceIds = [...new Set(overdueMovs.map((m: any) => m.invoice_id).filter(Boolean))]
76
+ for (const invId of invoiceIds) {
77
+ const { data: allMovs } = await pub.from('financial_movements')
78
+ .select('status')
79
+ .eq('invoice_id', invId)
80
+ .eq('movement_kind', 'bill')
81
+ const movs = allMovs ?? []
82
+ const hasOverdue = movs.some((m: any) => m.status === 'overdue')
83
+ const allPaid = movs.every((m: any) => m.status === 'paid')
84
+ const invoiceStatus = allPaid ? 'paid' : hasOverdue ? 'overdue' : 'open'
85
+ const invoiceStage = allPaid ? 'paid' : hasOverdue ? 'overdue' : 'invoiced'
86
+ // Only update financial stage — don't overwrite agenda status
87
+ await core.from('orders').update({ stage: invoiceStage }).eq('id', invId)
88
+ }
89
+ }
90
+ } catch {
91
+ // Non-critical — don't block the UI
92
+ }
93
+ }
94
+
95
+ // S2 (data-model refactor): paid amount, balance and status are DERIVED from the
96
+ // append-only ledger via v_invoice_balances — the single source of truth — not
97
+ // from stored order.stage / metadata.paidAmount (which drift). See
98
+ // docs/dogfood-sprint/DATA-MODEL-REFACTOR.md.
99
+ async function fetchInvoiceBalances(
100
+ pub: any,
101
+ ids: string[],
102
+ ): Promise<Map<string, { paid: number; balance: number; status: string }>> {
103
+ const map = new Map<string, { paid: number; balance: number; status: string }>()
104
+ if (!ids.length) return map
105
+ const { data } = await pub
106
+ .from('v_invoice_balances')
107
+ .select('invoice_id, paid, balance, status')
108
+ .in('invoice_id', ids)
109
+ for (const r of (data ?? []) as any[]) {
110
+ map.set(r.invoice_id, { paid: Number(r.paid) || 0, balance: Number(r.balance) || 0, status: r.status })
111
+ }
112
+ return map
113
+ }
114
+
115
+ export function createSupabaseFinancialProvider(): FinancialDataProvider {
116
+ const provider: FinancialDataProvider = {
117
+ // --- Invoices (orders at stage=invoiced/paid/partial/overdue/cancelled with direction) ---
118
+ async getInvoices(query: InvoiceQuery): Promise<PaginatedResult<Invoice>> {
119
+ const { core, pub } = getClients()
120
+ let qb = core.from('orders').select('*', { count: 'exact' })
121
+
122
+ // Show orders with financial relevance: booked (with amount) + invoiced stages
123
+ qb = qb.in('stage', ['booked', 'invoiced', 'paid', 'partial', 'overdue', 'cancelled'])
124
+ qb = qb.gt('total', 0)
125
+
126
+ // Filter by direction (credit=receivable, debit=payable)
127
+ if (query.direction) {
128
+ qb = qb.eq('direction', query.direction)
129
+ }
130
+
131
+ if (query.status) {
132
+ const statuses = Array.isArray(query.status) ? query.status : [query.status]
133
+ qb = qb.in('status', statuses)
134
+ }
135
+ if (query.contactId) qb = qb.eq('party_id', query.contactId)
136
+ if (query.search) qb = qb.ilike('notes', `%${query.search}%`)
137
+ if (query.dateRange) qb = qb.gte('created_at', query.dateRange.from).lte('created_at', query.dateRange.to)
138
+ const page = query.page ?? 1
139
+ const pageSize = query.pageSize ?? 50
140
+ qb = qb.range((page - 1) * pageSize, page * pageSize - 1).order('created_at', { ascending: false })
141
+ const { data, count } = await qb
142
+
143
+ const bal = await fetchInvoiceBalances(pub, (data ?? []).map((r: any) => r.id))
144
+ const invoices: Invoice[] = (data ?? []).map((r: any) => {
145
+ const meta = r.metadata ?? {}
146
+ // Resolve direction from stage model or legacy kind
147
+ const dir = r.direction ?? (r.kind === 'invoice_payable' ? 'debit' : 'credit')
148
+ const b = bal.get(r.id)
149
+ return {
150
+ id: r.id,
151
+ direction: dir,
152
+ invoiceDate: r.created_at?.slice(0, 10) ?? '',
153
+ fiscalNumber: r.reference_number,
154
+ totalAmount: r.total ?? 0,
155
+ paidAmount: b?.paid ?? meta.paidAmount ?? 0,
156
+ status: b?.status ?? mapStageToInvoiceStatus(r.stage),
157
+ totalInstallments: meta.installmentCount ?? 1,
158
+ contactId: r.party_id,
159
+ contactName: meta.contactName,
160
+ observations: r.notes,
161
+ itemsSummary: meta.itemsSummary,
162
+ tenantId: r.tenant_id,
163
+ createdAt: r.created_at,
164
+ updatedAt: r.updated_at,
165
+ } as Invoice
166
+ })
167
+ return { data: invoices, total: count ?? 0 }
168
+ },
169
+
170
+ async getInvoiceById(id: string): Promise<Invoice | null> {
171
+ const { core, pub } = getClients()
172
+ const { data } = await core.from('orders').select('*').eq('id', id).single()
173
+ if (!data) return null
174
+ const o = snakeToCamel(data) as any
175
+ const meta = data.metadata ?? {}
176
+ // Paid amount + status are DERIVED from the ledger view (single source of truth).
177
+ const balance = (await fetchInvoiceBalances(pub, [id])).get(id)
178
+ const { data: movs } = await pub.from('financial_movements')
179
+ .select('paid_amount')
180
+ .eq('invoice_id', id)
181
+ .eq('movement_kind', 'bill')
182
+ return {
183
+ id: o.id,
184
+ direction: o.direction ?? (o.kind === 'invoice_payable' ? 'debit' : 'credit'),
185
+ invoiceDate: o.createdAt?.slice(0, 10) ?? '',
186
+ fiscalNumber: o.referenceNumber,
187
+ totalAmount: o.total ?? 0, paidAmount: balance?.paid ?? 0,
188
+ status: balance?.status ?? mapStageToInvoiceStatus(o.stage),
189
+ totalInstallments: (movs ?? []).length || 1, contactId: o.partyId,
190
+ contactName: meta.contactName, itemsSummary: meta.itemsSummary,
191
+ bookingStartsAt: o.startsAt ?? undefined,
192
+ observations: o.notes,
193
+ tenantId: o.tenantId, createdAt: o.createdAt, updatedAt: o.updatedAt,
194
+ } as Invoice
195
+ },
196
+
197
+ async getInvoiceItems(invoiceId: string): Promise<InvoiceItem[]> {
198
+ const { core, pub } = getClients()
199
+ const { data } = await core.from('order_items').select('*').eq('order_id', invoiceId).order('sort_order')
200
+ return (data ?? []).map((r: any) => {
201
+ const i = snakeToCamel(r) as any
202
+ return {
203
+ id: i.id, invoiceId, itemKind: i.metadata?.itemKind ?? 'other',
204
+ description: i.name ?? '', quantity: i.quantity ?? 1,
205
+ unitPrice: i.unitPrice ?? 0, totalAmount: i.total ?? 0,
206
+ discount: i.discount ?? 0, surcharge: 0,
207
+ referenceId: i.productId ?? i.serviceId,
208
+ createdAt: i.createdAt,
209
+ } as InvoiceItem
210
+ })
211
+ },
212
+
213
+ async createInvoice(input: CreateInvoiceInput): Promise<Invoice> {
214
+ const { core, pub } = getClients()
215
+ const tenantId = getTenantId()
216
+ const kind = input.direction === 'debit' ? 'invoice_payable' : 'invoice_receivable'
217
+ const totalAmount = input.items.reduce((sum, item) => sum + item.quantity * item.unitPrice - (item.discount ?? 0) + (item.surcharge ?? 0), 0)
218
+ const itemsSummary = input.items.map((i: any) => i.description).filter(Boolean).join(', ')
219
+
220
+ // Auto-generate reference number if not provided
221
+ let referenceNumber = input.fiscalNumber
222
+ if (!referenceNumber && tenantId) {
223
+ const prefix = input.direction === 'credit' ? 'REC' : 'PAG'
224
+ const { data: seq } = await core.rpc('next_sequence', { p_tenant_id: tenantId, p_kind: kind })
225
+ if (seq) referenceNumber = `${prefix}-${String(seq).padStart(5, '0')}`
226
+ }
227
+
228
+ const { data: order } = await core.from('orders').insert({
229
+ tenant_id: tenantId, kind, status: 'open',
230
+ stage: 'invoiced',
231
+ direction: input.direction,
232
+ reference_number: referenceNumber,
233
+ total: totalAmount, party_id: input.contactId,
234
+ notes: input.observations,
235
+ currency: 'BRL',
236
+ metadata: { itemsSummary, contactName: input.contactName, installmentCount: input.installments.length || 1 },
237
+ }).select().single()
238
+
239
+ if (order && input.items.length > 0) {
240
+ await core.from('order_items').insert(
241
+ input.items.map((item, i) => ({
242
+ order_id: order.id, name: item.description,
243
+ quantity: item.quantity, unit_price: item.unitPrice,
244
+ discount: item.discount ?? 0,
245
+ total: item.quantity * item.unitPrice - (item.discount ?? 0) + (item.surcharge ?? 0),
246
+ sort_order: i,
247
+ metadata: { itemKind: item.itemKind, referenceId: item.referenceId },
248
+ }))
249
+ )
250
+ }
251
+
252
+ // Create installment movements
253
+ if (order && input.installments.length > 0) {
254
+ await pub.from('financial_movements').insert(
255
+ input.installments.map((inst, i) => ({
256
+ tenant_id: tenantId, invoice_id: order.id,
257
+ direction: input.direction, movement_kind: inst.movementKind,
258
+ amount: inst.amount, paid_amount: 0, status: 'pending',
259
+ due_date: inst.dueDate, installment_number: inst.installmentNumber ?? i + 1,
260
+ }))
261
+ )
262
+ }
263
+
264
+ return {
265
+ id: order!.id, direction: input.direction, invoiceDate: new Date().toISOString().slice(0, 10),
266
+ fiscalNumber: referenceNumber,
267
+ totalAmount, paidAmount: 0, status: 'open', totalInstallments: input.installments.length || 1,
268
+ contactName: input.contactName, itemsSummary,
269
+ tenantId: tenantId!, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
270
+ } as Invoice
271
+ },
272
+
273
+ async updateInvoice(id: string, partial: Partial<Invoice>): Promise<Invoice> {
274
+ const { core, pub } = getClients()
275
+ const row: any = {}
276
+ if (partial.observations) row.notes = partial.observations
277
+ if (partial.totalAmount) row.total = partial.totalAmount
278
+ if (partial.status) row.status = partial.status
279
+ if (Object.keys(row).length > 0) await core.from('orders').update(row).eq('id', id)
280
+ return (await provider.getInvoiceById(id))!
281
+ },
282
+
283
+ async cancelInvoice(id: string): Promise<void> {
284
+ const { core, pub } = getClients()
285
+ await core.from('orders').update({ status: 'cancelled' }).eq('id', id)
286
+ await pub.from('financial_movements').update({ status: 'cancelled' }).eq('invoice_id', id).eq('status', 'pending')
287
+ },
288
+
289
+ // --- Movements (public.financial_movements) ---
290
+ async getMovements(query: MovementQuery): Promise<PaginatedResult<FinancialMovement>> {
291
+ const { core, pub } = getClients()
292
+ let qb = pub.from('financial_movements').select('*', { count: 'exact' })
293
+ if (query.direction) qb = qb.eq('direction', query.direction)
294
+ if (query.status) {
295
+ const statuses = Array.isArray(query.status) ? query.status : [query.status]
296
+ qb = qb.in('status', statuses)
297
+ }
298
+ if (query.bankAccountId) qb = qb.eq('bank_account_id', query.bankAccountId)
299
+ if (query.cashSessionId) qb = qb.eq('cash_session_id', query.cashSessionId)
300
+ if (query.dateRange) qb = qb.gte('due_date', query.dateRange.from).lte('due_date', query.dateRange.to)
301
+ const page = query.page ?? 1
302
+ const pageSize = query.pageSize ?? 50
303
+ qb = qb.range((page - 1) * pageSize, page * pageSize - 1).order('due_date')
304
+ const { data, count } = await qb
305
+ return { data: (data ?? []).map((r: any) => snakeToCamel(r) as unknown as FinancialMovement), total: count ?? 0 }
306
+ },
307
+
308
+ async payMovement(input: PayMovementInput): Promise<FinancialMovement> {
309
+ const { core, pub } = getClients()
310
+ const { data: mov } = await pub.from('financial_movements').select('*').eq('id', input.movementId).single()
311
+ if (!mov) throw new Error('Movement not found')
312
+ const newPaid = (mov.paid_amount ?? 0) + input.amount
313
+ const newStatus = newPaid >= mov.amount ? 'paid' : 'partial'
314
+ const updates: any = { paid_amount: newPaid, status: newStatus, payment_date: input.paymentDate }
315
+ if (input.paymentMethodId) updates.payment_method_id = input.paymentMethodId
316
+ if (input.paymentMethodTypeId) updates.payment_method_type_id = input.paymentMethodTypeId
317
+ if (input.bankAccountId) updates.bank_account_id = input.bankAccountId
318
+ if (input.cashSessionId) updates.cash_session_id = input.cashSessionId
319
+ if (input.cardBrand) updates.card_brand = input.cardBrand
320
+ if (input.cardInstallments) updates.card_installments = input.cardInstallments
321
+ const { data } = await pub.from('financial_movements').update(updates).eq('id', input.movementId).select().single()
322
+
323
+ // Update invoice (order) status based on all its movements
324
+ if (mov.invoice_id) {
325
+ const { data: allMovs } = await pub.from('financial_movements')
326
+ .select('status, amount, paid_amount')
327
+ .eq('invoice_id', mov.invoice_id)
328
+ .eq('movement_kind', 'bill')
329
+ const movs = allMovs ?? []
330
+ const allPaid = movs.length > 0 && movs.every((m: any) => m.status === 'paid')
331
+ const anyPaid = movs.some((m: any) => m.status === 'paid' || m.status === 'partial')
332
+ const invoiceStatus = allPaid ? 'paid' : anyPaid ? 'partial' : 'open'
333
+ const totalPaid = movs.reduce((sum: number, m: any) => sum + (m.paid_amount ?? 0), 0)
334
+ // Store paid amount in metadata (orders table has no dedicated paid column)
335
+ const { data: order } = await core.from('orders').select('metadata').eq('id', mov.invoice_id).single()
336
+ const meta = (order?.metadata as any) ?? {}
337
+ const invoiceStage = allPaid ? 'paid' : anyPaid ? 'partial' : 'invoiced'
338
+ // Only update financial order status. Agenda status lives on saas_core.bookings.
339
+ await core.from('orders').update({
340
+ status: invoiceStage,
341
+ metadata: { ...meta, paidAmount: totalPaid },
342
+ }).eq('id', mov.invoice_id)
343
+ }
344
+
345
+ return snakeToCamel(data!) as unknown as FinancialMovement
346
+ },
347
+
348
+ async cancelMovement(id: string): Promise<void> {
349
+ const { core, pub } = getClients()
350
+ await pub.from('financial_movements').update({ status: 'cancelled' }).eq('id', id)
351
+ },
352
+
353
+ // --- Bank Accounts ---
354
+ async getBankAccounts(): Promise<BankAccount[]> {
355
+ const { core, pub } = getClients()
356
+ const { data } = await pub.from('bank_accounts').select('*').eq('is_active', true).order('name')
357
+ return (data ?? []).map((r: any) => snakeToCamel(r) as unknown as BankAccount)
358
+ },
359
+
360
+ async createBankAccount(input: CreateBankAccountInput): Promise<BankAccount> {
361
+ const { core, pub } = getClients()
362
+ const tenantId = getTenantId()
363
+ const { data } = await pub.from('bank_accounts').insert({
364
+ ...camelToSnake(input as any), tenant_id: tenantId, current_balance: input.initialBalance ?? 0,
365
+ }).select().single()
366
+ return snakeToCamel(data!) as unknown as BankAccount
367
+ },
368
+
369
+ async updateBankAccount(id: string, partial: Partial<BankAccount>): Promise<BankAccount> {
370
+ const { core, pub } = getClients()
371
+ const row = camelToSnake(partial as any)
372
+ delete row.id; delete row.tenant_id
373
+ const { data } = await pub.from('bank_accounts').update(row).eq('id', id).select().single()
374
+ return snakeToCamel(data!) as unknown as BankAccount
375
+ },
376
+
377
+ // --- Cash Sessions ---
378
+ async getCashSessions(bankAccountId?: string): Promise<CashSession[]> {
379
+ const { core, pub } = getClients()
380
+ let qb = pub.from('cash_register_sessions').select('*')
381
+ if (bankAccountId) qb = qb.eq('bank_account_id', bankAccountId)
382
+ const { data } = await qb.order('opened_at', { ascending: false })
383
+ return (data ?? []).map((r: any) => snakeToCamel(r) as unknown as CashSession)
384
+ },
385
+
386
+ async getOpenSession(bankAccountId: string): Promise<CashSession | null> {
387
+ const { core, pub } = getClients()
388
+ const { data } = await pub.from('cash_register_sessions').select('*').eq('bank_account_id', bankAccountId).eq('status', 'open').single()
389
+ return data ? snakeToCamel(data) as unknown as CashSession : null
390
+ },
391
+
392
+ async openCashSession(input: OpenCashSessionInput): Promise<CashSession> {
393
+ const { core, pub } = getClients()
394
+ const tenantId = getTenantId()
395
+ const { data } = await pub.from('cash_register_sessions').insert({
396
+ tenant_id: tenantId, bank_account_id: input.bankAccountId,
397
+ status: 'open', opening_balance: input.openingBalance,
398
+ opened_by_user_id: input.openedByUserId, opened_by_name: input.openedByName,
399
+ unit_id: input.unitId,
400
+ }).select().single()
401
+ return snakeToCamel(data!) as unknown as CashSession
402
+ },
403
+
404
+ async closeCashSession(input: CloseCashSessionInput): Promise<CashSession> {
405
+ const { core, pub } = getClients()
406
+ const { data } = await pub.from('cash_register_sessions').update({
407
+ status: 'closed', closing_balance: input.closingBalance,
408
+ closed_at: new Date().toISOString(),
409
+ closed_by_user_id: input.closedByUserId, closed_by_name: input.closedByName,
410
+ notes: input.notes,
411
+ }).eq('id', input.sessionId).select().single()
412
+ return snakeToCamel(data!) as unknown as CashSession
413
+ },
414
+
415
+ async getCashSessionSummary(sessionId: string): Promise<CashSessionSummary> {
416
+ const { core, pub } = getClients()
417
+ const session = (await pub.from('cash_register_sessions').select('*').eq('id', sessionId).single()).data
418
+ const { data: movs } = await pub.from('financial_movements').select('direction, paid_amount').eq('cash_session_id', sessionId).eq('status', 'paid')
419
+ const movements = movs ?? []
420
+ return {
421
+ session: snakeToCamel(session!) as unknown as CashSession,
422
+ movementCount: movements.length,
423
+ totalInflow: movements.filter((m: any) => m.direction === 'credit').reduce((s: number, m: any) => s + m.paid_amount, 0),
424
+ totalOutflow: movements.filter((m: any) => m.direction === 'debit').reduce((s: number, m: any) => s + m.paid_amount, 0),
425
+ }
426
+ },
427
+
428
+ // --- Payment Methods ---
429
+ async getPaymentMethods(): Promise<PaymentMethod[]> {
430
+ const { core, pub } = getClients()
431
+ const { data } = await pub.from('payment_methods').select('*').eq('is_active', true).order('name')
432
+ return (data ?? []).map((r: any) => snakeToCamel(r) as unknown as PaymentMethod)
433
+ },
434
+
435
+ async getPaymentMethodTypes(): Promise<PaymentMethodType[]> {
436
+ const { core, pub } = getClients()
437
+ const { data } = await pub.from('payment_method_types').select('*').eq('is_active', true).order('name')
438
+ return (data ?? []).map((r: any) => snakeToCamel(r) as unknown as PaymentMethodType)
439
+ },
440
+
441
+ // --- Chart of Accounts ---
442
+ async getChartOfAccounts(): Promise<ChartOfAccountsNode[]> {
443
+ const { core, pub } = getClients()
444
+ const { data } = await pub.from('chart_of_accounts').select('*').eq('is_active', true).order('code')
445
+ return (data ?? []).map((r: any) => snakeToCamel(r) as unknown as ChartOfAccountsNode)
446
+ },
447
+
448
+ // --- Card Transactions ---
449
+ async getCardTransactions(dateRange?: DateRange): Promise<CardTransaction[]> {
450
+ const { core, pub } = getClients()
451
+ return [] // TODO: implement when card_transactions table exists
452
+ },
453
+
454
+ // --- Statements ---
455
+ async getStatement(query: StatementQuery): Promise<StatementEntry[]> {
456
+ const { core, pub } = getClients()
457
+ const { data: movs } = await pub.from('financial_movements').select('*')
458
+ .eq('bank_account_id', query.bankAccountId).eq('status', 'paid')
459
+ .gte('payment_date', query.dateRange.from).lte('payment_date', query.dateRange.to)
460
+ .order('payment_date')
461
+
462
+ const { data: account } = await pub.from('bank_accounts').select('initial_balance').eq('id', query.bankAccountId).single()
463
+ let balance = account?.initial_balance ?? 0
464
+
465
+ return (movs ?? []).map((r: any) => {
466
+ const mov = snakeToCamel(r) as unknown as FinancialMovement
467
+ if (mov.direction === 'credit') balance += mov.paidAmount
468
+ else balance -= mov.paidAmount
469
+ return { movement: mov, runningBalance: balance }
470
+ })
471
+ },
472
+
473
+ // --- Summary ---
474
+ async getSummary(dateRange?: DateRange): Promise<FinancialSummary> {
475
+ // Sweep overdue on first load (background, non-blocking)
476
+ sweepOverdue()
477
+ const { core, pub } = getClients()
478
+ const { data: accounts } = await pub.from('bank_accounts').select('current_balance').eq('is_active', true)
479
+ const totalBalance = (accounts ?? []).reduce((s: number, a: any) => s + (a.current_balance ?? 0), 0)
480
+
481
+ const { data: movs } = await pub.from('financial_movements').select('direction, movement_kind, amount, paid_amount, status, due_date, payment_date')
482
+ const allMovs = movs ?? []
483
+ const unpaid = allMovs.filter((m: any) => ['pending', 'partial', 'overdue'].includes(m.status))
484
+ const today = new Date().toISOString().slice(0, 10)
485
+
486
+ const unpaidCredit = unpaid.filter((m: any) => m.direction === 'credit')
487
+ const unpaidDebit = unpaid.filter((m: any) => m.direction === 'debit')
488
+ const overdueCredit = unpaidCredit.filter((m: any) => m.status === 'overdue' || m.due_date < today)
489
+ const overdueDebit = unpaidDebit.filter((m: any) => m.status === 'overdue' || m.due_date < today)
490
+
491
+ const monthStart = today.slice(0, 7) + '-01'
492
+ const monthPaid = allMovs.filter((m: any) => m.status === 'paid' && m.payment_date >= monthStart)
493
+
494
+ return {
495
+ totalBalance,
496
+ totalReceivable: unpaidCredit.reduce((s: number, m: any) => s + m.amount - m.paid_amount, 0),
497
+ totalPayable: unpaidDebit.reduce((s: number, m: any) => s + m.amount - m.paid_amount, 0),
498
+ monthlyInflow: monthPaid.filter((m: any) => m.direction === 'credit').reduce((s: number, m: any) => s + m.paid_amount, 0),
499
+ monthlyOutflow: monthPaid.filter((m: any) => m.direction === 'debit').reduce((s: number, m: any) => s + m.paid_amount, 0),
500
+ overdueReceivableCount: overdueCredit.length,
501
+ overdueReceivableAmount: overdueCredit.reduce((s: number, m: any) => s + m.amount - m.paid_amount, 0),
502
+ overduePayableCount: overdueDebit.length,
503
+ overduePayableAmount: overdueDebit.reduce((s: number, m: any) => s + m.amount - m.paid_amount, 0),
504
+ }
505
+ },
506
+ }
507
+
508
+ return provider
509
+ }
@@ -0,0 +1,53 @@
1
+ import type {
2
+ Invoice, InvoiceItem, FinancialMovement, BankAccount,
3
+ CashSession, PaymentMethod, PaymentMethodType, ChartOfAccountsNode,
4
+ CardTransaction,
5
+ CreateInvoiceInput, PayMovementInput,
6
+ OpenCashSessionInput, CloseCashSessionInput, CreateBankAccountInput,
7
+ InvoiceQuery, MovementQuery, StatementQuery,
8
+ PaginatedResult, FinancialSummary, StatementEntry, CashSessionSummary,
9
+ DateRange,
10
+ } from '../types'
11
+
12
+ export interface FinancialDataProvider {
13
+ // --- Invoices ---
14
+ getInvoices(query: InvoiceQuery): Promise<PaginatedResult<Invoice>>
15
+ getInvoiceById(id: string): Promise<Invoice | null>
16
+ getInvoiceItems(invoiceId: string): Promise<InvoiceItem[]>
17
+ createInvoice(input: CreateInvoiceInput): Promise<Invoice>
18
+ updateInvoice(id: string, data: Partial<Invoice>): Promise<Invoice>
19
+ cancelInvoice(id: string): Promise<void>
20
+
21
+ // --- Movements ---
22
+ getMovements(query: MovementQuery): Promise<PaginatedResult<FinancialMovement>>
23
+ payMovement(input: PayMovementInput): Promise<FinancialMovement>
24
+ cancelMovement(id: string): Promise<void>
25
+
26
+ // --- Bank Accounts ---
27
+ getBankAccounts(): Promise<BankAccount[]>
28
+ createBankAccount(data: CreateBankAccountInput): Promise<BankAccount>
29
+ updateBankAccount(id: string, data: Partial<BankAccount>): Promise<BankAccount>
30
+
31
+ // --- Cash Sessions ---
32
+ getCashSessions(bankAccountId?: string): Promise<CashSession[]>
33
+ getOpenSession(bankAccountId: string): Promise<CashSession | null>
34
+ openCashSession(input: OpenCashSessionInput): Promise<CashSession>
35
+ closeCashSession(input: CloseCashSessionInput): Promise<CashSession>
36
+ getCashSessionSummary(sessionId: string): Promise<CashSessionSummary>
37
+
38
+ // --- Payment Methods ---
39
+ getPaymentMethods(): Promise<PaymentMethod[]>
40
+ getPaymentMethodTypes(): Promise<PaymentMethodType[]>
41
+
42
+ // --- Chart of Accounts ---
43
+ getChartOfAccounts(): Promise<ChartOfAccountsNode[]>
44
+
45
+ // --- Card Transactions ---
46
+ getCardTransactions(dateRange?: DateRange): Promise<CardTransaction[]>
47
+
48
+ // --- Statements ---
49
+ getStatement(query: StatementQuery): Promise<StatementEntry[]>
50
+
51
+ // --- Summary / Dashboard ---
52
+ getSummary(dateRange?: DateRange): Promise<FinancialSummary>
53
+ }