feat(transactions): edição cooperativa e visibilidade de divisões

Adiciona splitGroupId para vincular as duas shares de um lançamento
dividido (schema + índice + migration 0026). Habilita:

- Edição de par dividido com escolha de escopo (apenas este lado ou
  ambos) via novo SplitPairDialog e updateTransactionSplitPairAction
- Filtro "Somente divididos" (isDivided) na tabela de lançamentos
- Visibilidade de anexos para pessoas com acesso compartilhado via
  payerShares; upload e detach em massa expandem para shares irmãs
- Cópia independente de anexos no fluxo "Importar para Minha Conta"
  (novo fileKey, novo userId, S3 CopyObject) com seção read-only
  "Anexos que serão copiados" no dialog de importação
- Ícone de clipe na tabela de lançamentos da página da pessoa via
  EXISTS em fetchPagadorLancamentos

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Felipe Coutinho
2026-04-25 14:45:35 +00:00
parent 5b03824a72
commit b14f487824
20 changed files with 3595 additions and 86 deletions

View File

@@ -1,3 +1,4 @@
import { randomUUID } from "node:crypto";
import { and, eq, inArray } from "drizzle-orm";
import { z } from "zod";
import {
@@ -394,7 +395,11 @@ const refineLancamento = (
}
};
export const createSchema = baseFields.superRefine(refineLancamento);
export const createSchema = baseFields
.extend({
importFromTransactionId: uuidSchema("Lançamento fonte").optional(),
})
.superRefine(refineLancamento);
export const updateSchema = baseFields
.extend({
id: uuidSchema("Lançamento"),
@@ -544,6 +549,7 @@ export const buildLancamentoRecords = ({
seriesId,
}: BuildTransactionRecordsParams): TransactionInsert[] => {
const records: TransactionInsert[] = [];
const isSplit = (data.isSplit ?? false) && shares.length > 1;
const basePayload = {
name: data.name,
@@ -562,6 +568,8 @@ export const buildLancamentoRecords = ({
seriesId,
};
const cycleSplitGroupId = () => (isSplit ? randomUUID() : null);
const resolveSettledValue = (cycleIndex: number) => {
if (shouldNullifySettled) {
return null;
@@ -588,6 +596,7 @@ export const buildLancamentoRecords = ({
const installmentDueDate = dueDate
? addMonthsToDate(dueDate, installment)
: null;
const splitGroupId = cycleSplitGroupId();
shares.forEach((share, shareIndex) => {
const amountCents = amountsByShare[shareIndex]?.[installment] ?? 0;
@@ -603,6 +612,7 @@ export const buildLancamentoRecords = ({
currentInstallment: installment + 1,
recurrenceCount: null,
dueDate: installmentDueDate,
splitGroupId,
boletoPaymentDate:
data.paymentMethod === "Boleto" && settled
? boletoPaymentDate
@@ -623,6 +633,7 @@ export const buildLancamentoRecords = ({
const recurrenceDueDate = dueDate
? addMonthsToDate(dueDate, index)
: null;
const splitGroupId = cycleSplitGroupId();
shares.forEach((share) => {
const settled = resolveSettledValue(index);
@@ -635,6 +646,7 @@ export const buildLancamentoRecords = ({
isSettled: settled,
recurrenceCount: recurrenceTotal,
dueDate: recurrenceDueDate,
splitGroupId,
boletoPaymentDate:
data.paymentMethod === "Boleto" && settled
? boletoPaymentDate
@@ -646,6 +658,8 @@ export const buildLancamentoRecords = ({
return records;
}
const splitGroupId = cycleSplitGroupId();
shares.forEach((share) => {
const settled = resolveSettledValue(0);
records.push({
@@ -656,6 +670,7 @@ export const buildLancamentoRecords = ({
period,
isSettled: settled,
dueDate,
splitGroupId,
boletoPaymentDate:
data.paymentMethod === "Boleto" && settled ? boletoPaymentDate : null,
});