@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.
- package/LICENSE +21 -0
- package/README.md +34 -2
- package/dist/chunk-2XWEZXFT.js +2805 -0
- package/dist/chunk-2XWEZXFT.js.map +1 -0
- package/dist/chunk-4FWWKKZB.js +49 -0
- package/dist/chunk-4FWWKKZB.js.map +1 -0
- package/dist/chunk-4GXBOEI3.js +50 -0
- package/dist/chunk-4GXBOEI3.js.map +1 -0
- package/dist/chunk-4S5GK776.js +31 -0
- package/dist/chunk-4S5GK776.js.map +1 -0
- package/dist/chunk-56NQHYL7.js +20 -0
- package/dist/chunk-56NQHYL7.js.map +1 -0
- package/dist/chunk-7HISDBUG.js +84 -0
- package/dist/chunk-7HISDBUG.js.map +1 -0
- package/dist/chunk-FNXIAIUZ.js +81 -0
- package/dist/chunk-FNXIAIUZ.js.map +1 -0
- package/dist/chunk-HJPUHTI2.js +77 -0
- package/dist/chunk-HJPUHTI2.js.map +1 -0
- package/dist/chunk-IP5YH4S5.js +14 -0
- package/dist/chunk-IP5YH4S5.js.map +1 -0
- package/dist/chunk-J7BWATQU.js +10 -0
- package/dist/chunk-J7BWATQU.js.map +1 -0
- package/dist/chunk-MCSPCXQK.js +190 -0
- package/dist/chunk-MCSPCXQK.js.map +1 -0
- package/dist/chunk-T7WIM2V7.js +34 -0
- package/dist/chunk-T7WIM2V7.js.map +1 -0
- package/dist/chunk-TZZNVPBY.js +9 -0
- package/dist/chunk-TZZNVPBY.js.map +1 -0
- package/dist/chunk-UERJA7QH.js +893 -0
- package/dist/chunk-UERJA7QH.js.map +1 -0
- package/dist/chunk-XVELDCSL.js +11 -0
- package/dist/chunk-XVELDCSL.js.map +1 -0
- package/dist/chunk-YUNLQYOZ.js +26 -0
- package/dist/chunk-YUNLQYOZ.js.map +1 -0
- package/dist/chunk-ZFUQBYBG.js +124 -0
- package/dist/chunk-ZFUQBYBG.js.map +1 -0
- package/dist/data/fx-auto.d.ts +7 -0
- package/dist/data/fx-auto.d.ts.map +1 -0
- package/dist/data/fx-auto.js +3 -0
- package/dist/data/fx-auto.js.map +1 -0
- package/dist/data/idempotency.d.ts +2 -0
- package/dist/data/idempotency.d.ts.map +1 -0
- package/dist/data/idempotency.js +3 -0
- package/dist/data/idempotency.js.map +1 -0
- package/dist/data/mock.d.ts +29 -0
- package/dist/data/mock.d.ts.map +1 -0
- package/dist/data/mock.js +13 -0
- package/dist/data/mock.js.map +1 -0
- package/dist/data/provider.d.ts +265 -0
- package/dist/data/provider.d.ts.map +1 -0
- package/dist/data/provider.js +3 -0
- package/dist/data/provider.js.map +1 -0
- package/dist/data/reconcile.d.ts +37 -0
- package/dist/data/reconcile.d.ts.map +1 -0
- package/dist/data/reconcile.js +5 -0
- package/dist/data/reconcile.js.map +1 -0
- package/dist/data/supabase.d.ts +3 -0
- package/dist/data/supabase.d.ts.map +1 -0
- package/dist/data/supabase.js +16 -0
- package/dist/data/supabase.js.map +1 -0
- package/dist/data/tables.d.ts +39 -0
- package/dist/data/tables.d.ts.map +1 -0
- package/dist/data/tables.js +3 -0
- package/dist/data/tables.js.map +1 -0
- package/dist/domain/account.d.ts +66 -0
- package/dist/domain/account.d.ts.map +1 -0
- package/dist/domain/account.js +3 -0
- package/dist/domain/account.js.map +1 -0
- package/dist/domain/card.d.ts +33 -0
- package/dist/domain/card.d.ts.map +1 -0
- package/dist/domain/card.js +3 -0
- package/dist/domain/card.js.map +1 -0
- package/dist/domain/cash-session.d.ts +80 -0
- package/dist/domain/cash-session.d.ts.map +1 -0
- package/dist/domain/cash-session.js +4 -0
- package/dist/domain/cash-session.js.map +1 -0
- package/dist/domain/chart-of-accounts.d.ts +57 -0
- package/dist/domain/chart-of-accounts.d.ts.map +1 -0
- package/dist/domain/chart-of-accounts.js +114 -0
- package/dist/domain/chart-of-accounts.js.map +1 -0
- package/dist/domain/client-account.d.ts +28 -0
- package/dist/domain/client-account.d.ts.map +1 -0
- package/dist/domain/client-account.js +3 -0
- package/dist/domain/client-account.js.map +1 -0
- package/dist/domain/cnab240.d.ts +182 -0
- package/dist/domain/cnab240.d.ts.map +1 -0
- package/dist/domain/cnab240.js +265 -0
- package/dist/domain/cnab240.js.map +1 -0
- package/dist/domain/commission.d.ts +142 -0
- package/dist/domain/commission.d.ts.map +1 -0
- package/dist/domain/commission.js +3 -0
- package/dist/domain/commission.js.map +1 -0
- package/dist/domain/counterparty.d.ts +13 -0
- package/dist/domain/counterparty.d.ts.map +1 -0
- package/dist/domain/counterparty.js +48 -0
- package/dist/domain/counterparty.js.map +1 -0
- package/dist/domain/gift-card.d.ts +35 -0
- package/dist/domain/gift-card.d.ts.map +1 -0
- package/dist/domain/gift-card.js +3 -0
- package/dist/domain/gift-card.js.map +1 -0
- package/dist/domain/goal.d.ts +26 -0
- package/dist/domain/goal.d.ts.map +1 -0
- package/dist/domain/goal.js +3 -0
- package/dist/domain/goal.js.map +1 -0
- package/dist/domain/invoice.d.ts +228 -0
- package/dist/domain/invoice.d.ts.map +1 -0
- package/dist/domain/invoice.js +4 -0
- package/dist/domain/invoice.js.map +1 -0
- package/dist/domain/money.d.ts +5 -0
- package/dist/domain/money.d.ts.map +1 -0
- package/dist/domain/money.js +3 -0
- package/dist/domain/money.js.map +1 -0
- package/dist/domain/movement.d.ts +114 -0
- package/dist/domain/movement.d.ts.map +1 -0
- package/dist/domain/movement.js +4 -0
- package/dist/domain/movement.js.map +1 -0
- package/dist/domain/nota-fiscal.d.ts +47 -0
- package/dist/domain/nota-fiscal.d.ts.map +1 -0
- package/dist/domain/nota-fiscal.js +3 -0
- package/dist/domain/nota-fiscal.js.map +1 -0
- package/dist/domain/payment-method.d.ts +54 -0
- package/dist/domain/payment-method.d.ts.map +1 -0
- package/dist/domain/payment-method.js +25 -0
- package/dist/domain/payment-method.js.map +1 -0
- package/dist/domain/query.d.ts +9 -0
- package/dist/domain/query.d.ts.map +1 -0
- package/dist/domain/query.js +3 -0
- package/dist/domain/query.js.map +1 -0
- package/dist/domain/reconciliation.d.ts +277 -0
- package/dist/domain/reconciliation.d.ts.map +1 -0
- package/dist/domain/reconciliation.js +4 -0
- package/dist/domain/reconciliation.js.map +1 -0
- package/dist/domain/recurring-bill.d.ts +42 -0
- package/dist/domain/recurring-bill.d.ts.map +1 -0
- package/dist/domain/recurring-bill.js +3 -0
- package/dist/domain/recurring-bill.js.map +1 -0
- package/dist/domain/report.d.ts +92 -0
- package/dist/domain/report.d.ts.map +1 -0
- package/dist/domain/report.js +6 -0
- package/dist/domain/report.js.map +1 -0
- package/dist/domain/statement-match.d.ts +36 -0
- package/dist/domain/statement-match.d.ts.map +1 -0
- package/dist/domain/statement-match.js +3 -0
- package/dist/domain/statement-match.js.map +1 -0
- package/dist/domain/statement.d.ts +75 -0
- package/dist/domain/statement.d.ts.map +1 -0
- package/dist/domain/statement.js +4 -0
- package/dist/domain/statement.js.map +1 -0
- package/dist/domain/transfer.d.ts +29 -0
- package/dist/domain/transfer.d.ts.map +1 -0
- package/dist/domain/transfer.js +3 -0
- package/dist/domain/transfer.js.map +1 -0
- package/dist/store.d.ts +66 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +287 -0
- package/dist/store.js.map +1 -0
- package/dist/views/zones.d.ts +40 -0
- package/dist/views/zones.d.ts.map +1 -0
- package/dist/views/zones.js +16 -0
- package/dist/views/zones.js.map +1 -0
- 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
|