@fayz-ai/financial 0.0.0-stage → 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 (161) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +34 -2
  3. package/dist/chunk-2XWEZXFT.js +2805 -0
  4. package/dist/chunk-2XWEZXFT.js.map +1 -0
  5. package/dist/chunk-4FWWKKZB.js +49 -0
  6. package/dist/chunk-4FWWKKZB.js.map +1 -0
  7. package/dist/chunk-4GXBOEI3.js +50 -0
  8. package/dist/chunk-4GXBOEI3.js.map +1 -0
  9. package/dist/chunk-4S5GK776.js +31 -0
  10. package/dist/chunk-4S5GK776.js.map +1 -0
  11. package/dist/chunk-56NQHYL7.js +20 -0
  12. package/dist/chunk-56NQHYL7.js.map +1 -0
  13. package/dist/chunk-7HISDBUG.js +84 -0
  14. package/dist/chunk-7HISDBUG.js.map +1 -0
  15. package/dist/chunk-FNXIAIUZ.js +81 -0
  16. package/dist/chunk-FNXIAIUZ.js.map +1 -0
  17. package/dist/chunk-HJPUHTI2.js +77 -0
  18. package/dist/chunk-HJPUHTI2.js.map +1 -0
  19. package/dist/chunk-IP5YH4S5.js +14 -0
  20. package/dist/chunk-IP5YH4S5.js.map +1 -0
  21. package/dist/chunk-J7BWATQU.js +10 -0
  22. package/dist/chunk-J7BWATQU.js.map +1 -0
  23. package/dist/chunk-MCSPCXQK.js +190 -0
  24. package/dist/chunk-MCSPCXQK.js.map +1 -0
  25. package/dist/chunk-T7WIM2V7.js +34 -0
  26. package/dist/chunk-T7WIM2V7.js.map +1 -0
  27. package/dist/chunk-TZZNVPBY.js +9 -0
  28. package/dist/chunk-TZZNVPBY.js.map +1 -0
  29. package/dist/chunk-UERJA7QH.js +893 -0
  30. package/dist/chunk-UERJA7QH.js.map +1 -0
  31. package/dist/chunk-XVELDCSL.js +11 -0
  32. package/dist/chunk-XVELDCSL.js.map +1 -0
  33. package/dist/chunk-YUNLQYOZ.js +26 -0
  34. package/dist/chunk-YUNLQYOZ.js.map +1 -0
  35. package/dist/chunk-ZFUQBYBG.js +124 -0
  36. package/dist/chunk-ZFUQBYBG.js.map +1 -0
  37. package/dist/data/fx-auto.d.ts +7 -0
  38. package/dist/data/fx-auto.d.ts.map +1 -0
  39. package/dist/data/fx-auto.js +3 -0
  40. package/dist/data/fx-auto.js.map +1 -0
  41. package/dist/data/idempotency.d.ts +2 -0
  42. package/dist/data/idempotency.d.ts.map +1 -0
  43. package/dist/data/idempotency.js +3 -0
  44. package/dist/data/idempotency.js.map +1 -0
  45. package/dist/data/mock.d.ts +29 -0
  46. package/dist/data/mock.d.ts.map +1 -0
  47. package/dist/data/mock.js +13 -0
  48. package/dist/data/mock.js.map +1 -0
  49. package/dist/data/provider.d.ts +265 -0
  50. package/dist/data/provider.d.ts.map +1 -0
  51. package/dist/data/provider.js +3 -0
  52. package/dist/data/provider.js.map +1 -0
  53. package/dist/data/reconcile.d.ts +37 -0
  54. package/dist/data/reconcile.d.ts.map +1 -0
  55. package/dist/data/reconcile.js +5 -0
  56. package/dist/data/reconcile.js.map +1 -0
  57. package/dist/data/supabase.d.ts +3 -0
  58. package/dist/data/supabase.d.ts.map +1 -0
  59. package/dist/data/supabase.js +16 -0
  60. package/dist/data/supabase.js.map +1 -0
  61. package/dist/data/tables.d.ts +39 -0
  62. package/dist/data/tables.d.ts.map +1 -0
  63. package/dist/data/tables.js +3 -0
  64. package/dist/data/tables.js.map +1 -0
  65. package/dist/domain/account.d.ts +66 -0
  66. package/dist/domain/account.d.ts.map +1 -0
  67. package/dist/domain/account.js +3 -0
  68. package/dist/domain/account.js.map +1 -0
  69. package/dist/domain/card.d.ts +33 -0
  70. package/dist/domain/card.d.ts.map +1 -0
  71. package/dist/domain/card.js +3 -0
  72. package/dist/domain/card.js.map +1 -0
  73. package/dist/domain/cash-session.d.ts +80 -0
  74. package/dist/domain/cash-session.d.ts.map +1 -0
  75. package/dist/domain/cash-session.js +4 -0
  76. package/dist/domain/cash-session.js.map +1 -0
  77. package/dist/domain/chart-of-accounts.d.ts +57 -0
  78. package/dist/domain/chart-of-accounts.d.ts.map +1 -0
  79. package/dist/domain/chart-of-accounts.js +114 -0
  80. package/dist/domain/chart-of-accounts.js.map +1 -0
  81. package/dist/domain/client-account.d.ts +28 -0
  82. package/dist/domain/client-account.d.ts.map +1 -0
  83. package/dist/domain/client-account.js +3 -0
  84. package/dist/domain/client-account.js.map +1 -0
  85. package/dist/domain/cnab240.d.ts +182 -0
  86. package/dist/domain/cnab240.d.ts.map +1 -0
  87. package/dist/domain/cnab240.js +265 -0
  88. package/dist/domain/cnab240.js.map +1 -0
  89. package/dist/domain/commission.d.ts +142 -0
  90. package/dist/domain/commission.d.ts.map +1 -0
  91. package/dist/domain/commission.js +3 -0
  92. package/dist/domain/commission.js.map +1 -0
  93. package/dist/domain/counterparty.d.ts +13 -0
  94. package/dist/domain/counterparty.d.ts.map +1 -0
  95. package/dist/domain/counterparty.js +48 -0
  96. package/dist/domain/counterparty.js.map +1 -0
  97. package/dist/domain/gift-card.d.ts +35 -0
  98. package/dist/domain/gift-card.d.ts.map +1 -0
  99. package/dist/domain/gift-card.js +3 -0
  100. package/dist/domain/gift-card.js.map +1 -0
  101. package/dist/domain/goal.d.ts +26 -0
  102. package/dist/domain/goal.d.ts.map +1 -0
  103. package/dist/domain/goal.js +3 -0
  104. package/dist/domain/goal.js.map +1 -0
  105. package/dist/domain/invoice.d.ts +228 -0
  106. package/dist/domain/invoice.d.ts.map +1 -0
  107. package/dist/domain/invoice.js +4 -0
  108. package/dist/domain/invoice.js.map +1 -0
  109. package/dist/domain/money.d.ts +5 -0
  110. package/dist/domain/money.d.ts.map +1 -0
  111. package/dist/domain/money.js +3 -0
  112. package/dist/domain/money.js.map +1 -0
  113. package/dist/domain/movement.d.ts +114 -0
  114. package/dist/domain/movement.d.ts.map +1 -0
  115. package/dist/domain/movement.js +4 -0
  116. package/dist/domain/movement.js.map +1 -0
  117. package/dist/domain/nota-fiscal.d.ts +47 -0
  118. package/dist/domain/nota-fiscal.d.ts.map +1 -0
  119. package/dist/domain/nota-fiscal.js +3 -0
  120. package/dist/domain/nota-fiscal.js.map +1 -0
  121. package/dist/domain/payment-method.d.ts +54 -0
  122. package/dist/domain/payment-method.d.ts.map +1 -0
  123. package/dist/domain/payment-method.js +25 -0
  124. package/dist/domain/payment-method.js.map +1 -0
  125. package/dist/domain/query.d.ts +9 -0
  126. package/dist/domain/query.d.ts.map +1 -0
  127. package/dist/domain/query.js +3 -0
  128. package/dist/domain/query.js.map +1 -0
  129. package/dist/domain/reconciliation.d.ts +277 -0
  130. package/dist/domain/reconciliation.d.ts.map +1 -0
  131. package/dist/domain/reconciliation.js +4 -0
  132. package/dist/domain/reconciliation.js.map +1 -0
  133. package/dist/domain/recurring-bill.d.ts +42 -0
  134. package/dist/domain/recurring-bill.d.ts.map +1 -0
  135. package/dist/domain/recurring-bill.js +3 -0
  136. package/dist/domain/recurring-bill.js.map +1 -0
  137. package/dist/domain/report.d.ts +92 -0
  138. package/dist/domain/report.d.ts.map +1 -0
  139. package/dist/domain/report.js +6 -0
  140. package/dist/domain/report.js.map +1 -0
  141. package/dist/domain/statement-match.d.ts +36 -0
  142. package/dist/domain/statement-match.d.ts.map +1 -0
  143. package/dist/domain/statement-match.js +3 -0
  144. package/dist/domain/statement-match.js.map +1 -0
  145. package/dist/domain/statement.d.ts +75 -0
  146. package/dist/domain/statement.d.ts.map +1 -0
  147. package/dist/domain/statement.js +4 -0
  148. package/dist/domain/statement.js.map +1 -0
  149. package/dist/domain/transfer.d.ts +29 -0
  150. package/dist/domain/transfer.d.ts.map +1 -0
  151. package/dist/domain/transfer.js +3 -0
  152. package/dist/domain/transfer.js.map +1 -0
  153. package/dist/store.d.ts +66 -0
  154. package/dist/store.d.ts.map +1 -0
  155. package/dist/store.js +287 -0
  156. package/dist/store.js.map +1 -0
  157. package/dist/views/zones.d.ts +40 -0
  158. package/dist/views/zones.d.ts.map +1 -0
  159. package/dist/views/zones.js +16 -0
  160. package/dist/views/zones.js.map +1 -0
  161. package/package.json +49 -3
@@ -0,0 +1,2805 @@
1
+ import { hasCriteriaValue } from './chunk-J7BWATQU.js';
2
+ import { ensureReferenceRates } from './chunk-4S5GK776.js';
3
+ import { createIdempotencyKey } from './chunk-IP5YH4S5.js';
4
+ import { NAME_AMOUNT_TOLERANCE, matchStatementLines, scorePair } from './chunk-ZFUQBYBG.js';
5
+ import { emptyStatement, ledgerBalance, buildStatement } from './chunk-HJPUHTI2.js';
6
+ import { transferLegs } from './chunk-YUNLQYOZ.js';
7
+ import { cancellationReason, invoiceTotal, itemsDescription, summarizeInvoiceLines, lineTotal } from './chunk-7HISDBUG.js';
8
+ import { paymentFee, ledgerMethod, toLedgerDirection, fromLedgerDirection } from './chunk-4GXBOEI3.js';
9
+ import { sessionCashFlow } from './chunk-56NQHYL7.js';
10
+ import { runReconcileAsBill, runReconcileLink } from './chunk-FNXIAIUZ.js';
11
+ import { transferForStatementLine, statementLegOf, candidateTolerance, candidateScore, statementLinesPage, pageTotals } from './chunk-MCSPCXQK.js';
12
+ import { round2 } from './chunk-XVELDCSL.js';
13
+ import { T } from './chunk-4FWWKKZB.js';
14
+ import { getActiveTenantId, getSupabaseClientOptional } from '@fayz-ai/core';
15
+
16
+ var INVESTMENT_CODES = ["INVESTMENTS", "AUTOMATICINVESTMENT", "PROCEEDSINTERESTSANDDIVIDENDS", "MUTUALFUNDS", "FIXEDINCOME"];
17
+ function invoiceLinesPayload(items, lineTotal2) {
18
+ return items.map((item, i) => ({
19
+ name: item.description,
20
+ quantity: item.quantity,
21
+ unit_price: item.unitPrice,
22
+ discount: item.discount ?? 0,
23
+ total: lineTotal2(item),
24
+ sort_order: i,
25
+ metadata: { ...item.metadata, itemKind: item.itemKind, referenceId: item.referenceId }
26
+ }));
27
+ }
28
+ async function invoiceTenantId(pub, invoiceId) {
29
+ const { data, error } = await pub.from("plg_financial_invoices").select("tenant_id").eq("id", invoiceId).maybeSingle();
30
+ if (error) throw error;
31
+ const tenant = data?.tenant_id ?? getActiveTenantId();
32
+ if (!tenant) throw new Error("N\xE3o foi poss\xEDvel identificar a conta desta fatura");
33
+ return tenant;
34
+ }
35
+ function snakeToCamel(obj) {
36
+ const result = {};
37
+ for (const [key, value] of Object.entries(obj)) {
38
+ result[key.replace(/_([a-z])/g, (_, c) => c.toUpperCase())] = value;
39
+ }
40
+ return result;
41
+ }
42
+ function camelToSnake(obj) {
43
+ const result = {};
44
+ for (const [key, value] of Object.entries(obj)) {
45
+ if (key.startsWith("_")) continue;
46
+ result[key.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`)] = value;
47
+ }
48
+ return result;
49
+ }
50
+ function getClients() {
51
+ const supabase = getSupabaseClientOptional();
52
+ if (!supabase) throw new Error("Supabase not initialized");
53
+ return { core: supabase, pub: supabase };
54
+ }
55
+ async function anexaContraparte(rows) {
56
+ const ids = [...new Set(rows.map((r) => r.matchedMovementId).filter(Boolean))];
57
+ if (ids.length === 0) return;
58
+ const { core, pub } = getClients();
59
+ const { data: movs } = await pub.from(T.movements).select("id, invoice_id, financial_invoice_id, notes").in("id", ids);
60
+ const porMovimento = /* @__PURE__ */ new Map();
61
+ const faturaPorMovimento = /* @__PURE__ */ new Map();
62
+ const faturas = /* @__PURE__ */ new Map();
63
+ for (const m of movs ?? []) {
64
+ const faturaId = m.financial_invoice_id ?? m.invoice_id;
65
+ if (faturaId) {
66
+ faturaPorMovimento.set(m.id, faturaId);
67
+ const lista = faturas.get(faturaId) ?? [];
68
+ lista.push(m.id);
69
+ faturas.set(faturaId, lista);
70
+ } else if (m.notes) {
71
+ porMovimento.set(m.id, m.notes);
72
+ }
73
+ }
74
+ if (faturas.size > 0) {
75
+ const { data: orders } = await core.from("orders").select("id, metadata").in("id", [...faturas.keys()]);
76
+ for (const o of orders ?? []) {
77
+ const nome = o.metadata?.contactName;
78
+ if (!nome) continue;
79
+ for (const movId of faturas.get(o.id) ?? []) porMovimento.set(movId, nome);
80
+ }
81
+ }
82
+ for (const linha of rows) {
83
+ if (linha.matchedMovementId) {
84
+ linha.matchedCounterpartyName = porMovimento.get(linha.matchedMovementId);
85
+ linha.matchedInvoiceId = faturaPorMovimento.get(linha.matchedMovementId);
86
+ }
87
+ }
88
+ }
89
+ function toStatementLine(row) {
90
+ return {
91
+ id: row.id,
92
+ tenantId: row.tenant_id ?? void 0,
93
+ unitId: row.unit_id ?? void 0,
94
+ bankAccountId: row.bank_account_id ?? void 0,
95
+ externalId: row.external_id,
96
+ externalSource: row.external_source,
97
+ date: row.posted_on,
98
+ effectiveAt: row.effective_at ?? void 0,
99
+ direction: row.direction,
100
+ amount: Number(row.amount) || 0,
101
+ balanceAfter: row.balance_after == null ? void 0 : Number(row.balance_after),
102
+ description: row.description ?? "",
103
+ counterpartyName: row.counterparty_name ?? void 0,
104
+ counterpartyDocument: row.counterparty_document ?? void 0,
105
+ document: row.document ?? void 0,
106
+ code: row.code ?? void 0,
107
+ category: row.category ?? void 0,
108
+ transactionType: row.transaction_type ?? void 0,
109
+ rawPayload: row.raw_payload ?? void 0,
110
+ providerDeleted: row.provider_deleted ?? false,
111
+ reconciledAt: row.reconciled_at ?? void 0,
112
+ matchedMovementId: row.matched_movement_id ?? void 0,
113
+ createdAt: row.created_at ?? void 0,
114
+ updatedAt: row.updated_at ?? void 0
115
+ };
116
+ }
117
+ function mapRecurringBill(row, counterpartyName) {
118
+ const items = Array.isArray(row.items) ? row.items.map((item) => ({
119
+ itemKind: item.itemKind ?? item.item_kind ?? (item.product_id ? "product" : "other"),
120
+ referenceId: item.referenceId ?? item.reference_id ?? item.product_id ?? void 0,
121
+ description: item.description ?? "",
122
+ quantity: Number(item.quantity) || 1,
123
+ unitPrice: Number(item.unitPrice ?? item.unit_price) || 0,
124
+ discount: Number(item.discount) || 0,
125
+ surcharge: Number(item.surcharge ?? item.addition) || 0,
126
+ accountId: item.accountId ?? item.account_id ?? item.chart_account_id ?? void 0,
127
+ costCenterId: item.costCenterId ?? item.cost_center_id ?? void 0
128
+ })) : [];
129
+ return {
130
+ id: row.id,
131
+ direction: row.direction,
132
+ description: row.description,
133
+ counterpartyPersonId: row.counterparty_person_id,
134
+ counterpartyName,
135
+ amount: Number(row.amount) || 0,
136
+ items,
137
+ unitId: row.unit_id,
138
+ chartAccountId: row.chart_account_id,
139
+ costCenterId: row.cost_center_id,
140
+ frequency: row.frequency,
141
+ dueDay: Number(row.due_day) || 1,
142
+ startsOn: row.starts_on,
143
+ endsOn: row.ends_on,
144
+ isActive: Boolean(row.is_active),
145
+ notes: row.notes,
146
+ createdAt: row.created_at,
147
+ updatedAt: row.updated_at
148
+ };
149
+ }
150
+ async function findInvoiceIdsByLineAccounting(core, pub, filters) {
151
+ if (!filters.accountId && !filters.costCenterId) return void 0;
152
+ let sideQ = pub.from(T.invoiceItems).select("item_id");
153
+ if (filters.accountId) sideQ = sideQ.eq("chart_account_id", filters.accountId);
154
+ if (filters.costCenterId) sideQ = sideQ.eq("cost_center_id", filters.costCenterId);
155
+ const { data: sideRows } = await sideQ;
156
+ const itemIds = (sideRows ?? []).map((r) => r.item_id).filter(Boolean);
157
+ let legacyQ = core.from("items").select("id");
158
+ if (filters.accountId) legacyQ = legacyQ.filter("metadata->>accountId", "eq", filters.accountId);
159
+ if (filters.costCenterId) legacyQ = legacyQ.filter("metadata->>costCenterId", "eq", filters.costCenterId);
160
+ const { data: legacyRows } = await legacyQ;
161
+ for (const r of legacyRows ?? []) if (r.id) itemIds.push(r.id);
162
+ if (itemIds.length === 0) return [];
163
+ const { data: items } = await core.from("items").select("invoice_id").in("id", Array.from(new Set(itemIds)));
164
+ const ids = (items ?? []).map((r) => r.invoice_id).filter((v) => typeof v === "string" && v);
165
+ return Array.from(new Set(ids));
166
+ }
167
+ async function writeLineAccounting(pub, tenantId, insertedItems, inputItems) {
168
+ if (!tenantId || !insertedItems?.length) return;
169
+ const rows = insertedItems.map((row) => {
170
+ const src = inputItems[row.sort_order ?? -1];
171
+ if (!src || !src.accountId && !src.costCenterId) return null;
172
+ return {
173
+ item_id: row.id,
174
+ parent_kind: "invoice",
175
+ tenant_id: tenantId,
176
+ chart_account_id: src.accountId ?? null,
177
+ cost_center_id: src.costCenterId ?? null
178
+ };
179
+ }).filter(Boolean);
180
+ if (rows.length === 0) return;
181
+ const { error } = await pub.from(T.invoiceItems).upsert(rows, { onConflict: "item_id" });
182
+ if (error) console.warn("[financial] line accounting mirror skipped:", error.message);
183
+ }
184
+ function shiftIsoDate(value, days) {
185
+ const d = /* @__PURE__ */ new Date(`${value}T00:00:00Z`);
186
+ d.setUTCDate(d.getUTCDate() + days);
187
+ return d.toISOString().slice(0, 10);
188
+ }
189
+ function matchScore(line, m) {
190
+ const amount = Number(line.amount) || 0;
191
+ const amtDiff = Math.abs((Number(m.amount) || 0) - amount);
192
+ const amtScore = amount > 0 ? Math.max(0, 1 - amtDiff / amount) : 0;
193
+ const refDate = m.dueDate ?? m.paymentDate;
194
+ const days = line.date && refDate ? Math.abs((new Date(line.date).getTime() - new Date(refDate).getTime()) / 864e5) : 30;
195
+ return round2(0.7 * amtScore + 0.3 * Math.max(0, 1 - days / 30));
196
+ }
197
+ function billRowToMovement(r) {
198
+ return {
199
+ id: r.installment_id,
200
+ invoiceId: r.invoice_id,
201
+ direction: fromLedgerDirection(r.direction),
202
+ movementKind: "bill",
203
+ amount: Number(r.amount) || 0,
204
+ paidAmount: Number(r.paid) || 0,
205
+ status: r.status,
206
+ dueDate: r.due_date,
207
+ installmentNumber: r.number,
208
+ unitId: r.unit_id ?? void 0,
209
+ metadata: r.metadata ?? void 0,
210
+ tenantId: r.tenant_id,
211
+ createdAt: r.created_at,
212
+ updatedAt: r.updated_at
213
+ };
214
+ }
215
+ async function attachCurrency(pub, invoices) {
216
+ const ids = [...new Set(invoices.map((inv) => inv.invoiceId ?? inv.id).filter(Boolean))];
217
+ if (ids.length === 0) return;
218
+ try {
219
+ const { data, error } = await pub.from("plg_financial_invoices").select("id, currency").in("id", ids);
220
+ if (error || !Array.isArray(data)) return;
221
+ const byId = new Map(data.map((r) => [r.id, r.currency]));
222
+ for (const inv of invoices) inv.currency = byId.get(inv.invoiceId ?? inv.id) ?? inv.currency;
223
+ } catch {
224
+ }
225
+ }
226
+ async function attachTags(pub, invoice) {
227
+ try {
228
+ const { data, error } = await pub.from("plg_financial_invoices").select("tags").eq("id", invoice.id).maybeSingle();
229
+ if (!error && Array.isArray(data?.tags)) invoice.tags = data.tags;
230
+ } catch {
231
+ }
232
+ }
233
+ function rowToInvoiceLine(r) {
234
+ return {
235
+ id: r.installment_id,
236
+ invoiceId: r.invoice_id,
237
+ direction: fromLedgerDirection(r.direction),
238
+ invoiceDate: (r.competence_date ?? r.issue_date ?? "").slice(0, 10),
239
+ dueDate: (r.due_date ?? "").slice(0, 10) || void 0,
240
+ fiscalNumber: r.number ?? void 0,
241
+ totalAmount: Number(r.amount) || 0,
242
+ paidAmount: Number(r.paid) || 0,
243
+ status: r.status === "pending" ? "open" : r.status,
244
+ installmentNumber: Number(r.installment_number) || 1,
245
+ totalInstallments: Number(r.installment_count) || 1,
246
+ contactId: r.counterparty_person_id ?? void 0,
247
+ contactName: r.counterparty_name ?? r.metadata?.contactName,
248
+ observations: r.notes ?? void 0,
249
+ itemsSummary: r.description ?? r.metadata?.itemsSummary,
250
+ unitId: r.unit_id ?? void 0,
251
+ metadata: r.metadata ?? void 0,
252
+ tenantId: r.tenant_id,
253
+ createdAt: r.created_at,
254
+ updatedAt: r.updated_at,
255
+ createdBy: r.created_by ?? void 0
256
+ };
257
+ }
258
+ function rowToInvoice(r) {
259
+ return {
260
+ id: r.id,
261
+ // O tipo do plugin fala débito/crédito; o banco fala pagar/receber.
262
+ direction: fromLedgerDirection(r.direction),
263
+ invoiceDate: (r.competence_date ?? r.issue_date ?? "").slice(0, 10),
264
+ dueDate: (r.due_date ?? "").slice(0, 10) || void 0,
265
+ fiscalNumber: r.number ?? void 0,
266
+ totalAmount: Number(r.total) || 0,
267
+ paidAmount: Number(r.paid) || 0,
268
+ status: r.status,
269
+ totalInstallments: Number(r.installment_count) || 1,
270
+ contactId: r.counterparty_person_id ?? void 0,
271
+ contactName: r.counterparty_name ?? r.metadata?.contactName,
272
+ observations: r.notes ?? void 0,
273
+ itemsSummary: r.description ?? r.metadata?.itemsSummary,
274
+ unitId: r.unit_id ?? void 0,
275
+ metadata: r.metadata ?? void 0,
276
+ tenantId: r.tenant_id,
277
+ createdAt: r.created_at,
278
+ updatedAt: r.updated_at,
279
+ createdBy: r.created_by ?? void 0
280
+ };
281
+ }
282
+ async function getMethodFeePct(pub, paymentMethodId) {
283
+ if (!paymentMethodId) return 0;
284
+ const { data, error } = await pub.from(T.paymentMethods).select("discount_value").eq("id", paymentMethodId).single();
285
+ if (error) throw error;
286
+ return Number(data?.discount_value) || 0;
287
+ }
288
+ function genId() {
289
+ try {
290
+ if (typeof crypto !== "undefined" && crypto.randomUUID) return crypto.randomUUID();
291
+ } catch {
292
+ }
293
+ return "tr-" + Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 10);
294
+ }
295
+ function createSupabaseFinancialProvider() {
296
+ async function readStatementLine(id) {
297
+ const { pub } = getClients();
298
+ const { data } = await pub.from(T.statementLines).select("*").eq("id", id).single();
299
+ if (!data) throw new Error("Linha do extrato n\xE3o encontrada");
300
+ return {
301
+ id: data.id,
302
+ amount: Number(data.amount) || 0,
303
+ // `posted_on` é a coluna; `date` é o nome que o domínio usa.
304
+ date: data.posted_on,
305
+ direction: data.direction,
306
+ description: data.description ?? void 0,
307
+ bankAccountId: data.bank_account_id ?? void 0,
308
+ externalSource: data.external_source
309
+ };
310
+ }
311
+ async function searchBillTargets(line, query) {
312
+ const { pub } = getClients();
313
+ const pageSize = query.pageSize ?? 50;
314
+ const page = query.page ?? 1;
315
+ let qb = pub.from("v_financial_invoice_lines_due").select("installment_id,invoice_id,direction,number,due_date,description,counterparty_name,installment_number,installment_count,amount,paid,status").eq("direction", line.direction === "debit" ? "payable" : "receivable");
316
+ const status = query.status ?? "all";
317
+ qb = status === "open" ? qb.in("status", ["pending", "partial", "overdue"]) : status === "paid" ? qb.eq("status", "paid") : qb.neq("status", "cancelled");
318
+ if (query.dueFrom) qb = qb.gte("due_date", query.dueFrom);
319
+ if (query.dueTo) qb = qb.lte("due_date", query.dueTo);
320
+ if (query.amountMin != null) qb = qb.gte("amount", query.amountMin);
321
+ if (query.amountMax != null) qb = qb.lte("amount", query.amountMax);
322
+ const termo = query.search?.trim().replace(/[,()*%]/g, " ").replace(/\s+/g, " ");
323
+ if (termo) {
324
+ qb = qb.or(["description", "number", "counterparty_name"].map((c) => `${c}.ilike.%${termo}%`).join(","));
325
+ }
326
+ const { data, error } = await qb.order("due_date", { ascending: true }).range((page - 1) * pageSize, page * pageSize);
327
+ if (error) throw error;
328
+ const rows = data ?? [];
329
+ const hasMore = rows.length > pageSize;
330
+ const visiveis = rows.slice(0, pageSize);
331
+ const pagas = visiveis.filter((r) => r.status === "paid").map((r) => r.installment_id);
332
+ const pagamentoDe = /* @__PURE__ */ new Map();
333
+ if (pagas.length) {
334
+ const { data: allocs } = await pub.from(T.allocations).select("installment_id,payment_movement_id").in("installment_id", pagas);
335
+ const ids = [...new Set((allocs ?? []).map((a) => a.payment_movement_id).filter(Boolean))];
336
+ const { data: pays } = ids.length ? await pub.from(T.movements).select("*").in("id", ids).neq("status", "cancelled").is("reversed_by_id", null) : { data: [] };
337
+ const byId = new Map((pays ?? []).map((p) => [p.id, p]));
338
+ for (const a of allocs ?? []) {
339
+ const p = byId.get(a.payment_movement_id);
340
+ if (p) pagamentoDe.set(a.installment_id, p);
341
+ }
342
+ }
343
+ const candidates = [];
344
+ for (const r of visiveis) {
345
+ const invoice = {
346
+ id: r.invoice_id,
347
+ contactName: r.counterparty_name ?? void 0,
348
+ fiscalNumber: r.number ?? void 0
349
+ };
350
+ let movement;
351
+ if (r.status === "paid") {
352
+ const p = pagamentoDe.get(r.installment_id);
353
+ if (!p || p.reconciled_at) continue;
354
+ movement = {
355
+ ...snakeToCamel(p),
356
+ invoiceId: r.invoice_id,
357
+ settlesMovementId: r.installment_id,
358
+ dueDate: r.due_date,
359
+ notes: p.notes ?? r.description ?? void 0
360
+ };
361
+ } else {
362
+ movement = {
363
+ id: r.installment_id,
364
+ invoiceId: r.invoice_id,
365
+ direction: line.direction,
366
+ movementKind: "bill",
367
+ amount: Number(r.amount) || 0,
368
+ paidAmount: Number(r.paid) || 0,
369
+ status: r.status,
370
+ dueDate: r.due_date,
371
+ installmentNumber: r.installment_number ?? void 0,
372
+ notes: r.description ?? void 0
373
+ };
374
+ }
375
+ const pareado = scorePair(
376
+ { amount: line.amount, date: line.date, direction: line.direction, description: line.description ?? "" },
377
+ movement,
378
+ { contactName: r.counterparty_name ?? void 0 }
379
+ );
380
+ candidates.push({
381
+ movement,
382
+ invoice,
383
+ score: pareado > 0 ? pareado : round2(0.6 * matchScore(line, movement)),
384
+ installmentCount: r.installment_count ?? void 0
385
+ });
386
+ }
387
+ candidates.sort((a, b) => b.score - a.score);
388
+ return { data: candidates, hasMore };
389
+ }
390
+ async function markReconciled(statementLineId, matchedMovementId) {
391
+ const { pub } = getClients();
392
+ const now = (/* @__PURE__ */ new Date()).toISOString();
393
+ await pub.from(T.statementLines).update({
394
+ reconciled_at: now,
395
+ matched_movement_id: matchedMovementId ?? null
396
+ }).eq("id", statementLineId);
397
+ if (matchedMovementId) {
398
+ await pub.from(T.movements).update({ reconciled_at: now }).eq("id", matchedMovementId);
399
+ }
400
+ }
401
+ function reconcileFacade() {
402
+ return {
403
+ createInvoice: (input) => provider.createInvoice(input),
404
+ getMovements: (query) => provider.getMovements(query),
405
+ payMovement: (input) => provider.payMovement(input),
406
+ reconcileMovement: ({ bankMovementId, matchedMovementId }) => markReconciled(bankMovementId, matchedMovementId),
407
+ addInstallmentCharge: (input) => provider.addInstallmentCharge(input)
408
+ };
409
+ }
410
+ const provider = {
411
+ // --- Contas (plg_financial_invoices) ---
412
+ // A conta NÃO é uma order: `orders_kind_guard` aposentou invoice_payable e
413
+ // invoice_receivable, e o motivo está no HINT do próprio erro — ADR 0004,
414
+ // "orders is the Venda; money lives in the financial plugin". Uma conta a
415
+ // pagar não tem cliente, nem item vendido, nem entrega; era uma venda com o
416
+ // sinal trocado. A leitura vem de v_financial_invoice_balances, que já traz
417
+ // saldo e situação derivados das parcelas — e, ao contrário de
418
+ // v_invoice_balances, não exige que a conta tenha nascido de uma venda.
419
+ async getInvoices(query) {
420
+ const { core, pub } = getClients();
421
+ const COLUNAS = [
422
+ "installment_id",
423
+ "invoice_id",
424
+ "tenant_id",
425
+ "unit_id",
426
+ "direction",
427
+ "number",
428
+ "competence_date",
429
+ "due_date",
430
+ "description",
431
+ "counterparty_person_id",
432
+ "counterparty_name",
433
+ "installment_number",
434
+ "installment_count",
435
+ "amount",
436
+ "paid",
437
+ "status",
438
+ "created_at",
439
+ "updated_at"
440
+ ].join(",");
441
+ let qb = pub.from("v_financial_invoice_lines_due").select(COLUNAS, { count: "exact" });
442
+ if (query.direction) qb = qb.eq("direction", toLedgerDirection(query.direction));
443
+ if (query.status) {
444
+ const statuses = Array.isArray(query.status) ? query.status : [query.status];
445
+ qb = qb.in("status", statuses.map((st) => st === "open" ? "pending" : st));
446
+ } else {
447
+ qb = qb.neq("status", "cancelled");
448
+ }
449
+ const scoped = await findInvoiceIdsByLineAccounting(core, pub, {
450
+ accountId: query.accountId,
451
+ costCenterId: query.costCenterId
452
+ });
453
+ if (scoped) {
454
+ if (scoped.length === 0) return { data: [], total: 0 };
455
+ qb = qb.in("invoice_id", scoped);
456
+ }
457
+ if (query.contactId) qb = qb.eq("counterparty_person_id", query.contactId);
458
+ if (query.fiscalNumber) qb = qb.ilike("number", `%${query.fiscalNumber}%`);
459
+ if (query.bankAccountId) qb = qb.contains("payment_account_ids", [query.bankAccountId]);
460
+ const unidades = query.unitId ? ["unit_id.is.null", `unit_id.eq.${query.unitId}`] : [];
461
+ const buscas = query.search ? ["description", "notes", "number", "counterparty_name"].map((col) => `${col}.ilike.%${query.search}%`) : [];
462
+ if (unidades.length && buscas.length) {
463
+ qb = qb.or(unidades.flatMap((u) => buscas.map((b) => `and(${u},${b})`)).join(","));
464
+ } else if (unidades.length) {
465
+ qb = qb.or(unidades.join(","));
466
+ } else if (buscas.length) {
467
+ qb = qb.or(buscas.join(","));
468
+ }
469
+ if (query.dateRange) {
470
+ qb = qb.gte("due_date", query.dateRange.from.slice(0, 10)).lte("due_date", query.dateRange.to.slice(0, 10));
471
+ }
472
+ const page = query.page ?? 1;
473
+ const pageSize = query.pageSize ?? 50;
474
+ qb = qb.range((page - 1) * pageSize, page * pageSize - 1).order("due_date", { ascending: true });
475
+ const { data, count, error } = await qb;
476
+ if (error) throw error;
477
+ const linhas = (data ?? []).map(rowToInvoiceLine);
478
+ const titulos = [...new Set(linhas.map((l) => l.invoiceId).filter(Boolean))];
479
+ if (titulos.length) {
480
+ const { data: itemRows } = await pub.from("v_financial_invoice_lines").select("invoice_id,name,description,quantity").in("invoice_id", titulos);
481
+ const itemSummaries = summarizeInvoiceLines(itemRows ?? []);
482
+ for (const line of linhas) {
483
+ line.itemsSummary = itemSummaries.get(line.invoiceId ?? line.id) ?? line.itemsSummary;
484
+ }
485
+ const installmentIds = linhas.map((line) => line.id);
486
+ const { data: allocations } = await pub.from(T.allocations).select("installment_id,payment_movement_id,created_at").in("installment_id", installmentIds).order("created_at", { ascending: false });
487
+ const movementIds = [...new Set((allocations ?? []).map((row) => row.payment_movement_id).filter(Boolean))];
488
+ if (movementIds.length > 0) {
489
+ const { data: paymentRows } = await pub.from(T.movements).select("id,payment_method_id,payment_method_type_id,bank_account_id,card_installments,status,reversed_by_id").in("id", movementIds);
490
+ const paymentById = new Map((paymentRows ?? []).map((row) => [row.id, row]));
491
+ const latestByInstallment = /* @__PURE__ */ new Map();
492
+ for (const allocation of allocations ?? []) {
493
+ if (latestByInstallment.has(allocation.installment_id)) continue;
494
+ const payment = paymentById.get(allocation.payment_movement_id);
495
+ if (!payment || payment.status === "cancelled" || payment.reversed_by_id) continue;
496
+ latestByInstallment.set(allocation.installment_id, payment);
497
+ }
498
+ for (const line of linhas) {
499
+ const payment = latestByInstallment.get(line.id);
500
+ if (!payment) continue;
501
+ line.paymentMethodId = payment.payment_method_id ?? void 0;
502
+ line.paymentMethodTypeId = payment.payment_method_type_id ?? void 0;
503
+ line.paymentAccountId = payment.bank_account_id ?? void 0;
504
+ line.paymentCardInstallments = Number(payment.card_installments) || void 0;
505
+ }
506
+ }
507
+ const { data: docs } = await pub.from("documents").select("subject_id").eq("subject_type", "financial_invoice").eq("is_active", true).in("subject_id", titulos);
508
+ const porTitulo = /* @__PURE__ */ new Map();
509
+ for (const d of docs ?? []) {
510
+ porTitulo.set(d.subject_id, (porTitulo.get(d.subject_id) ?? 0) + 1);
511
+ }
512
+ for (const l of linhas) {
513
+ l.attachmentCount = porTitulo.get(l.invoiceId ?? l.id) ?? 0;
514
+ }
515
+ }
516
+ await attachCurrency(pub, linhas);
517
+ return { data: linhas, total: count ?? 0 };
518
+ },
519
+ async getInvoiceById(id) {
520
+ const { pub } = getClients();
521
+ const { data, error } = await pub.from("v_financial_invoice_balances").select("*").eq("id", id).maybeSingle();
522
+ if (error) throw error;
523
+ if (!data) return null;
524
+ const invoice = rowToInvoice(data);
525
+ await Promise.all([attachCurrency(pub, [invoice]), attachTags(pub, invoice)]);
526
+ return invoice;
527
+ },
528
+ async getInvoiceItems(invoiceId) {
529
+ const { pub } = getClients();
530
+ const { data, error } = await pub.from("v_financial_invoice_lines").select("*").eq("invoice_id", invoiceId);
531
+ if (error) throw error;
532
+ const rows = data ?? [];
533
+ const metaOf = (r) => r.item_metadata ?? r.metadata ?? {};
534
+ const productOf = (r) => r.product_id ?? (metaOf(r).itemKind === "product" ? metaOf(r).referenceId : void 0);
535
+ const productIds = [...new Set(rows.map(productOf).filter(Boolean))];
536
+ const pictures = /* @__PURE__ */ new Map();
537
+ if (productIds.length > 0) {
538
+ try {
539
+ const found = await pub.from("products").select("id, image_url").in("id", productIds);
540
+ for (const p of Array.isArray(found.data) ? found.data : []) {
541
+ if (p.image_url) pictures.set(p.id, p.image_url);
542
+ }
543
+ } catch {
544
+ }
545
+ }
546
+ return rows.map((r) => {
547
+ const i = snakeToCamel(r);
548
+ const meta = metaOf(r);
549
+ const pictured = productOf(r);
550
+ return {
551
+ imageUrl: pictured ? pictures.get(pictured) : void 0,
552
+ // A line that names a product IS a product line, whatever it was
553
+ // typed as: linking it later (stock entry) must change its icon too.
554
+ id: i.itemId,
555
+ invoiceId,
556
+ itemKind: meta.itemKind && meta.itemKind !== "other" ? meta.itemKind : i.productId ? "product" : "other",
557
+ // `name` OU `description`: quem levanta a conta pelo SQL
558
+ // (`financial_create_payable`, o borderô de comissões) escreve o
559
+ // texto em `description` e deixa `name` nulo. Lendo só `name`, a
560
+ // linha chegava vazia na tela e sobrava o `itemKind` cru no lugar
561
+ // dela — era o "other" que aparecia na fatura de comissão.
562
+ description: i.name ?? i.description ?? "",
563
+ quantity: i.quantity ?? 1,
564
+ unitPrice: i.unitPrice ?? 0,
565
+ totalAmount: i.total ?? 0,
566
+ discount: i.discount ?? 0,
567
+ surcharge: 0,
568
+ // `product_id` quando a linha veio de uma porta que o preenche (a
569
+ // RPC da agenda); do metadata quando veio DESTA tela, que é onde a
570
+ // escolha de catálogo é gravada. Lendo só a coluna, reabrir uma
571
+ // fatura apagava o item escolhido de toda linha e deixava o texto
572
+ // solto — e salvar de novo confirmava a perda.
573
+ referenceId: i.productId ?? meta.referenceId,
574
+ accountId: i.chartAccountId,
575
+ costCenterId: i.costCenterId,
576
+ // De onde a linha veio — o pacote que a gerou, por exemplo. Sem
577
+ // devolvê-lo, editar a fatura desfazia o vínculo em silêncio.
578
+ metadata: r.item_metadata ?? r.metadata ?? void 0,
579
+ cancelledAt: i.itemCancelledAt ?? void 0,
580
+ cancelReason: i.itemCancelReason ?? void 0,
581
+ createdAt: i.createdAt
582
+ };
583
+ });
584
+ },
585
+ async getInvoiceAttachments(invoiceId) {
586
+ const { pub } = getClients();
587
+ const { data, error } = await pub.from("documents").select("id, file_name, file_size, mime_type, file_url, storage_bucket, storage_path, storage_provider, created_at").eq("subject_type", "financial_invoice").eq("subject_id", invoiceId).eq("is_active", true).order("created_at");
588
+ if (error) throw error;
589
+ const linhas = data ?? [];
590
+ const assinadas = /* @__PURE__ */ new Map();
591
+ const porBucket = /* @__PURE__ */ new Map();
592
+ for (const r of linhas) {
593
+ if (!r.storage_bucket || !r.storage_path) continue;
594
+ const lista = porBucket.get(r.storage_bucket) ?? [];
595
+ lista.push(r.storage_path);
596
+ porBucket.set(r.storage_bucket, lista);
597
+ }
598
+ for (const [bucket, caminhos] of porBucket) {
599
+ try {
600
+ const { data: urls } = await pub.storage.from(bucket).createSignedUrls(caminhos, 60 * 30);
601
+ for (const u of urls ?? []) {
602
+ if (u.path && u.signedUrl) assinadas.set(u.path, u.signedUrl);
603
+ }
604
+ } catch {
605
+ }
606
+ }
607
+ return linhas.map((r) => {
608
+ const url = assinadas.get(r.storage_path) ?? void 0;
609
+ return {
610
+ id: r.id,
611
+ fileName: r.file_name ?? "Anexo",
612
+ fileSize: r.file_size ?? void 0,
613
+ mimeType: r.mime_type ?? void 0,
614
+ url,
615
+ // Pendente é o que NÃO ABRE: os bytes ainda no V1, ou a assinatura
616
+ // que não veio. O `file_url` gravado não conta — ele responde 400.
617
+ pending: !url,
618
+ createdAt: r.created_at
619
+ };
620
+ });
621
+ },
622
+ async addInvoiceAttachment(invoiceId, file) {
623
+ const { pub } = getClients();
624
+ const tenantId = getActiveTenantId();
625
+ if (!tenantId) throw new Error("Conta sem empresa ativa");
626
+ const safe = file.name.replace(/[^\w.\-]+/g, "_");
627
+ const path = `${tenantId}/financial_invoice/${invoiceId}/${Date.now()}_${safe}`;
628
+ const { error: upErr } = await pub.storage.from("attachments").upload(path, file, { contentType: file.type || void 0 });
629
+ if (upErr) throw upErr;
630
+ const { error } = await pub.from("documents").insert({
631
+ tenant_id: tenantId,
632
+ kind: "attachment",
633
+ status: "active",
634
+ subject_type: "financial_invoice",
635
+ subject_id: invoiceId,
636
+ file_name: file.name,
637
+ file_size: file.size,
638
+ mime_type: file.type || null,
639
+ storage_provider: "supabase",
640
+ storage_bucket: "attachments",
641
+ storage_path: path
642
+ });
643
+ if (error) {
644
+ await pub.storage.from("attachments").remove([path]).catch(() => {
645
+ });
646
+ throw error;
647
+ }
648
+ },
649
+ async createInvoice(input) {
650
+ const { pub } = getClients();
651
+ getActiveTenantId();
652
+ const totalAmount = invoiceTotal(input.items);
653
+ const itemsSummary = itemsDescription(input.items);
654
+ const installments = (input.installments.length > 0 ? input.installments : [
655
+ { amount: totalAmount, dueDate: input.invoiceDate ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10) }
656
+ ]).map((inst, n) => ({
657
+ number: inst.installmentNumber ?? n + 1,
658
+ due_date: inst.dueDate,
659
+ amount: round2(inst.amount),
660
+ ...inst.metadata ? { metadata: inst.metadata } : {}
661
+ }));
662
+ const { data: res, error } = await pub.rpc("financial_create_invoice", {
663
+ p_direction: toLedgerDirection(input.direction),
664
+ p_total: totalAmount,
665
+ p_counterparty_person_id: input.contactId ?? null,
666
+ p_order_id: null,
667
+ p_unit_id: input.unitId ?? null,
668
+ p_installments: installments,
669
+ p_competence_date: input.invoiceDate ?? null,
670
+ p_description: itemsSummary || null,
671
+ p_notes: input.observations ?? null,
672
+ p_metadata: {
673
+ contactName: input.contactName,
674
+ ...input.fiscalNumber ? { fiscalNumber: input.fiscalNumber } : {},
675
+ ...input.currency ? { currency: input.currency } : {}
676
+ },
677
+ p_idempotency_key: genId()
678
+ });
679
+ if (error) throw error;
680
+ const invoiceId = res.invoice_id;
681
+ if (input.tags?.length) {
682
+ await pub.from("plg_financial_invoices").update({ tags: input.tags }).eq("id", invoiceId);
683
+ }
684
+ if (input.items.length > 0) {
685
+ const { data: lines, error: linesErr } = await pub.rpc("financial_set_invoice_lines", {
686
+ p_invoice_id: invoiceId,
687
+ p_lines: invoiceLinesPayload(input.items, lineTotal)
688
+ });
689
+ if (linesErr) {
690
+ await pub.rpc("financial_cancel_invoice", { p_invoice_id: invoiceId, p_reason: "Itens recusados ao criar a conta" });
691
+ throw new Error(`Os itens da conta n\xE3o foram gravados: ${linesErr.message}`);
692
+ }
693
+ await writeLineAccounting(pub, await invoiceTenantId(pub, invoiceId), lines, input.items);
694
+ }
695
+ return await provider.getInvoiceById(invoiceId);
696
+ },
697
+ // A EDIÇÃO SALVAVA UMA COISA SÓ.
698
+ //
699
+ // Este método recebia o cabeçalho inteiro — data, contato, unidade, número
700
+ // fiscal, total — e gravava `notes`. Todo o resto caía no chão sem erro
701
+ // nenhum: o formulário fechava, a tela recarregava, e o campo voltava como
702
+ // estava. Um salvar que não salva e não reclama é pior que um que falha.
703
+ //
704
+ // O mapa é o mesmo que a LEITURA usa (ver `rowToInvoice`), e por isso
705
+ // `competence_date` e não `issue_date`: a leitura prefere a competência, e
706
+ // gravar na outra coluna faria a tela mostrar o valor antigo depois de
707
+ // salvar o novo.
708
+ async updateInvoice(id, partial) {
709
+ const { pub } = getClients();
710
+ const row = {};
711
+ if (partial.observations !== void 0) row.notes = partial.observations;
712
+ if (partial.tags !== void 0) row.tags = partial.tags;
713
+ if (partial.invoiceDate !== void 0) row.competence_date = partial.invoiceDate;
714
+ if (partial.contactId !== void 0) row.counterparty_person_id = partial.contactId ?? null;
715
+ if (partial.unitId !== void 0) row.unit_id = partial.unitId ?? null;
716
+ if (partial.totalAmount !== void 0) row.total = partial.totalAmount;
717
+ if (partial.contactName !== void 0 || "fiscalNumber" in partial) {
718
+ const { data: cur } = await pub.from("plg_financial_invoices").select("metadata").eq("id", id).maybeSingle();
719
+ row.metadata = {
720
+ ...cur?.metadata ?? {},
721
+ ...partial.contactName !== void 0 ? { contactName: partial.contactName } : {},
722
+ ..."fiscalNumber" in partial ? { fiscalNumber: partial.fiscalNumber ?? null } : {}
723
+ };
724
+ }
725
+ if (Object.keys(row).length > 0) {
726
+ const { error } = await pub.from("plg_financial_invoices").update(row).eq("id", id);
727
+ if (error) throw error;
728
+ }
729
+ return await provider.getInvoiceById(id);
730
+ },
731
+ // Escreve na TABELA, não na leitura.
732
+ //
733
+ // `T.bills` é `v_financial_installment_balances` — a view que soma o pago e
734
+ // devolve a parcela já com saldo. Ela não tem `id` (a chave dela se chama
735
+ // `installment_id`) e não é atualizável, então um UPDATE ali volta 400 com
736
+ // "column ... does not exist". A parcela mora em
737
+ // `plg_financial_invoice_installments`, e é lá que a data muda.
738
+ async updateInstallment(id, data) {
739
+ const { pub } = getClients();
740
+ const row = {};
741
+ if (data.dueDate !== void 0) row.due_date = data.dueDate;
742
+ if (data.amount !== void 0) row.amount = data.amount;
743
+ if (data.metadata !== void 0) row.metadata = data.metadata;
744
+ if (Object.keys(row).length === 0) return;
745
+ const { error } = await pub.from("plg_financial_invoice_installments").update(row).eq("id", id);
746
+ if (error) throw error;
747
+ },
748
+ // SUBSTITUI as linhas, e o total vai junto.
749
+ //
750
+ // Casar linha a linha pediria um id estável que o formulário não carrega —
751
+ // ele trabalha com `_id` de rascunho —, e uma fatura tem unidades de
752
+ // linhas, não milhares. Apagar e reinserir é o que o cadastro já faz, pelo
753
+ // mesmo caminho e com a mesma contabilidade de linha.
754
+ //
755
+ // O TOTAL ENTRA NA MESMA OPERAÇÃO. Ele é a soma das linhas: gravar as
756
+ // linhas sem ele deixaria a conta somando um valor na tela e cobrando
757
+ // outro no cronograma — e o `financial_reschedule_invoice`, que compara a
758
+ // soma das parcelas com o total GRAVADO, recusaria o cronograma novo sem
759
+ // dizer por quê. 099 abriu os dois enquanto nada foi recebido.
760
+ //
761
+ // O espelho contábil (`plg_financial_invoice_items`) some sozinho: a chave
762
+ // dele é `item_id ON DELETE CASCADE`.
763
+ async replaceInvoiceItems(invoiceId, items) {
764
+ const { pub } = getClients();
765
+ const tenantId = await invoiceTenantId(pub, invoiceId);
766
+ const totalAmount = invoiceTotal(items);
767
+ const { error: totalErr } = await pub.from("plg_financial_invoices").update({ total: totalAmount, description: itemsDescription(items) || null }).eq("id", invoiceId);
768
+ if (totalErr) throw totalErr;
769
+ const { data: inserted, error: insErr } = await pub.rpc("financial_set_invoice_lines", {
770
+ p_invoice_id: invoiceId,
771
+ p_lines: invoiceLinesPayload(items, lineTotal),
772
+ p_replace: true
773
+ });
774
+ if (insErr) throw insErr;
775
+ await writeLineAccounting(pub, tenantId, inserted, items);
776
+ },
777
+ // UMA chamada, não um laço de updates.
778
+ //
779
+ // Ir de 3x para 2x apaga uma linha e reescreve as outras duas, e
780
+ // (invoice_id, number) é UNIQUE e não deferrable: qualquer ordem de
781
+ // updates do cliente passa por um estado que o banco recusa. A RPC faz o
782
+ // apaga-e-reinsere numa transação, confere se a soma bate com o total do
783
+ // título e recusa o que já recebeu alguma coisa.
784
+ async rescheduleInvoice(invoiceId, installments) {
785
+ const { pub } = getClients();
786
+ const { error } = await pub.rpc("financial_reschedule_invoice", {
787
+ p_invoice_id: invoiceId,
788
+ p_installments: installments.map((inst, i) => ({
789
+ number: i + 1,
790
+ due_date: inst.dueDate,
791
+ amount: inst.amount,
792
+ // 111 faz `metadata = coalesce(v_meta, metadata)`: o que vai aqui
793
+ // SUBSTITUI a metadata da parcela, então o chamador manda o conjunto
794
+ // inteiro, não o delta.
795
+ ...inst.metadata ? { metadata: inst.metadata } : {}
796
+ })),
797
+ p_idempotency_key: createIdempotencyKey("financial:invoice-reschedule")
798
+ });
799
+ if (error) throw error;
800
+ },
801
+ async cancelInvoice(input) {
802
+ const { pub } = getClients();
803
+ const reason = cancellationReason(input);
804
+ const { error } = await pub.rpc("financial_cancel_invoice", {
805
+ p_invoice_id: input.invoiceId,
806
+ p_reason: reason,
807
+ p_idempotency_key: input.idempotencyKey ?? createIdempotencyKey("financial:invoice-cancel")
808
+ });
809
+ if (error) throw error;
810
+ },
811
+ async listCancellationReasons() {
812
+ const { pub } = getClients();
813
+ const { data, error } = await pub.rpc("financial_cancellation_reasons");
814
+ if (error) throw error;
815
+ return (data ?? []).map((r) => ({ id: r.id, name: r.name }));
816
+ },
817
+ async getInvoiceStockEntries(invoiceId) {
818
+ const { pub } = getClients();
819
+ const toEntry = (r) => ({
820
+ itemId: r.item_id,
821
+ productId: r.product_id,
822
+ locationId: r.location_id,
823
+ entered: Number(r.entered ?? 0),
824
+ available: Number(r.available ?? 0),
825
+ enteredOn: r.entered_on
826
+ });
827
+ const { data, error } = await pub.rpc("financial_invoice_stock_entries", { p_invoice_id: invoiceId });
828
+ if (error) throw error;
829
+ const guessed = (data ?? []).map(toEntry);
830
+ const linked = await pub.rpc("financial_invoice_stock_links", { p_invoice_id: invoiceId });
831
+ const direct = linked.error || !Array.isArray(linked.data) ? [] : linked.data.map(toEntry);
832
+ const seen = new Set(direct.map((e) => e.itemId));
833
+ return [...direct, ...guessed.filter((e) => !seen.has(e.itemId))];
834
+ },
835
+ async listStockLocations() {
836
+ const { pub } = getClients();
837
+ const { data, error } = await pub.from("plg_inventory_stock_locations").select("id, name").eq("is_active", true).order("name");
838
+ if (error) return [];
839
+ return (data ?? []).map((r) => ({ id: r.id, name: r.name }));
840
+ },
841
+ async listCurrencies() {
842
+ const { pub } = getClients();
843
+ const { data, error } = await pub.from("plg_financial_accounts").select("currency").eq("tenant_id", getActiveTenantId()).eq("is_active", true).not("kind", "in", "(system,person)");
844
+ if (error) return [];
845
+ return [...new Set((Array.isArray(data) ? data : []).map((r) => String(r.currency ?? "").toUpperCase()).filter(Boolean))];
846
+ },
847
+ async getProductPictures(ids) {
848
+ if (ids.length === 0) return {};
849
+ const { pub } = getClients();
850
+ const { data, error } = await pub.from("products").select("id, image_url").in("id", ids);
851
+ if (error) return {};
852
+ const out = {};
853
+ for (const p of Array.isArray(data) ? data : []) if (p.image_url) out[p.id] = p.image_url;
854
+ return out;
855
+ },
856
+ async searchStockProducts(query) {
857
+ const { pub } = getClients();
858
+ let qb = pub.from("products").select("id, name").eq("kind", "good").eq("is_active", true).order("name").limit(20);
859
+ const q = query.trim();
860
+ if (q) qb = qb.ilike("name", `%${q.replace(/[%_]/g, " ")}%`);
861
+ const { data, error } = await qb;
862
+ if (error) return [];
863
+ return (data ?? []).map((r) => ({ id: r.id, name: r.name }));
864
+ },
865
+ async getFxRate(from, to) {
866
+ const { pub } = getClients();
867
+ if (from === to) return 1;
868
+ const read = async () => {
869
+ const { data, error } = await pub.from("plg_financial_fx_rates").select("base_currency, quote_currency, rate, rate_date").or(`and(base_currency.eq.${from},quote_currency.eq.${to}),and(base_currency.eq.${to},quote_currency.eq.${from})`).order("rate_date", { ascending: false }).limit(1);
870
+ const row = !error && Array.isArray(data) ? data[0] : null;
871
+ if (!row || !(Number(row.rate) > 0)) return null;
872
+ return row.base_currency === from ? Number(row.rate) : 1 / Number(row.rate);
873
+ };
874
+ const known = await read();
875
+ if (known !== null) return known;
876
+ const tenantId = getActiveTenantId();
877
+ if (tenantId && await ensureReferenceRates(pub, tenantId, to, [from])) return read();
878
+ return null;
879
+ },
880
+ async listProductVariants(productId) {
881
+ const { pub } = getClients();
882
+ const { data, error } = await pub.from("product_variants").select("id, name, is_default").eq("product_id", productId).eq("is_active", true).order("sort_order").order("created_at");
883
+ if (error) return [];
884
+ return (data ?? []).map((r) => ({ id: r.id, name: r.name, isDefault: !!r.is_default }));
885
+ },
886
+ async getInvoiceStockDetails(invoiceId) {
887
+ const { pub } = getClients();
888
+ const { data, error } = await pub.rpc("financial_invoice_stock_link_details", { p_invoice_id: invoiceId });
889
+ if (error || !Array.isArray(data)) return [];
890
+ return data.map((r) => ({
891
+ itemId: r.item_id,
892
+ variantId: r.variant_id ?? void 0,
893
+ variantName: r.variant_name ?? void 0,
894
+ quantity: Number(r.quantity) || 0,
895
+ unitCost: Number(r.unit_cost) || 0,
896
+ currency: String(r.currency ?? ""),
897
+ sourceUnitCost: r.source_unit_cost == null ? void 0 : Number(r.source_unit_cost),
898
+ sourceCurrency: r.source_currency ?? void 0,
899
+ fxRate: r.fx_rate == null ? void 0 : Number(r.fx_rate),
900
+ locationName: r.location_name ?? void 0,
901
+ enteredOn: r.entered_on ?? void 0
902
+ }));
903
+ },
904
+ async receiveInvoiceItemStock(input) {
905
+ const { pub } = getClients();
906
+ const call = () => pub.rpc("financial_invoice_item_receive_stock_lines", {
907
+ p_item_id: input.itemId,
908
+ p_location_id: input.locationId,
909
+ p_lines: input.entries.map((e) => ({ variant_id: e.variantId ?? null, quantity: e.quantity })),
910
+ p_product_id: input.productId ?? null,
911
+ p_unit_cost: input.unitCost ?? null,
912
+ p_fx_rate: input.fxRate ?? null,
913
+ p_idempotency_key: input.idempotencyKey ?? null
914
+ });
915
+ let { error } = await call();
916
+ const pair = /de ([A-Z]{3}) para ([A-Z]{3})/.exec(String(error?.message ?? ""));
917
+ const tenantId = getActiveTenantId();
918
+ if (pair && tenantId && await ensureReferenceRates(pub, tenantId, pair[2], [pair[1]])) {
919
+ ({ error } = await call());
920
+ }
921
+ if (error) throw error;
922
+ },
923
+ async cancelInvoiceItems(input) {
924
+ const { pub } = getClients();
925
+ const { data, error } = await pub.rpc("financial_cancel_invoice_items", {
926
+ p_invoice_id: input.invoiceId,
927
+ p_item_ids: input.itemIds.length ? input.itemIds : null,
928
+ p_reason_id: input.reasonId,
929
+ p_detail: input.detail?.trim() || null,
930
+ p_stock: input.stock.map((s) => ({ item_id: s.itemId, destination: s.destination, note: s.note?.trim() || null })),
931
+ p_idempotency_key: input.idempotencyKey ?? createIdempotencyKey("financial:invoice-cancel-items")
932
+ });
933
+ if (error) throw error;
934
+ const r = data ?? {};
935
+ return { whole: !!r.whole, amount: Number(r.amount ?? 0), creditAmount: Number(r.credit_amount ?? 0) };
936
+ },
937
+ async addInstallmentCharge(input) {
938
+ const { pub } = getClients();
939
+ const { error } = await pub.rpc("financial_add_installment_charge", {
940
+ p_installment_id: input.installmentId,
941
+ p_kind: input.kind,
942
+ p_amount: input.amount,
943
+ p_chart_account_id: input.accountId ?? null,
944
+ p_description: input.description?.trim() || null,
945
+ p_idempotency_key: input.idempotencyKey ?? createIdempotencyKey("financial:installment-charge")
946
+ });
947
+ if (error) throw error;
948
+ },
949
+ async getSupplierCredit(personId) {
950
+ const { pub } = getClients();
951
+ const { data, error } = await pub.rpc("financial_supplier_credit", { p_person_id: personId });
952
+ if (error) throw error;
953
+ return Number(data ?? 0);
954
+ },
955
+ // --- Movements (public.financial_movements) ---
956
+ async getMovements(query) {
957
+ const { pub } = getClients();
958
+ const wantsSchedule = !!query.invoiceId && !query.bankAccountId && !query.cashSessionId;
959
+ if (wantsSchedule) {
960
+ let sq = pub.from(T.bills).select("*", { count: "exact" }).eq("invoice_id", query.invoiceId);
961
+ if (query.direction) sq = sq.eq("direction", toLedgerDirection(query.direction));
962
+ if (query.status) {
963
+ const statuses = Array.isArray(query.status) ? query.status : [query.status];
964
+ sq = sq.in("status", statuses);
965
+ }
966
+ if (query.dateRange) sq = sq.gte("due_date", query.dateRange.from).lte("due_date", query.dateRange.to);
967
+ const page2 = query.page ?? 1;
968
+ const pageSize2 = query.pageSize ?? 50;
969
+ const { data: data2, count: count2, error } = await sq.range((page2 - 1) * pageSize2, page2 * pageSize2 - 1).order("due_date");
970
+ if (error) throw error;
971
+ const bills = (data2 ?? []).map(billRowToMovement);
972
+ const { data: payRows } = await pub.from(T.movements).select("*").or(`financial_invoice_id.eq.${query.invoiceId},invoice_id.eq.${query.invoiceId}`).eq("movement_kind", "payment").order("payment_date");
973
+ const payments = (payRows ?? []).map((r) => ({
974
+ ...snakeToCamel(r),
975
+ invoiceId: r.financial_invoice_id ?? r.invoice_id
976
+ }));
977
+ return { data: [...bills, ...payments], total: count2 ?? 0 };
978
+ }
979
+ let qb = pub.from(T.movements).select("*", { count: "exact" });
980
+ if (query.direction) qb = qb.eq("direction", query.direction);
981
+ if (query.status) {
982
+ const statuses = Array.isArray(query.status) ? query.status : [query.status];
983
+ qb = qb.in("status", statuses);
984
+ }
985
+ if (query.bankAccountId) qb = qb.eq("bank_account_id", query.bankAccountId);
986
+ if (query.cashSessionId) qb = qb.eq("cash_session_id", query.cashSessionId);
987
+ if (query.invoiceId) {
988
+ qb = qb.or(`financial_invoice_id.eq.${query.invoiceId},invoice_id.eq.${query.invoiceId}`);
989
+ }
990
+ if (query.dateRange) qb = qb.gte("due_date", query.dateRange.from).lte("due_date", query.dateRange.to);
991
+ const page = query.page ?? 1;
992
+ const pageSize = query.pageSize ?? 50;
993
+ qb = qb.range((page - 1) * pageSize, page * pageSize - 1).order("due_date");
994
+ const { data, count } = await qb;
995
+ return {
996
+ // `invoiceId` carrega o canônico quando existe: quem lê a tela compara
997
+ // com o id da fatura que abriu, e esse é o do modelo novo.
998
+ data: (data ?? []).map((r) => ({
999
+ ...snakeToCamel(r),
1000
+ invoiceId: r.financial_invoice_id ?? r.invoice_id
1001
+ })),
1002
+ total: count ?? 0
1003
+ };
1004
+ },
1005
+ /**
1006
+ * Receber — pela RPC, que é o único lugar onde receber dinheiro acontece.
1007
+ *
1008
+ * Antes isto escrevia à mão: inseria um movimento de pagamento, atualizava o
1009
+ * cache do bill, e recalculava o status da fatura. Três escritas no ESPELHO
1010
+ * `plg_financial_movements`, que parou de ser escrito na Fase 2 (financial
1011
+ * 040) e hoje tem 7 linhas, a última de agosto. Tudo o mais já lia
1012
+ * `v_financial_installment_balances`, a parcela com o seu saldo — só este caminho continuava
1013
+ * do outro lado.
1014
+ *
1015
+ * Duas coisas que a mão fazia e a RPC faz melhor: ela valida a sessão de
1016
+ * caixa aberta para dinheiro, exige conta para os métodos que precisam de
1017
+ * uma, e tem chave de idempotência — um duplo clique deixa de virar dois
1018
+ * recebimentos.
1019
+ *
1020
+ * E uma que a mão fazia e NÃO devia: escrevia `orders.status` como
1021
+ * 'paid'/'partial'/'invoiced'. O status do pedido é do produto que o abriu —
1022
+ * uma comanda paga estaria com um status que o fluxo do salão não conhece, e
1023
+ * o quadro a perderia. Quem registra o dinheiro não renomeia a venda.
1024
+ *
1025
+ * `movementId` É o id da parcela: o modal recebe a linha de `T.bills`, que é
1026
+ * `v_financial_installment_balances`, e ali a chave chama `installment_id`.
1027
+ */
1028
+ /**
1029
+ * Estorna um pagamento. A RPC existia desde a base e NENHUMA camada de
1030
+ * TypeScript a expunha — o dinheiro entrava pela tela e só saía pelo SQL.
1031
+ *
1032
+ * Estornar NÃO apaga: lança a contrapartida e aponta `reversed_by_id`, então
1033
+ * o extrato mostra os dois lados e o balancete continua fechando. Um livro
1034
+ * onde um lançamento some não prova nada.
1035
+ */
1036
+ /**
1037
+ * A conta do cliente. `v_financial_invoice_balances` já traz total/pago/saldo
1038
+ * por fatura, então os indicadores são uma soma — não uma segunda contagem
1039
+ * que poderia divergir da lista logo abaixo dela.
1040
+ */
1041
+ async searchSellableServices(term) {
1042
+ const { pub } = getClients();
1043
+ let q = pub.from("products").select("id, name, price").eq("kind", "service").not("is_active", "is", false).order("name").limit(20);
1044
+ if (term.trim()) q = q.ilike("name", `%${term.trim()}%`);
1045
+ const { data, error } = await q;
1046
+ if (error) throw error;
1047
+ return (data ?? []).map((p) => ({ id: p.id, name: p.name, price: Number(p.price ?? 0) }));
1048
+ },
1049
+ async prepayServices(personId, lines) {
1050
+ const { pub } = getClients();
1051
+ const { data, error } = await pub.rpc("clinical_prepay_services", {
1052
+ p_party: personId,
1053
+ p_lines: lines.map((l) => ({
1054
+ service_id: l.serviceId,
1055
+ name: l.name,
1056
+ quantity: l.quantity,
1057
+ unit_price: l.unitPrice
1058
+ }))
1059
+ });
1060
+ if (error) throw error;
1061
+ const r = data ?? {};
1062
+ return { ok: r.ok === true, reason: r.reason, orderId: r.order_id, total: r.total };
1063
+ },
1064
+ async getClientAccount(personId) {
1065
+ const { pub } = getClients();
1066
+ const { data, error } = await pub.from("v_financial_invoice_balances").select("id, number, order_id, competence_date, total, paid, balance, status, description").eq("counterparty_person_id", personId).eq("direction", "receivable").is("cancelled_at", null).order("competence_date", { ascending: false });
1067
+ if (error) throw error;
1068
+ const rows = data ?? [];
1069
+ const invoices = rows.map((r) => ({
1070
+ id: r.id,
1071
+ number: r.number,
1072
+ orderId: r.order_id,
1073
+ competenceDate: r.competence_date,
1074
+ total: Number(r.total) || 0,
1075
+ paid: Number(r.paid) || 0,
1076
+ balance: Number(r.balance) || 0,
1077
+ status: r.status,
1078
+ description: r.description
1079
+ }));
1080
+ let paidToExecute;
1081
+ let openToExecute;
1082
+ try {
1083
+ const { data: exec } = await pub.from("v_clinical_to_execute").select("paid_to_execute, open_to_execute").eq("party_id", personId).maybeSingle();
1084
+ if (exec) {
1085
+ paidToExecute = Number(exec.paid_to_execute) || 0;
1086
+ openToExecute = Number(exec.open_to_execute) || 0;
1087
+ }
1088
+ } catch {
1089
+ }
1090
+ return {
1091
+ personId,
1092
+ contracted: invoices.reduce((a, i) => a + i.total, 0),
1093
+ paid: invoices.reduce((a, i) => a + i.paid, 0),
1094
+ open: invoices.reduce((a, i) => a + i.balance, 0),
1095
+ paidToExecute,
1096
+ openToExecute,
1097
+ invoices
1098
+ };
1099
+ },
1100
+ /**
1101
+ * A previsão de recebimento de cartão. `v_financial_card_receivables` já
1102
+ * reparte o bruto pelas parcelas, aplica a taxa da bandeira/faixa e data
1103
+ * cada liquidação — a tela só ordena e soma.
1104
+ */
1105
+ async getCardReceivables() {
1106
+ const { pub } = getClients();
1107
+ const { data, error } = await pub.from("v_financial_card_receivables").select("movement_id, account_name, card_brand, card_installments, installment_k, payment_date, gross, fee_rate, fee, net, settlement_date, missing_fee_config").order("settlement_date");
1108
+ if (error) throw error;
1109
+ return (data ?? []).map((r) => ({
1110
+ movementId: r.movement_id,
1111
+ accountName: r.account_name,
1112
+ cardBrand: r.card_brand,
1113
+ cardInstallments: r.card_installments,
1114
+ installmentK: r.installment_k,
1115
+ paymentDate: r.payment_date,
1116
+ gross: Number(r.gross) || 0,
1117
+ feeRate: r.fee_rate == null ? null : Number(r.fee_rate),
1118
+ fee: Number(r.fee) || 0,
1119
+ net: Number(r.net) || 0,
1120
+ settlementDate: r.settlement_date,
1121
+ missingFeeConfig: !!r.missing_fee_config
1122
+ }));
1123
+ },
1124
+ async reverseMovement(movementId, reason) {
1125
+ const { pub } = getClients();
1126
+ const { error } = await pub.rpc("financial_reverse_movement", {
1127
+ p_movement_id: movementId,
1128
+ p_reason: reason,
1129
+ p_idempotency_key: null
1130
+ });
1131
+ if (error) throw error;
1132
+ },
1133
+ async payMovement(input) {
1134
+ const { pub } = getClients();
1135
+ const { data: before } = await pub.from(T.bills).select("*").eq("installment_id", input.movementId).maybeSingle();
1136
+ if (!before) throw new Error("Parcela n\xE3o encontrada");
1137
+ let method = null;
1138
+ if (input.paymentMethodTypeId) {
1139
+ const { data: type } = await pub.from(T.paymentMethodTypes).select("transaction_type").eq("id", input.paymentMethodTypeId).maybeSingle();
1140
+ method = type?.transaction_type ?? null;
1141
+ }
1142
+ const feePct = before.direction === "receivable" ? await getMethodFeePct(pub, input.paymentMethodId) : 0;
1143
+ const fee = paymentFee(input.amount, feePct, before.direction);
1144
+ const { error } = await pub.rpc("financial_receive_payment", {
1145
+ p_installment_id: input.movementId,
1146
+ p_amount: input.amount,
1147
+ p_account_id: input.bankAccountId ?? null,
1148
+ p_method: ledgerMethod(method),
1149
+ p_cash_session_id: input.cashSessionId ?? null,
1150
+ p_card_brand: input.cardBrand ?? null,
1151
+ p_card_installments: input.cardInstallments ?? null,
1152
+ p_payment_date: input.paymentDate,
1153
+ p_fee_amount: fee || null,
1154
+ p_notes: input.notes ?? null,
1155
+ p_idempotency_key: input.idempotencyKey ?? createIdempotencyKey("financial:payment"),
1156
+ p_metadata: input.metadata ?? {}
1157
+ });
1158
+ if (error) throw error;
1159
+ try {
1160
+ const { data: after } = await pub.from(T.bills).select("*").eq("installment_id", input.movementId).maybeSingle();
1161
+ return billRowToMovement(after ?? before);
1162
+ } catch {
1163
+ return billRowToMovement(before);
1164
+ }
1165
+ },
1166
+ async cancelMovement(id) {
1167
+ const { pub } = getClients();
1168
+ await pub.from(T.movements).update({ status: "cancelled" }).eq("id", id);
1169
+ },
1170
+ // --- Transfers (between accounts) ---
1171
+ async createTransfer(input) {
1172
+ const { pub } = getClients();
1173
+ const transferId = genId();
1174
+ const legs = transferLegs(input, transferId);
1175
+ const tenantId = getActiveTenantId();
1176
+ const rowsToInsert = legs.map((leg) => ({
1177
+ tenant_id: tenantId,
1178
+ movement_kind: "transfer",
1179
+ direction: leg.direction,
1180
+ bank_account_id: leg.bankAccountId,
1181
+ amount: input.amount,
1182
+ paid_amount: input.amount,
1183
+ fee_amount: 0,
1184
+ status: "paid",
1185
+ due_date: input.date,
1186
+ payment_date: input.date,
1187
+ notes: input.notes ?? null,
1188
+ metadata: leg.metadata
1189
+ }));
1190
+ let { data, error } = await pub.from(T.movements).insert(rowsToInsert).select("id, direction");
1191
+ if (error && /fee_amount/i.test(error.message ?? "")) {
1192
+ const stripped = rowsToInsert.map(({ fee_amount, ...rest }) => rest);
1193
+ ({ data, error } = await pub.from(T.movements).insert(stripped).select("id, direction"));
1194
+ }
1195
+ if (error) throw error;
1196
+ const rows = data ?? [];
1197
+ const debitMovementId = rows.find((r) => r.direction === "debit")?.id ?? "";
1198
+ const creditMovementId = rows.find((r) => r.direction === "credit")?.id ?? "";
1199
+ return { transferId, debitMovementId, creditMovementId };
1200
+ },
1201
+ // --- Bank Accounts ---
1202
+ /**
1203
+ * As contas onde um recebimento pode CAIR — da mesma tabela que a RPC valida.
1204
+ *
1205
+ * `plg_financial_bank_accounts` e `plg_financial_accounts` são duas tabelas,
1206
+ * com dois vocabulários: `account_type` ('bank_account', 'cash_register')
1207
+ * contra `kind` ('bank', 'cash_physical', 'card_receivable_credit'…). Os ids
1208
+ * coincidem onde as duas têm a linha — mas as contas de recebível de cartão
1209
+ * existem SÓ na segunda, e era ela que a RPC conferia.
1210
+ *
1211
+ * O efeito: o seletor do modal, lendo a primeira, nunca conseguia oferecer
1212
+ * uma conta de cartão, e `financial_receive_payment` recusava com "method
1213
+ * credit_card must settle in a card_receivable_credit account". Cartão não
1214
+ * estava quebrado para um tenant — estava quebrado para todos.
1215
+ *
1216
+ * `accountType` carrega o `kind` canônico porque é ele que o filtro de
1217
+ * `allowed_account_types` compara. Um vocabulário, não dois.
1218
+ */
1219
+ async getBankAccounts() {
1220
+ const { pub } = getClients();
1221
+ const { data, error } = await pub.from("plg_financial_accounts").select("*").eq("is_active", true).order("name");
1222
+ if (error) throw error;
1223
+ return (data ?? []).map((r) => ({
1224
+ ...snakeToCamel(r),
1225
+ accountType: r.kind
1226
+ }));
1227
+ },
1228
+ async getAccountsByIds(ids) {
1229
+ if (ids.length === 0) return [];
1230
+ const { pub } = getClients();
1231
+ const { data, error } = await pub.from("plg_financial_accounts").select("*").in("id", ids);
1232
+ if (error) throw error;
1233
+ return (data ?? []).map((r) => ({
1234
+ ...snakeToCamel(r),
1235
+ accountType: r.kind
1236
+ }));
1237
+ },
1238
+ // Só a conta INTEGRADA, como no V1. A view já faz o `openfinance_configured`
1239
+ // — uma conta sem origem ativa nunca vai trazer uma linha, e oferecê-la é
1240
+ // convidar a pessoa a concluir que a tela está quebrada.
1241
+ async getStatementAccounts() {
1242
+ const { pub } = getClients();
1243
+ const { data, error } = await pub.from(T.statementAccounts).select("*").order("kind").order("name");
1244
+ if (error) throw error;
1245
+ return (data ?? []).map((r) => ({
1246
+ ...snakeToCamel(r),
1247
+ accountType: r.kind
1248
+ }));
1249
+ },
1250
+ async searchFinancialAccounts(query, limit = 20) {
1251
+ const term = query.trim();
1252
+ if (term.length < 2) return [];
1253
+ const { pub } = getClients();
1254
+ const groups = [
1255
+ ["person"],
1256
+ ["bank", "unit"],
1257
+ ["cash_physical", "cash_session"],
1258
+ ["card_receivable_credit", "card_receivable_debit", "card_payable"],
1259
+ ["unidentified", "system"]
1260
+ ];
1261
+ const perGroup = Math.min(Math.max(Math.ceil(limit / groups.length), 4), 12);
1262
+ const answers = await Promise.all(groups.map(async (kinds) => {
1263
+ const { data, error } = await pub.from("plg_financial_accounts").select("*").eq("is_active", true).in("kind", kinds).ilike("name", `%${term}%`).order("name").limit(perGroup);
1264
+ if (error) throw error;
1265
+ return (data ?? []).map((row) => ({
1266
+ ...snakeToCamel(row),
1267
+ accountType: row.kind
1268
+ }));
1269
+ }));
1270
+ const out = [];
1271
+ for (let index = 0; out.length < limit && answers.some((rows) => index < rows.length); index += 1) {
1272
+ for (const rows of answers) {
1273
+ if (rows[index]) out.push(rows[index]);
1274
+ if (out.length === limit) break;
1275
+ }
1276
+ }
1277
+ return out;
1278
+ },
1279
+ /**
1280
+ * Writes where getBankAccounts READS.
1281
+ *
1282
+ * It used to insert into `plg_financial_bank_accounts` while the reader
1283
+ * came from `plg_financial_accounts` — so an account created through the UI
1284
+ * was never seen again by any screen or RPC. The caixa screen's own empty
1285
+ * state says "crie uma conta do tipo Caixa primeiro", and following that
1286
+ * instruction landed the row in a table nothing reads.
1287
+ *
1288
+ * `accountType` carries the canonical `kind` (see getBankAccounts), so it is
1289
+ * written to `kind`; `account_type` keeps the descriptive value the older
1290
+ * rows carry.
1291
+ */
1292
+ async createBankAccount(input) {
1293
+ const { pub } = getClients();
1294
+ const tenantId = getActiveTenantId();
1295
+ const row = camelToSnake(input);
1296
+ const kind = row.account_type;
1297
+ delete row.account_type;
1298
+ delete row.initial_balance;
1299
+ const { data, error } = await pub.from("plg_financial_accounts").insert({
1300
+ ...row,
1301
+ tenant_id: tenantId,
1302
+ kind,
1303
+ account_type: kind,
1304
+ is_active: true
1305
+ }).select().single();
1306
+ if (error) throw error;
1307
+ return { ...snakeToCamel(data), accountType: data.kind };
1308
+ },
1309
+ async updateBankAccount(id, partial) {
1310
+ const { pub } = getClients();
1311
+ const row = camelToSnake(partial);
1312
+ delete row.id;
1313
+ delete row.tenant_id;
1314
+ if (row.account_type) {
1315
+ row.kind = row.account_type;
1316
+ }
1317
+ const { data, error } = await pub.from("plg_financial_accounts").update(row).eq("id", id).select().single();
1318
+ if (error) throw error;
1319
+ return { ...snakeToCamel(data), accountType: data.kind };
1320
+ },
1321
+ // --- Cash Sessions ---
1322
+ /**
1323
+ * Sessions carrying what the caixa SHOULD hold right now.
1324
+ *
1325
+ * The close screen used to compare the counted cash against the OPENING
1326
+ * balance, so every cash sale of the shift read as a surplus. The expected
1327
+ * value comes from the same view the close RPC reads, so the operator
1328
+ * confirms the number the system is about to record.
1329
+ */
1330
+ async getCashSessions(bankAccountId) {
1331
+ const { pub } = getClients();
1332
+ let qb = pub.from(T.cashRegisterSessions).select("*");
1333
+ if (bankAccountId) qb = qb.eq("bank_account_id", bankAccountId);
1334
+ const { data, error } = await qb.order("opened_at", { ascending: false });
1335
+ if (error) throw error;
1336
+ const rows = (data ?? []).map((r) => snakeToCamel(r));
1337
+ if (!rows.length) return rows;
1338
+ const { data: exp } = await pub.from("v_financial_cash_session_expected").select("session_id, expected_cash, received_by_method, payment_count").in("session_id", rows.map((r) => r.id));
1339
+ const by = new Map((exp ?? []).map((e) => [e.session_id, e]));
1340
+ return rows.map((r) => {
1341
+ const e = by.get(r.id);
1342
+ return e ? {
1343
+ ...r,
1344
+ expectedCash: Number(e.expected_cash ?? 0),
1345
+ receivedByMethod: e.received_by_method ?? {},
1346
+ paymentCount: e.payment_count ?? 0
1347
+ } : r;
1348
+ });
1349
+ },
1350
+ async getOpenSession(bankAccountId) {
1351
+ const { pub } = getClients();
1352
+ const { data, error } = await pub.from(T.cashRegisterSessions).select("*").eq("bank_account_id", bankAccountId).eq("status", "open").single();
1353
+ if (error) throw error;
1354
+ return data ? snakeToCamel(data) : null;
1355
+ },
1356
+ // Verbo, não INSERT. A escrita direta parecia funcionar e não abria caixa
1357
+ // nenhum: `financial_cash_session_open` cria uma CONTA dedicada para a
1358
+ // caixinha, funda-a com um movimento a partir do cofre, exige
1359
+ // `financial.cash_session`, recusa a segunda sessão aberta do mesmo
1360
+ // operador na mesma unidade, valida que o cofre é `cash_physical` e audita.
1361
+ // A linha inserida à mão não tinha conta — e sem conta nenhum movimento
1362
+ // pode pertencer a ela, então o fechamento contava zero.
1363
+ async openCashSession(input) {
1364
+ const { pub } = getClients();
1365
+ const { data, error } = await pub.rpc("financial_cash_session_open", {
1366
+ p_cofre_account_id: input.bankAccountId,
1367
+ p_opening_amount: input.openingBalance,
1368
+ p_idempotency_key: null
1369
+ });
1370
+ if (error) throw error;
1371
+ const sessionId = data?.session_id;
1372
+ if (!sessionId) throw new Error("financial_cash_session_open n\xE3o devolveu sess\xE3o");
1373
+ const { data: row, error: readError } = await pub.from(T.cashRegisterSessions).select("*").eq("id", sessionId).single();
1374
+ if (readError) throw readError;
1375
+ return snakeToCamel(row);
1376
+ },
1377
+ // O fechamento é uma CONFERÊNCIA, e o UPDATE à mão não conferia nada: a RPC
1378
+ // calcula o esperado (saldo da conta da caixinha para dinheiro, soma dos
1379
+ // pagamentos em cartão da sessão), compara com o contado, devolve o
1380
+ // dinheiro contado ao cofre e lança a diferença como quebra de caixa —
1381
+ // falta ou sobra — em conta de sistema própria. Depois audita.
1382
+ //
1383
+ // A RPC fecha a sessão aberta DE QUEM CHAMA, e por isso não recebe id:
1384
+ // fecha-se o próprio caixa. `sessionId` continua na entrada pelo contrato
1385
+ // do provider e é conferido aqui, para o erro sair como "não é o seu
1386
+ // caixa" em vez de fechar silenciosamente o caixa errado.
1387
+ async closeCashSession(input) {
1388
+ const { pub } = getClients();
1389
+ const { data, error } = await pub.rpc("financial_cash_session_close", {
1390
+ p_counted_cash: input.closingBalance,
1391
+ p_counted_card: input.countedCard ?? 0,
1392
+ p_idempotency_key: null,
1393
+ p_notes: input.notes ?? null
1394
+ });
1395
+ if (error) throw error;
1396
+ const closedId = data?.session_id;
1397
+ if (closedId && input.sessionId && closedId !== input.sessionId) {
1398
+ throw new Error("o caixa fechado n\xE3o \xE9 o que a tela pediu \u2014 recarregue a lista");
1399
+ }
1400
+ const { data: row, error: readError } = await pub.from(T.cashRegisterSessions).select("*").eq("id", closedId ?? input.sessionId).single();
1401
+ if (readError) throw readError;
1402
+ return snakeToCamel(row);
1403
+ },
1404
+ async getCashSessionSummary(sessionId) {
1405
+ const { pub } = getClients();
1406
+ const session = (await pub.from(T.cashRegisterSessions).select("*").eq("id", sessionId).single()).data;
1407
+ const { data: movs } = await pub.from(T.movements).select("direction, paid_amount").eq("cash_session_id", sessionId).eq("status", "paid");
1408
+ const flow = sessionCashFlow((movs ?? []).map((m) => ({ direction: m.direction, paidAmount: m.paid_amount })));
1409
+ return { session: snakeToCamel(session), ...flow };
1410
+ },
1411
+ // --- Payment Methods ---
1412
+ async getPaymentMethods() {
1413
+ const { pub } = getClients();
1414
+ const { data, error } = await pub.from(T.paymentMethods).select("*").eq("is_active", true).order("name");
1415
+ if (error) throw error;
1416
+ return (data ?? []).map((r) => snakeToCamel(r));
1417
+ },
1418
+ async getPaymentMethodTypes() {
1419
+ const { pub } = getClients();
1420
+ const { data, error } = await pub.from(T.paymentMethodTypes).select("*").eq("is_active", true).order("name");
1421
+ if (error) throw error;
1422
+ return (data ?? []).map((r) => snakeToCamel(r));
1423
+ },
1424
+ // --- Chart of Accounts ---
1425
+ async getChartOfAccounts() {
1426
+ const { pub } = getClients();
1427
+ const { data, error } = await pub.from(T.chartOfAccounts).select("*").eq("is_active", true).order("code");
1428
+ if (error) throw error;
1429
+ return (data ?? []).map((r) => snakeToCamel(r));
1430
+ },
1431
+ async resolveChartAccountRefs(refs) {
1432
+ const unique = [...new Set(refs.map((ref) => String(ref).trim()).filter(Boolean))];
1433
+ if (unique.length === 0) return {};
1434
+ const { pub } = getClients();
1435
+ const { data, error } = await pub.rpc("financial_resolve_chart_account_refs", { p_refs: unique });
1436
+ if (error) throw error;
1437
+ return Object.fromEntries((data ?? []).map((row) => [String(row.source_ref), String(row.chart_account_id)]));
1438
+ },
1439
+ async getCostCenters() {
1440
+ const { pub } = getClients();
1441
+ const { data, error } = await pub.from(T.costCenters).select("*").eq("is_active", true).order("code");
1442
+ if (error) throw error;
1443
+ return (data ?? []).map((r) => snakeToCamel(r));
1444
+ },
1445
+ // --- Card Transactions ---
1446
+ async getCardTransactions(dateRange) {
1447
+ getClients();
1448
+ return [];
1449
+ },
1450
+ // --- Statements (ERP extract: opening balance + realized cash + per-row net/fee) ---
1451
+ async getStatement(query) {
1452
+ const { core, pub } = getClients();
1453
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1454
+ const { data: acctRows } = await pub.from("plg_financial_accounts").select("id, name, is_active");
1455
+ const allAccounts = acctRows ?? [];
1456
+ const consolidated = !query.bankAccountId;
1457
+ const scopeAccounts = consolidated ? allAccounts.filter((a) => a.is_active) : allAccounts.filter((a) => a.id === query.bankAccountId);
1458
+ const accountIds = scopeAccounts.map((a) => a.id);
1459
+ const accountNameById = new Map(allAccounts.map((a) => [a.id, a.name]));
1460
+ if (!consolidated && accountIds.length === 0) return emptyStatement(query.bankAccountId);
1461
+ const scopeFilter = (qb) => consolidated ? qb : qb.in("bank_account_id", accountIds);
1462
+ const toMovement = (r) => snakeToCamel(r);
1463
+ const { data: priorRows } = await scopeFilter(pub.from(T.cashMovements).select("*").neq("status", "cancelled").lt("payment_date", query.dateRange.from));
1464
+ const openingBalance = ledgerBalance(0, (priorRows ?? []).map(toMovement));
1465
+ const { data: movs } = await scopeFilter(pub.from(T.cashMovements).select("*").neq("status", "cancelled").gte("payment_date", query.dateRange.from).lte("payment_date", query.dateRange.to).order("payment_date").order("created_at"));
1466
+ const invoiceIds = [...new Set((movs ?? []).map((r) => r.invoice_id).filter(Boolean))];
1467
+ const invoiceById = /* @__PURE__ */ new Map();
1468
+ if (invoiceIds.length) {
1469
+ const { data: orders } = await core.from("orders").select("id, party_id, reference_number, metadata").in("id", invoiceIds);
1470
+ for (const o of orders ?? []) {
1471
+ invoiceById.set(o.id, { id: o.id, contactId: o.party_id, contactName: o.metadata?.contactName, fiscalNumber: o.reference_number });
1472
+ }
1473
+ }
1474
+ const statement = buildStatement({
1475
+ openingBalance,
1476
+ movements: (movs ?? []).map(toMovement),
1477
+ consolidated,
1478
+ accountNameOf: (id) => accountNameById.get(id),
1479
+ invoiceOf: (movement) => movement.invoiceId ? invoiceById.get(movement.invoiceId) : void 0
1480
+ });
1481
+ const entries = statement.entries;
1482
+ const matchedIds = [...new Set(entries.map((e) => e.movement.matchedMovementId).filter(Boolean))];
1483
+ if (matchedIds.length) {
1484
+ const { data: matched } = await pub.from(T.movements).select("id, invoice_id, notes").in("id", matchedIds);
1485
+ const matchedById = new Map((matched ?? []).map((m) => [m.id, m]));
1486
+ const matchedInvoiceIds = [...new Set((matched ?? []).map((m) => m.invoice_id).filter(Boolean))];
1487
+ const invById = /* @__PURE__ */ new Map();
1488
+ if (matchedInvoiceIds.length) {
1489
+ const { data: orders } = await core.from("orders").select("id, reference_number, metadata").in("id", matchedInvoiceIds);
1490
+ for (const o of orders ?? []) invById.set(o.id, o);
1491
+ }
1492
+ for (const e of entries) {
1493
+ const mid = e.movement.matchedMovementId;
1494
+ const mm = mid ? matchedById.get(mid) : void 0;
1495
+ if (!mm) continue;
1496
+ const inv = mm.invoice_id ? invById.get(mm.invoice_id) : void 0;
1497
+ e.reconciledWith = {
1498
+ movementId: mm.id,
1499
+ description: mm.notes ?? void 0,
1500
+ invoiceId: mm.invoice_id ?? void 0,
1501
+ fiscalNumber: inv?.reference_number ?? void 0,
1502
+ contactName: inv?.metadata?.contactName ?? void 0
1503
+ };
1504
+ }
1505
+ }
1506
+ const { data: todayRows } = await scopeFilter(pub.from(T.cashMovements).select("*").neq("status", "cancelled").lte("payment_date", today));
1507
+ return {
1508
+ ...statement,
1509
+ accountId: query.bankAccountId,
1510
+ accountCurrentBalance: ledgerBalance(0, (todayRows ?? []).map(toMovement))
1511
+ };
1512
+ },
1513
+ async getDre(query) {
1514
+ const { pub } = getClients();
1515
+ const columns = "unit_id,competence_month,direction,account_id,account_code,account_name,account_type,account_parent_id,statement_code,statement_name,unclassified,line_count,amount,discount";
1516
+ const rpcResult = await pub.rpc("financial_dre", {
1517
+ p_from: `${query.year}-01-01`,
1518
+ p_to: `${query.year}-12-31`,
1519
+ p_unit_id: query.unitId ?? null
1520
+ });
1521
+ if (!rpcResult.error) return (rpcResult.data ?? []).map((row) => ({
1522
+ unitId: row.unit_id ?? void 0,
1523
+ competenceMonth: row.competence_month ?? void 0,
1524
+ accountId: row.account_id ?? void 0,
1525
+ accountCode: row.account_code ?? void 0,
1526
+ accountName: row.account_name ?? void 0,
1527
+ accountType: row.account_type,
1528
+ parentAccountId: row.account_parent_id ?? void 0,
1529
+ statementCode: row.statement_code ?? void 0,
1530
+ statementName: row.statement_name ?? void 0,
1531
+ unclassified: Boolean(row.unclassified),
1532
+ lineCount: Number(row.line_count) || 0,
1533
+ amount: Number(row.amount) || 0,
1534
+ discount: Number(row.discount) || 0
1535
+ }));
1536
+ if (rpcResult.error.code !== "PGRST202" && rpcResult.error.code !== "42883") throw rpcResult.error;
1537
+ const { data, error } = await pub.from("plg_financial_rep_dre").select(columns).or(`competence_month.is.null,and(competence_month.gte.${query.year}-01-01,competence_month.lte.${query.year}-12-31)`).order("account_code", { ascending: true }).order("competence_month", { ascending: true });
1538
+ if (error) throw error;
1539
+ return (data ?? []).filter((row) => !row.competence_month || !query.unitId || row.unit_id === query.unitId).map((row) => ({
1540
+ unitId: row.unit_id ?? void 0,
1541
+ competenceMonth: row.competence_month ?? void 0,
1542
+ accountId: row.account_id ?? void 0,
1543
+ accountCode: row.account_code ?? void 0,
1544
+ accountName: row.account_name ?? void 0,
1545
+ accountType: row.direction === "receivable" ? "revenue" : row.direction === "payable" ? "expense" : row.account_type,
1546
+ parentAccountId: row.account_parent_id ?? void 0,
1547
+ statementCode: row.statement_code ?? void 0,
1548
+ statementName: row.statement_name ?? void 0,
1549
+ unclassified: Boolean(row.unclassified),
1550
+ lineCount: Number(row.line_count) || 0,
1551
+ amount: Number(row.amount) || 0,
1552
+ discount: Number(row.discount) || 0
1553
+ }));
1554
+ },
1555
+ // --- Summary ---
1556
+ // --- Fechamento e prova do livro (006) ---
1557
+ async getPeriods() {
1558
+ const { pub } = getClients();
1559
+ const { data, error } = await pub.from(T.periods).select("*").order("competence_month", { ascending: false });
1560
+ if (error) throw error;
1561
+ return (data ?? []).map((r) => ({
1562
+ id: r.id,
1563
+ competenceMonth: r.competence_month,
1564
+ status: r.status,
1565
+ closedAt: r.closed_at,
1566
+ reopenReason: r.reopen_reason
1567
+ }));
1568
+ },
1569
+ async closePeriod(month, notes) {
1570
+ const { pub } = getClients();
1571
+ const { error } = await pub.rpc("financial_period_close", { p_month: month, p_notes: notes ?? null });
1572
+ if (error) throw error;
1573
+ },
1574
+ async reopenPeriod(month, reason) {
1575
+ const { pub } = getClients();
1576
+ const { error } = await pub.rpc("financial_period_reopen", { p_month: month, p_reason: reason });
1577
+ if (error) throw error;
1578
+ },
1579
+ async getTrialBalance(competenceMonth) {
1580
+ const { pub } = getClients();
1581
+ let qb = pub.from(T.trialBalance).select("*");
1582
+ if (competenceMonth) qb = qb.eq("competence_month", competenceMonth);
1583
+ const { data, error } = await qb.order("account_code");
1584
+ if (error) throw error;
1585
+ return (data ?? []).map((r) => ({
1586
+ accountId: r.account_id,
1587
+ accountCode: r.account_code,
1588
+ accountName: r.account_name,
1589
+ competenceMonth: r.competence_month,
1590
+ totalDebit: Number(r.total_debit) || 0,
1591
+ totalCredit: Number(r.total_credit) || 0,
1592
+ balance: Number(r.balance) || 0
1593
+ }));
1594
+ },
1595
+ // --- Cobrança (016) e margem (015) ---
1596
+ async getAging() {
1597
+ const { pub } = getClients();
1598
+ const { data, error } = await pub.from(T.aging).select("*").eq("direction", "receivable").order("due_date");
1599
+ if (error) throw error;
1600
+ return (data ?? []).map((r) => ({
1601
+ installmentId: r.installment_id,
1602
+ invoiceNumber: r.invoice_number,
1603
+ counterpartyName: r.counterparty_name,
1604
+ dueDate: r.due_date,
1605
+ balance: Number(r.balance) || 0,
1606
+ daysOverdue: Number(r.days_overdue) || 0,
1607
+ bucket: r.bucket
1608
+ }));
1609
+ },
1610
+ async getMarginByMonth() {
1611
+ const { pub } = getClients();
1612
+ const { data, error } = await pub.from(T.marginByMonth).select("*").order("competence_month", { ascending: false });
1613
+ if (error) throw error;
1614
+ return (data ?? []).map((r) => ({
1615
+ competenceMonth: r.competence_month,
1616
+ revenue: Number(r.revenue) || 0,
1617
+ cogs: Number(r.cogs) || 0,
1618
+ commission: Number(r.commission) || 0,
1619
+ margin: Number(r.margin) || 0,
1620
+ marginPercent: Number(r.margin_percent) || 0
1621
+ }));
1622
+ },
1623
+ // --- Contas recorrentes (014) ---
1624
+ async getRecurringBills(direction) {
1625
+ const { pub } = getClients();
1626
+ const { data, error } = await pub.from(T.recurringBills).select("*").eq("direction", direction).order("is_active", { ascending: false }).order("description");
1627
+ if (error) throw error;
1628
+ const rows = data ?? [];
1629
+ const personIds = [...new Set(rows.map((row) => row.counterparty_person_id).filter(Boolean))];
1630
+ const names = /* @__PURE__ */ new Map();
1631
+ if (personIds.length > 0) {
1632
+ const { data: people, error: peopleError } = await pub.from("people").select("id,name").in("id", personIds);
1633
+ if (peopleError) throw peopleError;
1634
+ for (const person of people ?? []) names.set(person.id, person.name);
1635
+ }
1636
+ return rows.map((row) => mapRecurringBill(row, names.get(row.counterparty_person_id) ?? null));
1637
+ },
1638
+ // ------------------------------------------------------------------
1639
+ // O PREVISTO, ao lado do LANÇADO.
1640
+ //
1641
+ // A ocorrência provisionada não é conta: não tem fatura, não entra no
1642
+ // razão e não cobra ninguém. Mas ela é o que faz o mês aparecer inteiro na
1643
+ // lista — o V1 injeta essas linhas ao lado das contas de verdade, e é
1644
+ // olhando as duas juntas que alguém decide o que vira dívida.
1645
+ //
1646
+ // Linha VIRTUAL, com `id` prefixado: ela não pode colidir com o id de uma
1647
+ // parcela nem ser aberta como fatura, porque não há fatura.
1648
+ // ------------------------------------------------------------------
1649
+ async getProvisionedOccurrences(query) {
1650
+ const { pub } = getClients();
1651
+ const direcao = query.direction === "debit" ? "payable" : "receivable";
1652
+ let qb = pub.from(T.recurringOccurrences).select(`id, seq, due_date, competence_date, amount, unit_id, recurring_bill_id,
1653
+ ${T.recurringBills}!inner(id, description, notes, direction, counterparty_person_id,
1654
+ chart_account_id, cost_center_id, is_active)`).eq("status", "provisioned").eq(`${T.recurringBills}.direction`, direcao).eq(`${T.recurringBills}.is_active`, true);
1655
+ if (query.contactId) qb = qb.eq(`${T.recurringBills}.counterparty_person_id`, query.contactId);
1656
+ if (query.accountId) qb = qb.eq(`${T.recurringBills}.chart_account_id`, query.accountId);
1657
+ if (query.costCenterId) qb = qb.eq(`${T.recurringBills}.cost_center_id`, query.costCenterId);
1658
+ if (query.unitId) qb = qb.or(`unit_id.is.null,unit_id.eq.${query.unitId}`);
1659
+ if (query.dateRange) {
1660
+ qb = qb.gte("due_date", query.dateRange.from.slice(0, 10)).lte("due_date", query.dateRange.to.slice(0, 10));
1661
+ }
1662
+ const { data, error } = await qb.order("due_date", { ascending: true }).limit(200);
1663
+ if (error) throw error;
1664
+ const linhas = data ?? [];
1665
+ const pessoaIds = [...new Set(linhas.map((row) => row[T.recurringBills]?.counterparty_person_id).filter(Boolean))];
1666
+ const nomes = /* @__PURE__ */ new Map();
1667
+ if (pessoaIds.length > 0) {
1668
+ const { data: people } = await pub.from("people").select("id,name").in("id", pessoaIds);
1669
+ for (const person of people ?? []) nomes.set(person.id, person.name);
1670
+ }
1671
+ const termo = query.search?.trim().toLocaleLowerCase("pt-BR");
1672
+ return linhas.filter((row) => {
1673
+ if (!termo) return true;
1674
+ const regua = row[T.recurringBills] ?? {};
1675
+ const nome = regua.counterparty_person_id ? nomes.get(regua.counterparty_person_id) : "";
1676
+ return [regua.description, regua.notes, nome].some((campo) => (campo ?? "").toLocaleLowerCase("pt-BR").includes(termo));
1677
+ }).map((row) => {
1678
+ const regua = row[T.recurringBills] ?? {};
1679
+ return {
1680
+ id: `occ:${row.id}`,
1681
+ direction: query.direction ?? "debit",
1682
+ invoiceDate: row.competence_date ?? row.due_date,
1683
+ dueDate: row.due_date,
1684
+ totalAmount: Number(row.amount) || 0,
1685
+ paidAmount: 0,
1686
+ status: "provisioned",
1687
+ totalInstallments: 1,
1688
+ installmentNumber: row.seq ?? 1,
1689
+ contactId: regua.counterparty_person_id ?? void 0,
1690
+ contactName: regua.counterparty_person_id ? nomes.get(regua.counterparty_person_id) : void 0,
1691
+ unitId: row.unit_id ?? void 0,
1692
+ itemsSummary: regua.description ?? void 0,
1693
+ isProvisionedOccurrence: true,
1694
+ occurrenceId: row.id,
1695
+ recurringBillId: row.recurring_bill_id,
1696
+ tenantId: "",
1697
+ createdAt: row.due_date,
1698
+ updatedAt: row.due_date
1699
+ };
1700
+ });
1701
+ },
1702
+ async consolidateOccurrence(occurrenceId) {
1703
+ const { pub } = getClients();
1704
+ const { data, error } = await pub.rpc("financial_consolidate_occurrence", {
1705
+ p_occurrence_id: occurrenceId
1706
+ });
1707
+ if (error) throw new Error(error.message);
1708
+ return data?.invoice_id ?? null;
1709
+ },
1710
+ async saveRecurringBill(input) {
1711
+ const { pub } = getClients();
1712
+ const row = {
1713
+ direction: input.direction,
1714
+ description: input.description.trim(),
1715
+ counterparty_person_id: input.counterpartyPersonId,
1716
+ amount: round2(input.amount),
1717
+ items: input.items,
1718
+ unit_id: input.unitId || null,
1719
+ chart_account_id: input.chartAccountId || null,
1720
+ cost_center_id: input.costCenterId || null,
1721
+ frequency: input.frequency,
1722
+ due_day: input.dueDay,
1723
+ starts_on: input.startsOn,
1724
+ ends_on: input.endsOn || null,
1725
+ is_active: input.isActive ?? true,
1726
+ notes: input.notes?.trim() || null
1727
+ };
1728
+ const request = input.id ? pub.from(T.recurringBills).update(row).eq("id", input.id) : pub.from(T.recurringBills).insert({ ...row, tenant_id: getActiveTenantId() });
1729
+ const { data, error } = await request.select("*").single();
1730
+ if (error) throw error;
1731
+ return mapRecurringBill(data);
1732
+ },
1733
+ /**
1734
+ * Provisiona os vencimentos futuros de uma régua.
1735
+ *
1736
+ * Chamado logo depois de salvar: sem isto a recorrência cadastrada hoje não
1737
+ * aparece como previsto em lugar nenhum — ela só existiria no dia em que
1738
+ * alguém rodasse a rotina, já como dívida.
1739
+ */
1740
+ async provisionRecurringBill(billId) {
1741
+ const { pub } = getClients();
1742
+ const { data, error } = await pub.rpc("financial_recurring_provision", { p_bill_id: billId });
1743
+ if (error) throw new Error(error.message);
1744
+ return data?.provisioned ?? 0;
1745
+ },
1746
+ async updateRecurringBill(id, patch) {
1747
+ const { pub } = getClients();
1748
+ const row = {};
1749
+ if (patch.isActive !== void 0) row.is_active = patch.isActive;
1750
+ if (patch.endsOn !== void 0) row.ends_on = patch.endsOn;
1751
+ const { error } = await pub.from(T.recurringBills).update(row).eq("id", id);
1752
+ if (error) throw error;
1753
+ },
1754
+ async runRecurring(through) {
1755
+ const { pub } = getClients();
1756
+ const { data, error } = await pub.rpc("financial_recurring_run", { p_through: through ?? null });
1757
+ if (error) throw error;
1758
+ return { created: Number(data?.created) || 0, total: Number(data?.total) || 0 };
1759
+ },
1760
+ // --- Commissions (009) ---
1761
+ async getCommissions(query) {
1762
+ const { pub } = getClients();
1763
+ let qb = pub.from(T.commissions).select("*");
1764
+ if (query.periodStart) qb = qb.gte("competence_date", query.periodStart);
1765
+ if (query.periodEnd) qb = qb.lte("competence_date", query.periodEnd);
1766
+ if (query.personId) qb = qb.eq("person_id", query.personId);
1767
+ if (query.status) qb = qb.eq("status", query.status);
1768
+ const { data, error } = await qb.order("competence_date", { ascending: false });
1769
+ if (error) throw error;
1770
+ return (data ?? []).map((r) => ({
1771
+ id: r.id,
1772
+ personId: r.person_id,
1773
+ personName: r.person_name,
1774
+ itemId: r.item_id,
1775
+ itemName: r.item_name,
1776
+ orderId: r.order_id,
1777
+ invoiceId: r.invoice_id,
1778
+ invoiceNumber: r.invoice_number,
1779
+ competenceDate: r.competence_date,
1780
+ ruleName: r.rule_name,
1781
+ functionName: r.function_name,
1782
+ baseAmount: Number(r.base_amount) || 0,
1783
+ rate: Number(r.rate) || 0,
1784
+ amount: Number(r.amount) || 0,
1785
+ share: Number(r.share) || 1,
1786
+ role: r.role,
1787
+ status: r.status
1788
+ }));
1789
+ },
1790
+ async generateCommissions(input) {
1791
+ const { pub } = getClients();
1792
+ const { data, error } = await pub.rpc("financial_commission_generate", {
1793
+ p_period_start: input.periodStart,
1794
+ p_period_end: input.periodEnd,
1795
+ p_unit_id: input.unitId ?? null
1796
+ });
1797
+ if (error) throw error;
1798
+ return {
1799
+ generationId: data?.generation_id,
1800
+ entries: Number(data?.entries) || 0,
1801
+ total: Number(data?.total) || 0
1802
+ };
1803
+ },
1804
+ // ── As regras de comissão: a política que Configurações edita ────────────
1805
+ //
1806
+ // Numa consulta só, com critérios e funções aninhados, como o V1 faz. O
1807
+ // funil da tela simula qual regra venceria para uma combinação, e para isso
1808
+ // precisa de TODAS as regras inteiras em memória — buscar função por regra
1809
+ // seria N+1 a cada troca de filtro.
1810
+ async getCommissionRules() {
1811
+ const { pub } = getClients();
1812
+ const { data, error } = await pub.from(T.commissionRules).select(`*, ${T.commissionRuleCriteria}(*), ${T.commissionFunctions}(*)`).order("priority", { ascending: false });
1813
+ if (error) throw error;
1814
+ return (data ?? []).map((r) => ({
1815
+ id: r.id,
1816
+ name: r.name,
1817
+ description: r.description,
1818
+ priority: Number(r.priority) || 0,
1819
+ isDefault: !!r.is_default,
1820
+ isActive: !!r.is_active,
1821
+ unitId: r.unit_id,
1822
+ valueType: r.value_type ?? "percentage",
1823
+ value: Number(r.value) || 0,
1824
+ criteria: (r[T.commissionRuleCriteria] ?? []).map((c) => ({
1825
+ id: c.id,
1826
+ criteriaType: c.criteria_type,
1827
+ criteriaValue: c.criteria_value ?? {}
1828
+ })),
1829
+ functions: (r[T.commissionFunctions] ?? []).slice().sort((a, b) => (a.display_order ?? 0) - (b.display_order ?? 0)).map((f) => ({
1830
+ id: f.id,
1831
+ ruleId: f.rule_id,
1832
+ functionName: f.function_name,
1833
+ accountType: f.account_type,
1834
+ personId: f.person_id,
1835
+ valueType: f.value_type,
1836
+ // numeric(14,4) chega como string no PostgREST; `Number` aqui evita
1837
+ // o "R$ NaN" que já apareceu nos cartões por somar string.
1838
+ valueAmount: Number(f.value_amount) || 0,
1839
+ calculationBase: f.calculation_base,
1840
+ tierMode: f.tier_mode,
1841
+ minGuaranteed: f.min_guaranteed == null ? null : Number(f.min_guaranteed),
1842
+ tiers: Array.isArray(f.tiers) ? f.tiers : [],
1843
+ isVariable: !!f.is_variable,
1844
+ displayOrder: f.display_order ?? 0,
1845
+ isActive: !!f.is_active
1846
+ }))
1847
+ }));
1848
+ },
1849
+ async saveCommissionRule(input) {
1850
+ const { pub } = getClients();
1851
+ const row = {
1852
+ name: input.name,
1853
+ description: input.description ?? null,
1854
+ priority: input.priority,
1855
+ is_default: input.isDefault ?? false,
1856
+ is_active: input.isActive ?? true,
1857
+ unit_id: input.unitId ?? null
1858
+ };
1859
+ let ruleId = input.id;
1860
+ if (ruleId) {
1861
+ const { error } = await pub.from(T.commissionRules).update(row).eq("id", ruleId);
1862
+ if (error) throw error;
1863
+ } else {
1864
+ const { data, error } = await pub.from(T.commissionRules).insert({ ...row, tenant_id: getActiveTenantId() }).select("id").single();
1865
+ if (error) throw error;
1866
+ ruleId = data.id;
1867
+ }
1868
+ if (input.criteria) {
1869
+ const { error: delErr } = await pub.from(T.commissionRuleCriteria).delete().eq("rule_id", ruleId);
1870
+ if (delErr) throw delErr;
1871
+ const rows = input.criteria.filter((c) => hasCriteriaValue(c.criteriaValue)).map((c) => ({ tenant_id: getActiveTenantId(), rule_id: ruleId, criteria_type: c.criteriaType, criteria_value: c.criteriaValue }));
1872
+ if (rows.length) {
1873
+ const { error } = await pub.from(T.commissionRuleCriteria).insert(rows);
1874
+ if (error) throw error;
1875
+ }
1876
+ }
1877
+ return ruleId;
1878
+ },
1879
+ async saveCommissionFunctions(ruleId, functions) {
1880
+ const { pub } = getClients();
1881
+ const { error: delErr } = await pub.from(T.commissionFunctions).delete().eq("rule_id", ruleId);
1882
+ if (delErr) throw delErr;
1883
+ if (!functions.length) return;
1884
+ const rows = functions.map((f, i) => ({
1885
+ tenant_id: getActiveTenantId(),
1886
+ rule_id: ruleId,
1887
+ function_name: f.functionName,
1888
+ account_type: f.accountType,
1889
+ person_id: f.personId ?? null,
1890
+ value_type: f.valueType,
1891
+ value_amount: f.valueAmount,
1892
+ calculation_base: f.calculationBase,
1893
+ tier_mode: f.tierMode,
1894
+ // O piso só existe na faixa que o usa; guardá-lo nas outras deixaria um
1895
+ // número invisível na tela e ativo no cálculo se a faixa mudasse.
1896
+ min_guaranteed: f.tierMode === "guaranteed_min" ? f.minGuaranteed ?? 0 : null,
1897
+ tiers: f.tierMode === "marginal" || f.tierMode === "retroactive" ? f.tiers : [],
1898
+ is_variable: f.isVariable,
1899
+ display_order: i,
1900
+ is_active: f.isActive
1901
+ }));
1902
+ const { error } = await pub.from(T.commissionFunctions).insert(rows);
1903
+ if (error) throw error;
1904
+ },
1905
+ async deleteCommissionRule(id) {
1906
+ const { pub } = getClients();
1907
+ const { error } = await pub.from(T.commissionRules).delete().eq("id", id);
1908
+ if (error) throw error;
1909
+ },
1910
+ async duplicateCommissionRule(id) {
1911
+ const { pub } = getClients();
1912
+ const { data, error } = await pub.from(T.commissionRules).select(`*, ${T.commissionRuleCriteria}(*), ${T.commissionFunctions}(*)`).eq("id", id).single();
1913
+ if (error) throw error;
1914
+ const src = data;
1915
+ const { data: created, error: insErr } = await pub.from(T.commissionRules).insert({
1916
+ tenant_id: getActiveTenantId(),
1917
+ name: `${src.name} (c\xF3pia)`,
1918
+ description: src.description,
1919
+ priority: src.priority,
1920
+ is_default: false,
1921
+ is_active: false,
1922
+ unit_id: src.unit_id,
1923
+ value_type: src.value_type,
1924
+ value: src.value
1925
+ }).select("id").single();
1926
+ if (insErr) throw insErr;
1927
+ const newId = created.id;
1928
+ const crit = (src[T.commissionRuleCriteria] ?? []).map((c) => ({
1929
+ tenant_id: getActiveTenantId(),
1930
+ rule_id: newId,
1931
+ criteria_type: c.criteria_type,
1932
+ criteria_value: c.criteria_value
1933
+ }));
1934
+ if (crit.length) {
1935
+ const { error: e } = await pub.from(T.commissionRuleCriteria).insert(crit);
1936
+ if (e) throw e;
1937
+ }
1938
+ const fns = (src[T.commissionFunctions] ?? []).map((f) => ({
1939
+ tenant_id: getActiveTenantId(),
1940
+ rule_id: newId,
1941
+ function_name: f.function_name,
1942
+ account_type: f.account_type,
1943
+ person_id: f.person_id,
1944
+ value_type: f.value_type,
1945
+ value_amount: f.value_amount,
1946
+ calculation_base: f.calculation_base,
1947
+ tier_mode: f.tier_mode,
1948
+ min_guaranteed: f.min_guaranteed,
1949
+ tiers: f.tiers,
1950
+ is_variable: f.is_variable,
1951
+ display_order: f.display_order,
1952
+ is_active: f.is_active
1953
+ }));
1954
+ if (fns.length) {
1955
+ const { error: e } = await pub.from(T.commissionFunctions).insert(fns);
1956
+ if (e) throw e;
1957
+ }
1958
+ return newId;
1959
+ },
1960
+ async getCommissionCatalogs() {
1961
+ const { pub } = getClients();
1962
+ const [svc, cat, ppl, unt] = await Promise.all([
1963
+ pub.from("products").select("id,name,category_id").eq("is_active", true).order("name"),
1964
+ pub.from("categories").select("id,name").eq("is_active", true).order("name"),
1965
+ // `kind = 'staff'` é quem executa; a lista de critério não é a agenda de
1966
+ // clientes, e oferecer os dois juntos é como se escolhe o errado.
1967
+ pub.from("people").select("id,name").eq("kind", "staff").eq("is_active", true).order("name"),
1968
+ pub.from("v_units").select("id,name").eq("is_active", true).order("name")
1969
+ ]);
1970
+ return {
1971
+ services: (svc.data ?? []).map((r) => ({ id: r.id, name: r.name, categoryId: r.category_id })),
1972
+ categories: (cat.data ?? []).map((r) => ({ id: r.id, name: r.name })),
1973
+ professionals: (ppl.data ?? []).map((r) => ({ id: r.id, name: r.name })),
1974
+ units: (unt.data ?? []).map((r) => ({ id: r.id, name: r.name }))
1975
+ };
1976
+ },
1977
+ // ── O caminho do apurado até o dinheiro sair ────────────────────────────
1978
+ //
1979
+ // São TRÊS passos e três decisões, e o V2 só tinha o primeiro ligado:
1980
+ // apurar (gera os lançamentos), aprovar (autoriza), e o borderô (levanta
1981
+ // uma conta a pagar por pessoa). "Apurar não é autorizar" está no próprio
1982
+ // comentário da 012 — quem confere pode não ser quem manda pagar.
1983
+ async getCommissionGenerations() {
1984
+ const { pub } = getClients();
1985
+ const { data, error } = await pub.from(T.commissionGenerations).select("*").order("created_at", { ascending: false }).limit(20);
1986
+ if (error) throw error;
1987
+ return (data ?? []).map((r) => ({
1988
+ id: r.id,
1989
+ periodStart: r.period_start,
1990
+ periodEnd: r.period_end,
1991
+ status: r.status,
1992
+ // `total_amount` e `entry_count`, conferidos no schema — não `total`
1993
+ // nem `entries`, que era o palpite pelo nome do campo do resultado.
1994
+ total: Number(r.total_amount) || 0,
1995
+ entries: Number(r.entry_count) || 0
1996
+ }));
1997
+ },
1998
+ async approveCommissions(generationId) {
1999
+ const { pub } = getClients();
2000
+ const { data, error } = await pub.rpc("financial_commission_approve", {
2001
+ p_generation_id: generationId
2002
+ });
2003
+ if (error) throw error;
2004
+ return {
2005
+ generationId: data?.generation_id ?? generationId,
2006
+ approved: Number(data?.approved) || 0
2007
+ };
2008
+ },
2009
+ async payRunCommissions(generationId, dueDate) {
2010
+ const { pub } = getClients();
2011
+ const { data, error } = await pub.rpc("financial_commission_pay_run", {
2012
+ p_generation_id: generationId,
2013
+ p_due_date: dueDate ?? null
2014
+ });
2015
+ if (error) throw error;
2016
+ return {
2017
+ generationId: data?.generation_id ?? generationId,
2018
+ payables: Number(data?.payables) || 0,
2019
+ total: Number(data?.total) || 0,
2020
+ dueDate: data?.due_date
2021
+ };
2022
+ },
2023
+ // ── Metas: três degraus por mês, e o realizado vem do livro ─────────────
2024
+ // ── Conferência de caixa ────────────────────────────────────────────────
2025
+ async getCashSessionReviews() {
2026
+ const { pub } = getClients();
2027
+ const { data, error } = await pub.from("v_financial_cash_session_review").select("*").order("closed_at", { ascending: false }).limit(200);
2028
+ if (error) throw error;
2029
+ return (data ?? []).map((r) => ({
2030
+ sessionId: r.session_id,
2031
+ unitId: r.unit_id,
2032
+ openedAt: r.opened_at,
2033
+ closedAt: r.closed_at,
2034
+ openedByName: r.opened_by_name,
2035
+ closedByName: r.closed_by_name,
2036
+ closedByUserId: r.closed_by_user_id,
2037
+ openingBalance: Number(r.opening_balance) || 0,
2038
+ closingBalance: Number(r.closing_balance) || 0,
2039
+ difference: Number(r.difference) || 0,
2040
+ reviewStatus: r.review_status,
2041
+ reviewedAt: r.reviewed_at,
2042
+ reviewerName: r.reviewer_name,
2043
+ reviewNotes: r.review_notes
2044
+ }));
2045
+ },
2046
+ async reviewCashSession(sessionId, verdict, notes) {
2047
+ const { pub } = getClients();
2048
+ const { error } = await pub.rpc("financial_cash_session_review", {
2049
+ p_session_id: sessionId,
2050
+ p_verdict: verdict,
2051
+ p_notes: notes ?? null
2052
+ });
2053
+ if (error) throw error;
2054
+ },
2055
+ async getGoalYear(unitId, year) {
2056
+ const { pub } = getClients();
2057
+ const { data, error } = await pub.from("v_financial_goal_year").select("*").eq("unit_id", unitId).eq("year", year).order("month");
2058
+ if (error) throw error;
2059
+ return (data ?? []).map((r) => ({
2060
+ goalId: r.goal_id,
2061
+ unitId: r.unit_id,
2062
+ year: Number(r.year),
2063
+ month: Number(r.month),
2064
+ targetAmount: Number(r.target_amount) || 0,
2065
+ superAmount: Number(r.super_amount) || 0,
2066
+ hyperAmount: Number(r.hyper_amount) || 0,
2067
+ achievedAmount: Number(r.achieved_amount) || 0,
2068
+ targetPercent: Number(r.target_percent) || 0,
2069
+ superPercent: Number(r.super_percent) || 0,
2070
+ hyperPercent: Number(r.hyper_percent) || 0,
2071
+ notes: r.notes
2072
+ }));
2073
+ },
2074
+ async saveGoal(input) {
2075
+ const { pub } = getClients();
2076
+ const row = {
2077
+ tenant_id: getActiveTenantId(),
2078
+ unit_id: input.unitId,
2079
+ year: input.year,
2080
+ month: input.month,
2081
+ target_amount: input.targetAmount,
2082
+ super_amount: input.superAmount,
2083
+ hyper_amount: input.hyperAmount,
2084
+ notes: input.notes ?? null
2085
+ };
2086
+ const { error } = await pub.from("plg_financial_goals").upsert(row, { onConflict: "tenant_id,unit_id,year,month" });
2087
+ if (error) throw error;
2088
+ },
2089
+ async deleteGoal(id) {
2090
+ const { pub } = getClients();
2091
+ const { error } = await pub.from("plg_financial_goals").delete().eq("id", id);
2092
+ if (error) throw error;
2093
+ },
2094
+ // --- Vales-presente (027) ---
2095
+ // O cartão mora na loja; o Financeiro entra por RPC própria, que filtra por
2096
+ // tenant e permissão. Ler a tabela direto daqui daria de brinde a loja
2097
+ // inteira a quem só tem financial.read.
2098
+ async giftCardsEnabled() {
2099
+ const { pub } = getClients();
2100
+ const { data, error } = await pub.rpc("financial_gift_cards_enabled");
2101
+ if (error) throw error;
2102
+ return data === true;
2103
+ },
2104
+ async getGiftCards(query) {
2105
+ const { pub } = getClients();
2106
+ const { data, error } = await pub.rpc("financial_gift_cards", {
2107
+ p_search: query?.search?.trim() || null,
2108
+ p_state: query?.state ?? null,
2109
+ p_min: query?.minValue ?? null,
2110
+ p_max: query?.maxValue ?? null,
2111
+ p_from: query?.issuedFrom || null,
2112
+ p_to: query?.issuedTo || null,
2113
+ p_limit: 500
2114
+ });
2115
+ if (error) throw error;
2116
+ return (data ?? []).map((r) => ({
2117
+ id: r.id,
2118
+ code: r.code,
2119
+ faceValue: Number(r.face_value) || 0,
2120
+ // NULL atravessa como NULL: é "nunca ativado", e virar 0 aqui apagaria
2121
+ // a única diferença que esta tela existe para mostrar.
2122
+ balance: r.balance == null ? null : Number(r.balance),
2123
+ state: r.state,
2124
+ currency: r.currency,
2125
+ issuedAt: r.issued_at,
2126
+ expiresAt: r.expires_at,
2127
+ activatedAt: r.activated_at,
2128
+ note: r.note,
2129
+ customerEmail: r.customer_email
2130
+ }));
2131
+ },
2132
+ async findGiftCardByCode(raw) {
2133
+ const code = raw.replace(/[\s.-]/g, "").toUpperCase();
2134
+ if (!code) return null;
2135
+ const { pub } = getClients();
2136
+ let { data, error } = await pub.rpc("financial_gift_card_by_code", { p_code: code });
2137
+ if (error?.code === "PGRST202") {
2138
+ ({ data, error } = await pub.rpc("financial_gift_cards", { p_search: code, p_limit: 5 }));
2139
+ }
2140
+ if (error) throw error;
2141
+ const row = (data ?? []).find((r) => String(r.code).toUpperCase() === code);
2142
+ if (!row) return null;
2143
+ return {
2144
+ id: row.id,
2145
+ code: row.code,
2146
+ faceValue: Number(row.face_value) || 0,
2147
+ balance: row.balance == null ? null : Number(row.balance),
2148
+ state: row.state,
2149
+ currency: row.currency,
2150
+ issuedAt: row.issued_at,
2151
+ expiresAt: row.expires_at,
2152
+ activatedAt: row.activated_at,
2153
+ note: row.note,
2154
+ customerEmail: row.customer_email
2155
+ };
2156
+ },
2157
+ async issueGiftCards(input) {
2158
+ const { pub } = getClients();
2159
+ const { data, error } = await pub.rpc("financial_issue_gift_cards", {
2160
+ p_quantity: input.quantity,
2161
+ p_amount: input.unitAmount,
2162
+ p_expires_at: input.expiresAt || null,
2163
+ p_note: input.note || null
2164
+ });
2165
+ if (error) throw error;
2166
+ return (data ?? []).map((r) => r.code);
2167
+ },
2168
+ async activateGiftCard(id) {
2169
+ const { pub } = getClients();
2170
+ const { error } = await pub.rpc("financial_activate_gift_card", { p_id: id });
2171
+ if (error) throw error;
2172
+ },
2173
+ async getSummary(dateRange) {
2174
+ const { pub } = getClients();
2175
+ const n = (v) => Number(v) || 0;
2176
+ const args = { p_from: dateRange?.from ?? null, p_to: dateRange?.to ?? null };
2177
+ let split = await pub.rpc("financial_summary_by_currency", args);
2178
+ if (!split.error && Array.isArray(split.data)) {
2179
+ const missing = split.data.filter((r) => r.fx_rate === null || r.fx_rate === void 0);
2180
+ if (missing.length > 0) {
2181
+ const house = String(missing[0].house_currency);
2182
+ const tenantId = getActiveTenantId();
2183
+ if (tenantId && await ensureReferenceRates(pub, tenantId, house, missing.map((r) => String(r.currency)))) {
2184
+ const again = await pub.rpc("financial_summary_by_currency", args);
2185
+ if (!again.error && Array.isArray(again.data)) split = again;
2186
+ }
2187
+ }
2188
+ const total = {
2189
+ totalBalance: 0,
2190
+ totalReceivable: 0,
2191
+ totalPayable: 0,
2192
+ monthlyInflow: 0,
2193
+ monthlyOutflow: 0,
2194
+ overdueReceivableCount: 0,
2195
+ overdueReceivableAmount: 0,
2196
+ overduePayableCount: 0,
2197
+ overduePayableAmount: 0
2198
+ };
2199
+ const excluded = [];
2200
+ for (const r of split.data) {
2201
+ total.overdueReceivableCount += n(r.overdue_receivable_count);
2202
+ total.overduePayableCount += n(r.overdue_payable_count);
2203
+ if (r.fx_rate === null || r.fx_rate === void 0) {
2204
+ const left = {
2205
+ currency: String(r.currency),
2206
+ balance: n(r.total_balance),
2207
+ receivable: n(r.total_receivable),
2208
+ payable: n(r.total_payable),
2209
+ inflow: n(r.monthly_inflow),
2210
+ outflow: n(r.monthly_outflow)
2211
+ };
2212
+ if (left.balance || left.receivable || left.payable || left.inflow || left.outflow) excluded.push(left);
2213
+ continue;
2214
+ }
2215
+ const rate = n(r.fx_rate);
2216
+ total.totalBalance += n(r.total_balance) * rate;
2217
+ total.totalReceivable += n(r.total_receivable) * rate;
2218
+ total.totalPayable += n(r.total_payable) * rate;
2219
+ total.monthlyInflow += n(r.monthly_inflow) * rate;
2220
+ total.monthlyOutflow += n(r.monthly_outflow) * rate;
2221
+ total.overdueReceivableAmount += n(r.overdue_receivable_amount) * rate;
2222
+ total.overduePayableAmount += n(r.overdue_payable_amount) * rate;
2223
+ }
2224
+ const cents = (v) => Math.round(v * 100) / 100;
2225
+ return {
2226
+ totalBalance: cents(total.totalBalance),
2227
+ totalReceivable: cents(total.totalReceivable),
2228
+ totalPayable: cents(total.totalPayable),
2229
+ monthlyInflow: cents(total.monthlyInflow),
2230
+ monthlyOutflow: cents(total.monthlyOutflow),
2231
+ overdueReceivableCount: total.overdueReceivableCount,
2232
+ overdueReceivableAmount: cents(total.overdueReceivableAmount),
2233
+ overduePayableCount: total.overduePayableCount,
2234
+ overduePayableAmount: cents(total.overduePayableAmount),
2235
+ ...excluded.length ? { excluded } : {}
2236
+ };
2237
+ }
2238
+ const { data, error } = await pub.rpc("financial_summary", args);
2239
+ if (error) throw error;
2240
+ const row = (Array.isArray(data) ? data[0] : data) ?? {};
2241
+ return {
2242
+ totalBalance: n(row.total_balance),
2243
+ totalReceivable: n(row.total_receivable),
2244
+ totalPayable: n(row.total_payable),
2245
+ monthlyInflow: n(row.monthly_inflow),
2246
+ monthlyOutflow: n(row.monthly_outflow),
2247
+ overdueReceivableCount: n(row.overdue_receivable_count),
2248
+ overdueReceivableAmount: n(row.overdue_receivable_amount),
2249
+ overduePayableCount: n(row.overdue_payable_count),
2250
+ overduePayableAmount: n(row.overdue_payable_amount)
2251
+ };
2252
+ },
2253
+ // --- Conciliação Bancária ---
2254
+ //
2255
+ // A linha do extrato mora em `plg_financial_bank_statement_lines` e NÃO em
2256
+ // `plg_financial_movements`. A migration 112 conta a história inteira; o
2257
+ // resumo é que uma linha recém-chegada não tem as duas pernas que
2258
+ // `plg_financial_movements_legs_check` exige, porque ainda não se sabe
2259
+ // contra o quê ela lança — e era por isso que todo INSERT do import antigo
2260
+ // era recusado pelo banco enquanto os testes do mock passavam.
2261
+ async importBankTransactions(input) {
2262
+ const { pub } = getClients();
2263
+ const tenantId = getActiveTenantId();
2264
+ const rows = input.lines.map((line) => ({
2265
+ tenant_id: tenantId,
2266
+ bank_account_id: line.bankAccountId ?? input.bankAccountId ?? null,
2267
+ external_id: line.externalId,
2268
+ external_source: line.externalSource,
2269
+ posted_on: line.date,
2270
+ effective_at: line.effectiveAt ?? null,
2271
+ direction: line.direction,
2272
+ amount: Math.abs(line.amount),
2273
+ balance_after: line.balanceAfter ?? null,
2274
+ description: line.description,
2275
+ counterparty_name: line.counterpartyName ?? null,
2276
+ counterparty_document: line.counterpartyDocument ?? null,
2277
+ document: line.document ?? null,
2278
+ code: line.code ?? null,
2279
+ category: line.category ?? null,
2280
+ transaction_type: line.transactionType ?? null,
2281
+ raw_payload: line.rawPayload ?? null
2282
+ }));
2283
+ if (rows.length === 0) return { imported: 0, duplicates: 0 };
2284
+ const { data, error } = await pub.from(T.statementLines).upsert(rows, { onConflict: "tenant_id,external_source,external_id", ignoreDuplicates: true }).select("id");
2285
+ if (error) throw error;
2286
+ const imported = (data ?? []).length;
2287
+ return { imported, duplicates: rows.length - imported };
2288
+ },
2289
+ // A lista da tela. Ela mostra o extrato INTEIRO do período — pendente e
2290
+ // conciliado — porque conferir o que se conciliou ontem é parte do
2291
+ // trabalho, e uma tela que só mostra pendência esconde o próprio resultado.
2292
+ async getStatementLines(query) {
2293
+ const { pub } = getClients();
2294
+ const { page, pageSize } = statementLinesPage(query);
2295
+ const from = (page - 1) * pageSize;
2296
+ let qb = pub.from(T.statementLines).select("*", { count: "exact" }).eq("provider_deleted", false);
2297
+ if (query?.bankAccountId) qb = qb.eq("bank_account_id", query.bankAccountId);
2298
+ if (query?.dateRange) qb = qb.gte("posted_on", query.dateRange.from).lte("posted_on", query.dateRange.to);
2299
+ if (query?.direction) qb = qb.eq("direction", query.direction);
2300
+ if (query?.status === "reconciled") qb = qb.not("reconciled_at", "is", null);
2301
+ else if (query?.status === "pending") qb = qb.is("reconciled_at", null);
2302
+ const term = (query?.search ?? "").trim();
2303
+ if (term) {
2304
+ const safe = term.replace(/[,()*%]/g, " ");
2305
+ qb = qb.or([
2306
+ `description.ilike.%${safe}%`,
2307
+ `counterparty_name.ilike.%${safe}%`,
2308
+ `counterparty_document.ilike.%${safe}%`,
2309
+ `document.ilike.%${safe}%`,
2310
+ `code.ilike.%${safe}%`,
2311
+ `category.ilike.%${safe}%`,
2312
+ `transaction_type.ilike.%${safe}%`,
2313
+ `external_id.ilike.%${safe}%`
2314
+ ].join(","));
2315
+ }
2316
+ if (query?.hideInvestments) qb = qb.or(`code.is.null,code.not.in.(${INVESTMENT_CODES.join(",")})`);
2317
+ const { data, error, count } = await qb.order("posted_on", { ascending: false }).order("id", { ascending: false }).range(from, from + pageSize - 1);
2318
+ if (error) throw error;
2319
+ const rows = (data ?? []).map(toStatementLine);
2320
+ await anexaContraparte(rows);
2321
+ return { rows, total: count ?? 0, page, pageSize, ...pageTotals(rows) };
2322
+ },
2323
+ async getUnreconciled(query) {
2324
+ const { pub } = getClients();
2325
+ let qb = pub.from(T.statementLines).select("*").is("reconciled_at", null).eq("provider_deleted", false);
2326
+ if (query?.bankAccountId) qb = qb.eq("bank_account_id", query.bankAccountId);
2327
+ if (query?.dateRange) qb = qb.gte("posted_on", query.dateRange.from).lte("posted_on", query.dateRange.to);
2328
+ const { data, error } = await qb.order("posted_on", { ascending: false });
2329
+ if (error) throw error;
2330
+ return (data ?? []).map(toStatementLine);
2331
+ },
2332
+ async suggestReconciliation(statementLineId) {
2333
+ const { core, pub } = getClients();
2334
+ const { data: line } = await pub.from(T.statementLines).select("*").eq("id", statementLineId).single();
2335
+ if (!line) return [];
2336
+ const amount = Number(line.amount) || 0;
2337
+ const lineDate = line.posted_on;
2338
+ const tol = candidateTolerance(amount);
2339
+ const { data, error } = await pub.from(T.movements).select("*").is("external_source", null).is("reconciled_at", null).eq("direction", line.direction).gte("amount", round2(amount - tol)).lte("amount", round2(amount + tol)).limit(25);
2340
+ if (error) throw error;
2341
+ const candidates = (data ?? []).map((r) => {
2342
+ const movement = snakeToCamel(r);
2343
+ return { movement, score: candidateScore({ amount, date: lineDate }, movement) };
2344
+ }).sort((a, b) => b.score - a.score);
2345
+ const invoiceIds = [...new Set(candidates.map((c) => c.movement.invoiceId).filter(Boolean))];
2346
+ if (invoiceIds.length) {
2347
+ const { data: orders } = await core.from("orders").select("id, reference_number, metadata").in("id", invoiceIds);
2348
+ const byId = new Map((orders ?? []).map((o) => [o.id, o]));
2349
+ for (const c of candidates) {
2350
+ const o = c.movement.invoiceId ? byId.get(c.movement.invoiceId) : void 0;
2351
+ if (o) c.invoice = { id: o.id, contactName: o.metadata?.contactName, fiscalNumber: o.reference_number };
2352
+ }
2353
+ }
2354
+ const linha = { id: line.id, amount, date: lineDate, direction: line.direction, externalSource: line.external_source };
2355
+ const abertas = await searchBillTargets(linha, {
2356
+ status: "open",
2357
+ amountMin: round2(amount - tol),
2358
+ amountMax: round2(amount + tol),
2359
+ pageSize: 25
2360
+ }).catch(() => ({ data: [], hasMore: false }));
2361
+ return [...candidates, ...abertas.data].sort((a, b) => b.score - a.score);
2362
+ },
2363
+ async searchReconciliationTargets(statementLineId, query) {
2364
+ return searchBillTargets(await readStatementLine(statementLineId), query);
2365
+ },
2366
+ // ------------------------------------------------------------------
2367
+ // A SUGESTÃO QUE CHEGA SOZINHA, para a página inteira.
2368
+ //
2369
+ // DUAS perguntas ao banco para cinquenta linhas, e não duas por linha:
2370
+ //
2371
+ // 1. os lançamentos nativos em aberto cujo valor cabe em ALGUMA das
2372
+ // faixas de ±2% da página (um `or` só, com uma faixa por valor
2373
+ // distinto — o PostgREST aceita um `or` por consulta);
2374
+ // 2. o nome de quem, para os documentos desses lançamentos.
2375
+ //
2376
+ // O cruzamento é em memória (ver match-statement.ts): valor, nome e data,
2377
+ // e nenhum lançamento oferecido a duas linhas.
2378
+ // ------------------------------------------------------------------
2379
+ async syncStatements(input) {
2380
+ const { pub } = getClients();
2381
+ const { data, error } = await pub.functions.invoke("financial-statement-sync", {
2382
+ body: { accountId: input?.bankAccountId, tenantId: getActiveTenantId() ?? null }
2383
+ });
2384
+ if (error) {
2385
+ let body = null;
2386
+ try {
2387
+ body = await error?.context?.json?.();
2388
+ } catch {
2389
+ }
2390
+ throw new Error(body?.error || error.message || "N\xE3o foi poss\xEDvel buscar o extrato do banco.");
2391
+ }
2392
+ const results = data?.results ?? [];
2393
+ return {
2394
+ imported: Number(data?.imported ?? 0),
2395
+ failed: Number(data?.failed ?? 0),
2396
+ errors: results.map((r) => r.error).filter((e) => Boolean(e))
2397
+ };
2398
+ },
2399
+ async suggestReconciliationBatch(lines) {
2400
+ const { core, pub } = getClients();
2401
+ const abertas = lines.filter((line) => !line.reconciledAt && (Number(line.amount) || 0) > 0);
2402
+ if (abertas.length === 0) return {};
2403
+ const faixas = [...new Set(abertas.map((line) => round2(line.amount)))].map((valor) => {
2404
+ const tol = Math.max(0.01, round2(valor * NAME_AMOUNT_TOLERANCE));
2405
+ return `and(amount.gte.${round2(valor - tol)},amount.lte.${round2(valor + tol)})`;
2406
+ });
2407
+ const direcoes = [...new Set(abertas.map((line) => line.direction))];
2408
+ const { data, error } = await pub.from(T.movements).select("*").is("external_source", null).is("reconciled_at", null).in("direction", direcoes).or(faixas.join(",")).limit(500);
2409
+ if (error) throw error;
2410
+ const movimentos = (data ?? []).map((r) => snakeToCamel(r));
2411
+ const datas = abertas.map((line) => line.date).filter(Boolean).sort();
2412
+ const contexto = /* @__PURE__ */ new Map();
2413
+ if (datas.length) {
2414
+ const { data: billRows } = await pub.from("v_financial_invoice_lines_due").select("installment_id,invoice_id,direction,number,due_date,description,counterparty_name,installment_number,amount,paid,status").in("direction", direcoes.map((d) => d === "debit" ? "payable" : "receivable")).in("status", ["pending", "partial", "overdue"]).gte("due_date", shiftIsoDate(datas[0], -30)).lte("due_date", shiftIsoDate(datas[datas.length - 1], 30)).or(faixas.join(",")).limit(500);
2415
+ for (const r of billRows ?? []) {
2416
+ movimentos.push({
2417
+ id: r.installment_id,
2418
+ invoiceId: r.invoice_id,
2419
+ direction: r.direction === "payable" ? "debit" : "credit",
2420
+ movementKind: "bill",
2421
+ amount: Number(r.amount) || 0,
2422
+ paidAmount: Number(r.paid) || 0,
2423
+ status: r.status,
2424
+ dueDate: r.due_date,
2425
+ installmentNumber: r.installment_number ?? void 0,
2426
+ notes: r.description ?? void 0
2427
+ });
2428
+ contexto.set(r.installment_id, {
2429
+ contactName: r.counterparty_name ?? void 0,
2430
+ invoice: { id: r.invoice_id, contactName: r.counterparty_name ?? void 0, fiscalNumber: r.number ?? void 0 }
2431
+ });
2432
+ }
2433
+ }
2434
+ if (movimentos.length === 0) return {};
2435
+ const invoiceIds = [...new Set(movimentos.filter((m) => !contexto.has(m.id)).map((m) => m.invoiceId).filter(Boolean))];
2436
+ if (invoiceIds.length > 0) {
2437
+ const { data: orders } = await core.from("orders").select("id, reference_number, metadata").in("id", invoiceIds);
2438
+ const byId = new Map((orders ?? []).map((o) => [o.id, o]));
2439
+ for (const movimento of movimentos) {
2440
+ const order = movimento.invoiceId ? byId.get(movimento.invoiceId) : void 0;
2441
+ if (!order) continue;
2442
+ const contactName = order.metadata?.contactName;
2443
+ contexto.set(movimento.id, {
2444
+ contactName,
2445
+ invoice: { id: order.id, contactName, fiscalNumber: order.reference_number }
2446
+ });
2447
+ }
2448
+ }
2449
+ return matchStatementLines(abertas, movimentos, contexto);
2450
+ },
2451
+ async reconcileMovement(input) {
2452
+ if (input.mode === "settle" && input.matchedMovementId) {
2453
+ const line = await readStatementLine(input.bankMovementId);
2454
+ const { data: alvo } = await getClients().pub.from(T.bills).select("amount, paid").eq("installment_id", input.matchedMovementId).maybeSingle();
2455
+ if (!alvo) throw new Error("Lan\xE7amento n\xE3o encontrado");
2456
+ await runReconcileLink(
2457
+ reconcileFacade(),
2458
+ line,
2459
+ {
2460
+ movementId: input.matchedMovementId,
2461
+ mode: "settle",
2462
+ openAmount: round2((Number(alvo.amount) || 0) - (Number(alvo.paid) || 0))
2463
+ },
2464
+ { payment: input.payment, adjustments: input.adjustments, partial: input.partial }
2465
+ );
2466
+ return;
2467
+ }
2468
+ await markReconciled(input.bankMovementId, input.matchedMovementId);
2469
+ },
2470
+ // «Marcar como transferência». A conta do extrato é sempre uma das pontas.
2471
+ // SAÍDA no extrato = o dinheiro deixou aquela conta, logo ela é a ORIGEM;
2472
+ // entrada é o inverso. Mesma convenção do V1 e da sangria de caixa —
2473
+ // inverter faz o saldo consolidado andar duas vezes no sentido errado.
2474
+ async reconcileAsTransfer(input) {
2475
+ const line = await readStatementLine(input.bankMovementId);
2476
+ const transfer = await provider.createTransfer(transferForStatementLine(line, input));
2477
+ await provider.reconcileMovement({ bankMovementId: input.bankMovementId, matchedMovementId: statementLegOf(line, transfer) });
2478
+ return transfer;
2479
+ },
2480
+ // «Lançar como conta». A conta nasce classificada, é quitada contra a conta
2481
+ // do extrato, e a linha passa a ser a prova dela. É o caminho que faz o
2482
+ // extrato BATER: sem ele, uma linha sem contrapartida fica marcada como
2483
+ // conciliada e o dinheiro continua fora do livro.
2484
+ async reconcileAsBill(input) {
2485
+ const line = await readStatementLine(input.bankMovementId);
2486
+ const { invoiceId } = await runReconcileAsBill(reconcileFacade(), line, input);
2487
+ const invoice = await provider.getInvoiceById(invoiceId);
2488
+ if (!invoice) throw new Error("A conta n\xE3o foi criada");
2489
+ return invoice;
2490
+ },
2491
+ async unreconcileMovement(statementLineId) {
2492
+ const { pub } = getClients();
2493
+ const { data: line } = await pub.from(T.statementLines).select("matched_movement_id").eq("id", statementLineId).single();
2494
+ await pub.from(T.statementLines).update({ reconciled_at: null, matched_movement_id: null }).eq("id", statementLineId);
2495
+ if (line?.matched_movement_id) {
2496
+ await pub.from(T.movements).update({ reconciled_at: null }).eq("id", line.matched_movement_id);
2497
+ }
2498
+ },
2499
+ // --- Notas fiscais (NF-e / NFC-e / NFS-e) ---
2500
+ async getNotas(query) {
2501
+ const { pub } = getClients();
2502
+ let qb = pub.from(T.notas).select("*", { count: "exact" });
2503
+ if (query.status) {
2504
+ const statuses = Array.isArray(query.status) ? query.status : [query.status];
2505
+ qb = qb.in("status", statuses);
2506
+ }
2507
+ if (query.model) qb = qb.eq("model", query.model);
2508
+ if (query.orderId) qb = qb.eq("order_id", query.orderId);
2509
+ if (query.search) qb = qb.or(`number.ilike.%${query.search}%,contact_name.ilike.%${query.search}%`);
2510
+ const page = query.page ?? 1;
2511
+ const pageSize = query.pageSize ?? 50;
2512
+ qb = qb.range((page - 1) * pageSize, page * pageSize - 1).order("created_at", { ascending: false });
2513
+ const { data, count } = await qb;
2514
+ return { data: (data ?? []).map((r) => snakeToCamel(r)), total: count ?? 0 };
2515
+ },
2516
+ async getNotaById(id) {
2517
+ const { pub } = getClients();
2518
+ const { data, error } = await pub.from(T.notas).select("*").eq("id", id).maybeSingle();
2519
+ if (error) throw error;
2520
+ return data ? snakeToCamel(data) : null;
2521
+ },
2522
+ async createNota(input) {
2523
+ const { pub } = getClients();
2524
+ const row = {
2525
+ tenant_id: getActiveTenantId(),
2526
+ model: input.model ?? "nfe",
2527
+ status: "draft",
2528
+ series: input.series ?? null,
2529
+ order_id: input.orderId ?? null,
2530
+ contact_id: input.contactId ?? null,
2531
+ contact_name: input.contactName ?? null,
2532
+ total_amount: input.totalAmount,
2533
+ observations: input.observations ?? null
2534
+ };
2535
+ const { data, error } = await pub.from(T.notas).insert(row).select().single();
2536
+ if (error) throw error;
2537
+ return snakeToCamel(data);
2538
+ },
2539
+ async updateNota(id, patch) {
2540
+ const { pub } = getClients();
2541
+ const { data, error } = await pub.from(T.notas).update(camelToSnake(patch)).eq("id", id).select().single();
2542
+ if (error) throw error;
2543
+ return snakeToCamel(data);
2544
+ },
2545
+ // Emission seam: no SEFAZ integration yet, so assign a número/série locally
2546
+ // and mark it issued. An external emissor replaces this body (or overrides
2547
+ // issueNota on a wrapping provider) without any UI change.
2548
+ async issueNota(id) {
2549
+ const { pub } = getClients();
2550
+ const { data: current } = await pub.from(T.notas).select("series").eq("id", id).single();
2551
+ const { count } = await pub.from(T.notas).select("id", { count: "exact", head: true }).eq("status", "issued");
2552
+ const patch = {
2553
+ status: "issued",
2554
+ number: String((count ?? 0) + 1).padStart(6, "0"),
2555
+ series: current?.series ?? "1",
2556
+ issued_at: (/* @__PURE__ */ new Date()).toISOString(),
2557
+ emissor: "manual"
2558
+ };
2559
+ const { data, error } = await pub.from(T.notas).update(patch).eq("id", id).select().single();
2560
+ if (error) throw error;
2561
+ return snakeToCamel(data);
2562
+ },
2563
+ async cancelNota(id, reason) {
2564
+ const { pub } = getClients();
2565
+ const { data, error } = await pub.from(T.notas).update({ status: "cancelled", error_message: reason ?? null }).eq("id", id).select().single();
2566
+ if (error) throw error;
2567
+ return snakeToCamel(data);
2568
+ },
2569
+ // --- Remessa CNAB 240 (080) ---
2570
+ /**
2571
+ * The Bacen registry, by COMPE code or by name.
2572
+ *
2573
+ * A bank is the country's, not the tenant's, so `public.banks` is global and
2574
+ * read-only. A pool where migration 167 has not run yet answers with an
2575
+ * empty list, and the caller falls back to the static catalogue — a missing
2576
+ * table costs a longer list, never the screen.
2577
+ */
2578
+ async searchBanks(query, limit = 12) {
2579
+ const term = query.trim();
2580
+ const { pub } = getClients();
2581
+ let qb = pub.from("banks").select("ispb,compe,name,legal_name,brand_color,logo_url").eq("is_active", true);
2582
+ if (term) {
2583
+ const digits = term.replace(/\D/g, "");
2584
+ qb = digits.length >= 2 && digits.length <= 3 ? qb.like("compe", `${digits}%`) : qb.or(`name.ilike.%${term}%,legal_name.ilike.%${term}%`);
2585
+ }
2586
+ const { data, error } = await qb.order("compe", { nullsFirst: false }).limit(limit);
2587
+ if (error) return [];
2588
+ return (data ?? []).map((row) => ({
2589
+ ispb: row.ispb,
2590
+ compe: row.compe ?? void 0,
2591
+ name: row.name,
2592
+ legalName: row.legal_name ?? void 0,
2593
+ brandColor: row.brand_color ?? void 0,
2594
+ logoUrl: row.logo_url ?? void 0
2595
+ }));
2596
+ },
2597
+ /**
2598
+ * Who gets paid, who pays, and which file number is next.
2599
+ *
2600
+ * The payee's document lives on `people`, the account on `payee_accounts`
2601
+ * and the address on `addresses` — three tables because each answers a
2602
+ * different question, and the CNPJ of the paying company is a fourth. The
2603
+ * wizard asks once and gets all of it, so step 3 can show a form with the
2604
+ * blanks already filled instead of a sentence saying data is missing.
2605
+ */
2606
+ async getCnabContext({ contactIds, bankAccountId, unitId }) {
2607
+ const { pub } = getClients();
2608
+ const ids = [...new Set(contactIds.filter(Boolean))];
2609
+ const [peopleRes, accountsRes, addressRes, companyRes, unitRes, seqRes] = await Promise.all([
2610
+ ids.length ? pub.from("people").select("id,name,document_number").in("id", ids) : Promise.resolve({ data: [], error: null }),
2611
+ ids.length ? pub.from(T.payeeAccounts).select("*").eq("owner_type", "person").in("owner_id", ids).order("is_default", { ascending: false }).order("created_at") : Promise.resolve({ data: [], error: null }),
2612
+ ids.length ? pub.from("addresses").select("*").eq("owner_type", "person").in("owner_id", ids) : Promise.resolve({ data: [], error: null }),
2613
+ // As empresas do tenant. São unidades de linhas, então vêm todas e a
2614
+ // escolha é feita aqui — a da unidade da conta, senão a sem unidade,
2615
+ // senão a única que existe.
2616
+ pub.from("company_legal_entities").select("id,unit_id,kind,legal_name,trade_name,document_number").eq("is_active", true).limit(20),
2617
+ // O nome da unidade é o que a tela escreve enquanto não há empresa
2618
+ // cadastrada — melhor que um campo em branco pedindo razão social.
2619
+ unitId ? pub.from(T.unitCompanies).select("unit_id,unit_name").eq("unit_id", unitId).maybeSingle() : Promise.resolve({ data: null, error: null }),
2620
+ pub.from(T.cnabBatches).select("sequence_number").eq("bank_account_id", bankAccountId).order("sequence_number", { ascending: false }).limit(1).maybeSingle()
2621
+ ]);
2622
+ if (peopleRes.error) throw peopleRes.error;
2623
+ if (accountsRes.error) throw accountsRes.error;
2624
+ const accountOf = /* @__PURE__ */ new Map();
2625
+ for (const row of accountsRes.data ?? []) {
2626
+ if (!accountOf.has(row.owner_id)) accountOf.set(row.owner_id, row);
2627
+ }
2628
+ const addressOf = /* @__PURE__ */ new Map();
2629
+ for (const row of addressRes.data ?? []) {
2630
+ if (!addressOf.has(row.owner_id)) addressOf.set(row.owner_id, row);
2631
+ }
2632
+ const payees = (peopleRes.data ?? []).map((person) => {
2633
+ const account = accountOf.get(person.id);
2634
+ const address = addressOf.get(person.id);
2635
+ return {
2636
+ contactId: person.id,
2637
+ name: person.name ?? "",
2638
+ document: person.document_number ?? "",
2639
+ payeeAccountId: account?.id,
2640
+ bankCode: account?.bank_code ?? "",
2641
+ bankName: account?.bank_name ?? "",
2642
+ agencyNumber: account?.agency_number ?? "",
2643
+ agencyDigit: account?.agency_digit ?? "",
2644
+ accountNumber: account?.account_number ?? "",
2645
+ accountDigit: account?.account_digit ?? "",
2646
+ accountType: account?.account_type ?? "",
2647
+ pixKey: account?.pix_key ?? "",
2648
+ pixKeyType: account?.pix_key_type ?? "",
2649
+ street: address?.street ?? "",
2650
+ number: address?.number ?? "",
2651
+ complement: address?.complement ?? "",
2652
+ district: address?.district ?? "",
2653
+ city: address?.city ?? "",
2654
+ state: address?.state ?? "",
2655
+ postalCode: address?.postal_code ?? ""
2656
+ };
2657
+ });
2658
+ const companies = companyRes.data ?? [];
2659
+ const companyRow = companies.find((row) => unitId && row.unit_id === unitId) ?? companies.find((row) => !row.unit_id) ?? companies[0];
2660
+ const unitName = unitRes.data?.unit_name ?? "";
2661
+ const company = {
2662
+ id: companyRow?.id ?? void 0,
2663
+ unitId,
2664
+ name: companyRow?.trade_name || companyRow?.legal_name || unitName,
2665
+ document: companyRow?.document_number || ""
2666
+ };
2667
+ return {
2668
+ company,
2669
+ nextSequence: Number(seqRes.data?.sequence_number ?? 0) + 1,
2670
+ payees
2671
+ };
2672
+ },
2673
+ /**
2674
+ * A empresa pagadora, cadastrada de dentro da remessa.
2675
+ *
2676
+ * `company_legal_entities` nasceu sem tela: nenhuma superfície do SDK
2677
+ * escreve nela, e o resultado é um pool com treze unidades e zero empresas.
2678
+ * Como o CNPJ é obrigatório no cabeçalho do CNAB, é aqui que a pessoa o
2679
+ * digita — e gravá-lo é o que faz a PRÓXIMA remessa já vir preenchida.
2680
+ *
2681
+ * `unit_id` na linha é o que recorta a empresa por unidade. O elo inverso
2682
+ * (`app.units.company_legal_entity_id`) fica para quem tiver a RPC: o schema
2683
+ * `app` não é alcançável por aqui, e a leitura já resolve pelos dois lados.
2684
+ */
2685
+ async saveCompany(input) {
2686
+ const { pub } = getClients();
2687
+ const tenantId = getActiveTenantId();
2688
+ const digits = (input.document || "").replace(/\D/g, "");
2689
+ const name = input.name.trim();
2690
+ if (!name) throw new Error("A empresa precisa de um nome.");
2691
+ if (digits.length !== 11 && digits.length !== 14) {
2692
+ throw new Error("CNPJ com 14 d\xEDgitos ou CPF com 11 \u2014 o cabe\xE7alho do CNAB n\xE3o aceita outro.");
2693
+ }
2694
+ const row = {
2695
+ kind: digits.length === 14 ? "company" : "individual",
2696
+ legal_name: name,
2697
+ trade_name: name,
2698
+ document_number: digits
2699
+ };
2700
+ if (input.id) {
2701
+ const { error: error2 } = await pub.from("company_legal_entities").update(row).eq("id", input.id);
2702
+ if (error2) throw error2;
2703
+ return { id: input.id };
2704
+ }
2705
+ const { data, error } = await pub.from("company_legal_entities").insert({ ...row, tenant_id: tenantId, unit_id: input.unitId ?? null, is_active: true }).select("id").single();
2706
+ if (error) throw error;
2707
+ return { id: data.id };
2708
+ },
2709
+ /**
2710
+ * What the wizard collected goes back to the cadastro, not to the file only.
2711
+ * Otherwise the same CPF is typed again on the next remittance, and the two
2712
+ * typings disagree.
2713
+ */
2714
+ async saveCnabPayees(payees) {
2715
+ const { pub } = getClients();
2716
+ const tenantId = getActiveTenantId();
2717
+ const text = (v) => {
2718
+ const s = (v ?? "").trim();
2719
+ return s === "" ? null : s;
2720
+ };
2721
+ for (const payee of payees) {
2722
+ if (text(payee.document)) {
2723
+ const { error: error2 } = await pub.from("people").update({ document_number: text(payee.document) }).eq("id", payee.contactId);
2724
+ if (error2) throw error2;
2725
+ }
2726
+ const row = {
2727
+ bank_code: text(payee.bankCode),
2728
+ bank_name: text(payee.bankName),
2729
+ agency_number: text(payee.agencyNumber),
2730
+ agency_digit: text(payee.agencyDigit),
2731
+ account_number: text(payee.accountNumber),
2732
+ account_digit: text(payee.accountDigit),
2733
+ account_type: text(payee.accountType),
2734
+ pix_key: text(payee.pixKey),
2735
+ pix_key_type: text(payee.pixKeyType)
2736
+ };
2737
+ if (!row.account_number && !row.pix_key) continue;
2738
+ let targetId = payee.payeeAccountId;
2739
+ if (!targetId) {
2740
+ const { data: existing } = await pub.from(T.payeeAccounts).select("id").eq("owner_type", "person").eq("owner_id", payee.contactId).order("is_default", { ascending: false }).order("created_at").limit(1).maybeSingle();
2741
+ targetId = existing?.id;
2742
+ }
2743
+ const { data: written, error } = targetId ? await pub.from(T.payeeAccounts).update(row).eq("id", targetId).select("id") : await pub.from(T.payeeAccounts).insert({
2744
+ ...row,
2745
+ tenant_id: tenantId,
2746
+ owner_type: "person",
2747
+ owner_id: payee.contactId,
2748
+ label: "Principal",
2749
+ is_default: true
2750
+ }).select("id");
2751
+ if (error) throw error;
2752
+ if (!written?.length) throw new Error("payee account was not saved (no financial edit permission?)");
2753
+ }
2754
+ },
2755
+ /**
2756
+ * The file that already left for the bank. `sequence_number` is the bank's
2757
+ * count, not the file's — two remittances carrying the same one is a
2758
+ * rejected remittance, so it is written with the batch and never derived
2759
+ * again.
2760
+ */
2761
+ async recordCnabBatch(input) {
2762
+ const { pub } = getClients();
2763
+ const tenantId = getActiveTenantId();
2764
+ const { data, error } = await pub.from(T.cnabBatches).insert({
2765
+ tenant_id: tenantId,
2766
+ unit_id: input.unitId ?? null,
2767
+ bank_account_id: input.bankAccountId,
2768
+ layout: input.layout,
2769
+ sequence_number: input.sequenceNumber,
2770
+ total_amount: input.totalAmount,
2771
+ items_count: input.items.length,
2772
+ file_name: input.fileName,
2773
+ // The file's home is `public.documents` through `document_id`; until the
2774
+ // upload path exists it rides in metadata, which keeps a generated
2775
+ // remittance downloadable instead of gone.
2776
+ metadata: { file_content: input.fileContent }
2777
+ }).select("id").single();
2778
+ if (error) throw error;
2779
+ const batchId = data.id;
2780
+ if (input.items.length) {
2781
+ const ids = [...new Set(input.items.map((item) => item.movementId))];
2782
+ const { data: movementRows } = await pub.from(T.movements).select("id").in("id", ids);
2783
+ const isMovement = new Set((movementRows ?? []).map((row) => row.id));
2784
+ const { error: itemsError } = await pub.from(T.cnabItems).insert(
2785
+ input.items.map((item) => ({
2786
+ tenant_id: tenantId,
2787
+ batch_id: batchId,
2788
+ installment_id: item.movementId,
2789
+ movement_id: isMovement.has(item.movementId) ? item.movementId : null,
2790
+ payment_form: item.paymentForm,
2791
+ payment_date: item.paymentDate,
2792
+ amount: item.amount
2793
+ }))
2794
+ );
2795
+ if (itemsError) throw itemsError;
2796
+ }
2797
+ return { id: batchId };
2798
+ }
2799
+ };
2800
+ return provider;
2801
+ }
2802
+
2803
+ export { createSupabaseFinancialProvider };
2804
+ //# sourceMappingURL=chunk-2XWEZXFT.js.map
2805
+ //# sourceMappingURL=chunk-2XWEZXFT.js.map