@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.
- package/README.md +2 -0
- package/dist/FinancialContext.d.ts +2 -0
- package/dist/FinancialContext.d.ts.map +1 -1
- package/dist/FinancialPage.d.ts.map +1 -1
- package/dist/data/index.d.ts +1 -0
- package/dist/data/index.d.ts.map +1 -1
- package/dist/data/mock.d.ts +15 -0
- package/dist/data/mock.d.ts.map +1 -1
- package/dist/data/supabase.d.ts.map +1 -1
- package/dist/data/tables.d.ts +11 -0
- package/dist/data/tables.d.ts.map +1 -0
- package/dist/data/types.d.ts +2 -1
- package/dist/data/types.d.ts.map +1 -1
- package/dist/index.cjs +1594 -252
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +13 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1456 -131
- package/dist/index.js.map +1 -1
- package/dist/locales/en.d.ts.map +1 -1
- package/dist/locales/pt-BR.d.ts.map +1 -1
- package/dist/migrations/index.d.ts +16 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/quick-add.d.ts +13 -0
- package/dist/quick-add.d.ts.map +1 -0
- package/dist/registries.d.ts.map +1 -1
- package/dist/store.d.ts +3 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/types.d.ts +42 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/views/CardsView.d.ts.map +1 -1
- package/dist/views/InvoiceListView.d.ts.map +1 -1
- package/dist/views/QuickTransactionForm.d.ts +16 -0
- package/dist/views/QuickTransactionForm.d.ts.map +1 -0
- package/dist/views/SummaryView.d.ts +5 -4
- package/dist/views/SummaryView.d.ts.map +1 -1
- package/dist/views/dashboardWidgets.d.ts.map +1 -1
- package/package.json +7 -4
- package/src/FinancialContext.tsx +2 -0
- package/src/FinancialPage.tsx +86 -7
- package/src/README.md +5 -5
- package/src/contract.test.ts +1 -1
- package/src/data/capability.test.ts +45 -0
- package/src/data/index.ts +1 -0
- package/src/data/mock.ts +50 -10
- package/src/data/supabase.ts +95 -49
- package/src/data/tables.ts +12 -0
- package/src/data/types.ts +2 -1
- package/src/index.ts +24 -4
- package/src/locales/en.ts +75 -0
- package/src/locales/pt-BR.ts +75 -0
- package/src/migrations/000_plg_rename.sql +30 -0
- package/src/migrations/001_financial_base.sql +30 -30
- package/src/migrations/002_chart_of_accounts.sql +10 -10
- package/src/migrations/003_card_brands.sql +4 -4
- package/src/migrations/004_order_to_cash.sql +28 -28
- package/src/migrations/005_seed_defaults.sql +6 -6
- package/src/migrations/006_rls_policies.sql +5 -5
- package/src/migrations/{006_extract_fee_amount.sql → 006b_extract_fee_amount.sql} +4 -4
- package/src/migrations/007_reconciliation.sql +9 -9
- package/src/migrations/007b_movement_payment_method_type.sql +6 -0
- package/src/migrations/008_split_payment_movements.sql +5 -5
- package/src/migrations/index.ts +544 -0
- package/src/quick-add.ts +58 -0
- package/src/registries.ts +8 -7
- package/src/store.ts +88 -0
- package/src/types.ts +45 -0
- package/src/views/CardsView.tsx +186 -50
- package/src/views/InvoiceListView.tsx +60 -3
- package/src/views/QuickTransactionForm.tsx +537 -0
- package/src/views/SummaryView.tsx +197 -6
- 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(
|
|
69
|
+
function createStore(options?: MockFinancialProviderOptions): MockStore {
|
|
69
70
|
const tenantId = 'mock-tenant'
|
|
71
|
+
const seed = options?.seed
|
|
70
72
|
|
|
71
|
-
const defaultPMTypes: PaymentMethodType[] = (
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
|
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
|
-
|
|
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) =>
|
package/src/data/supabase.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,
|
|
@@ -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
|
-
|
|
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(
|
|
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(
|
|
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/
|
|
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(
|
|
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(
|
|
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: {
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
520
|
-
const { data: movs } = await pub.from(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
830
|
-
await pub.from(
|
|
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(
|
|
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[]>
|