Files
openmonetis/db/schema.ts
Felipe Coutinho fd817683ca feat: implementar sistema de preferências do usuário e refatorar changelog
Adiciona sistema completo de preferências de usuário:
  - Cria tabela userPreferences no schema com campos disableMagnetlines, periodMonthsBefore e periodMonthsAfter
  - Implementa página de Ajustes com abas (Preferências, Alterar nome, Senha, E-mail, Deletar conta)
  - Adiciona componente PreferencesForm para configuração de magnetlines e períodos de exibição
  - Propaga periodPreferences para todos os componentes de lançamentos e calendário

  Refatora sistema de changelog:
  - Remove implementação anterior baseada em JSON estático
  - Adiciona nova página de changelog dinâmica em app/(dashboard)/changelog
  - Adiciona componente changelog-list.tsx
  - Remove arquivos obsoletos (changelog-notification, actions, data, utils, scripts)

  Adiciona controle de saldo inicial em contas:
  - Novo campo excludeInitialBalanceFromIncome em contas
  - Permite excluir saldo inicial do cálculo de receitas
  - Atualiza queries de lançamentos para respeitar esta configuração

  Melhorias adicionais:
  - Adiciona componente ui/accordion.tsx do shadcn/ui
  - Refatora formatPeriodLabel para displayPeriod centralizado
  - Propaga estabelecimentos para componentes de lançamentos
  - Remove variável DB_PROVIDER obsoleta do .env.example e documentação
  - Adiciona 6 migrações de banco de dados (0003-0008)
2026-01-03 14:18:03 +00:00

738 lines
20 KiB
TypeScript

import { relations, sql } from "drizzle-orm";
import {
boolean,
date,
index,
integer,
jsonb,
numeric,
pgTable,
smallint,
text,
timestamp,
uniqueIndex,
uuid,
} from "drizzle-orm/pg-core";
export const user = pgTable("user", {
id: text("id").primaryKey(),
name: text("name").notNull(),
email: text("email").notNull().unique(),
emailVerified: boolean("emailVerified").notNull(),
image: text("image"),
createdAt: timestamp("createdAt", {
mode: "date",
withTimezone: true,
}).notNull(),
updatedAt: timestamp("updatedAt", {
mode: "date",
withTimezone: true,
}).notNull(),
});
export const account = pgTable("account", {
id: text("id").primaryKey(),
accountId: text("accountId").notNull(),
providerId: text("providerId").notNull(),
userId: text("userId")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
accessToken: text("accessToken"),
refreshToken: text("refreshToken"),
idToken: text("idToken"),
accessTokenExpiresAt: timestamp("accessTokenExpiresAt", {
mode: "date",
withTimezone: true,
}),
refreshTokenExpiresAt: timestamp("refreshTokenExpiresAt", {
mode: "date",
withTimezone: true,
}),
scope: text("scope"),
password: text("password"),
createdAt: timestamp("createdAt", {
mode: "date",
withTimezone: true,
}).notNull(),
updatedAt: timestamp("updatedAt", {
mode: "date",
withTimezone: true,
}).notNull(),
});
export const session = pgTable("session", {
id: text("id").primaryKey(),
expiresAt: timestamp("expiresAt", {
mode: "date",
withTimezone: true,
}).notNull(),
token: text("token").notNull().unique(),
createdAt: timestamp("createdAt", {
mode: "date",
withTimezone: true,
}).notNull(),
updatedAt: timestamp("updatedAt", {
mode: "date",
withTimezone: true,
}).notNull(),
ipAddress: text("ipAddress"),
userAgent: text("userAgent"),
userId: text("userId")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
});
export const verification = pgTable("verification", {
id: text("id").primaryKey(),
identifier: text("identifier").notNull(),
value: text("value").notNull(),
expiresAt: timestamp("expiresAt", {
mode: "date",
withTimezone: true,
}).notNull(),
createdAt: timestamp("createdAt", {
mode: "date",
withTimezone: true,
}),
updatedAt: timestamp("updatedAt", {
mode: "date",
withTimezone: true,
}),
});
export const userPreferences = pgTable("user_preferences", {
id: uuid("id")
.primaryKey()
.default(sql`gen_random_uuid()`),
userId: text("user_id")
.notNull()
.unique()
.references(() => user.id, { onDelete: "cascade" }),
disableMagnetlines: boolean("disable_magnetlines").notNull().default(false),
periodMonthsBefore: integer("period_months_before").notNull().default(3),
periodMonthsAfter: integer("period_months_after").notNull().default(3),
createdAt: timestamp("created_at", {
mode: "date",
withTimezone: true,
})
.notNull()
.default(sql`now()`),
updatedAt: timestamp("updated_at", {
mode: "date",
withTimezone: true,
})
.notNull()
.default(sql`now()`),
});
// ===================== PUBLIC TABLES =====================
export const contas = pgTable(
"contas",
{
id: uuid("id")
.primaryKey()
.default(sql`gen_random_uuid()`),
name: text("nome").notNull(),
accountType: text("tipo_conta").notNull(),
note: text("anotacao"),
status: text("status").notNull(),
logo: text("logo").notNull(),
initialBalance: numeric("saldo_inicial", { precision: 12, scale: 2 })
.notNull()
.default("0"),
excludeFromBalance: boolean("excluir_do_saldo")
.notNull()
.default(false),
excludeInitialBalanceFromIncome: boolean("excluir_saldo_inicial_receitas")
.notNull()
.default(false),
userId: text("user_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
createdAt: timestamp("created_at", {
mode: "date",
withTimezone: true,
})
.notNull()
.defaultNow(),
},
(table) => ({
userIdStatusIdx: index("contas_user_id_status_idx").on(
table.userId,
table.status
),
})
);
export const categorias = pgTable(
"categorias",
{
id: uuid("id")
.primaryKey()
.default(sql`gen_random_uuid()`),
name: text("nome").notNull(),
type: text("tipo").notNull(),
icon: text("icone"),
createdAt: timestamp("created_at", {
mode: "date",
withTimezone: true,
})
.notNull()
.defaultNow(),
userId: text("user_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
},
(table) => ({
userIdTypeIdx: index("categorias_user_id_type_idx").on(
table.userId,
table.type
),
})
);
export const pagadores = pgTable(
"pagadores",
{
id: uuid("id")
.primaryKey()
.default(sql`gen_random_uuid()`),
name: text("nome").notNull(),
email: text("email"),
avatarUrl: text("avatar_url"),
status: text("status").notNull(),
note: text("anotacao"),
role: text("role"),
isAutoSend: boolean("is_auto_send").notNull().default(false),
shareCode: text("share_code")
.notNull()
.default(sql`substr(encode(gen_random_bytes(24), 'base64'), 1, 24)`),
lastMailAt: timestamp("last_mail", {
mode: "date",
withTimezone: true,
}),
createdAt: timestamp("created_at", {
mode: "date",
withTimezone: true,
})
.notNull()
.defaultNow(),
userId: text("user_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
},
(table) => ({
uniqueShareCode: uniqueIndex("pagadores_share_code_key").on(
table.shareCode
),
userIdStatusIdx: index("pagadores_user_id_status_idx").on(
table.userId,
table.status
),
userIdRoleIdx: index("pagadores_user_id_role_idx").on(
table.userId,
table.role
),
})
);
export const pagadorShares = pgTable(
"pagador_shares",
{
id: uuid("id")
.primaryKey()
.default(sql`gen_random_uuid()`),
pagadorId: uuid("pagador_id")
.notNull()
.references(() => pagadores.id, { onDelete: "cascade" }),
sharedWithUserId: text("shared_with_user_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
permission: text("permission").notNull().default("read"),
createdByUserId: text("created_by_user_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
createdAt: timestamp("created_at", {
mode: "date",
withTimezone: true,
})
.notNull()
.defaultNow(),
},
(table) => ({
uniquePagadorShare: uniqueIndex("pagador_shares_unique").on(
table.pagadorId,
table.sharedWithUserId
),
})
);
export const cartoes = pgTable(
"cartoes",
{
id: uuid("id")
.primaryKey()
.default(sql`gen_random_uuid()`),
name: text("nome").notNull(),
closingDay: text("dt_fechamento").notNull(),
dueDay: text("dt_vencimento").notNull(),
note: text("anotacao"),
limit: numeric("limite", { precision: 10, scale: 2 }),
brand: text("bandeira"),
logo: text("logo"),
status: text("status").notNull(),
createdAt: timestamp("created_at", {
mode: "date",
withTimezone: true,
})
.notNull()
.defaultNow(),
userId: text("user_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
contaId: uuid("conta_id")
.notNull()
.references(() => contas.id, { onDelete: "cascade", onUpdate: "cascade" }),
},
(table) => ({
userIdStatusIdx: index("cartoes_user_id_status_idx").on(
table.userId,
table.status
),
})
);
export const faturas = pgTable(
"faturas",
{
id: uuid("id")
.primaryKey()
.default(sql`gen_random_uuid()`),
paymentStatus: text("status_pagamento"),
period: text("periodo"),
createdAt: timestamp("created_at", {
mode: "date",
withTimezone: true,
})
.notNull()
.defaultNow(),
userId: text("user_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
cartaoId: uuid("cartao_id").references(() => cartoes.id, {
onDelete: "cascade",
onUpdate: "cascade",
}),
},
(table) => ({
userIdPeriodIdx: index("faturas_user_id_period_idx").on(
table.userId,
table.period
),
cartaoIdPeriodIdx: index("faturas_cartao_id_period_idx").on(
table.cartaoId,
table.period
),
})
);
export const orcamentos = pgTable(
"orcamentos",
{
id: uuid("id")
.primaryKey()
.default(sql`gen_random_uuid()`),
amount: numeric("valor", { precision: 10, scale: 2 }).notNull(),
period: text("periodo").notNull(),
createdAt: timestamp("created_at", {
mode: "date",
withTimezone: true,
})
.notNull()
.defaultNow(),
userId: text("user_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
categoriaId: uuid("categoria_id").references(() => categorias.id, {
onDelete: "cascade",
onUpdate: "cascade",
}),
},
(table) => ({
userIdPeriodIdx: index("orcamentos_user_id_period_idx").on(
table.userId,
table.period
),
})
);
export const anotacoes = pgTable("anotacoes", {
id: uuid("id")
.primaryKey()
.default(sql`gen_random_uuid()`),
title: text("titulo"),
description: text("descricao"),
type: text("tipo").notNull().default("nota"), // "nota" ou "tarefa"
tasks: text("tasks"), // JSON stringificado com array de tarefas
arquivada: boolean("arquivada").notNull().default(false),
createdAt: timestamp("created_at", {
mode: "date",
withTimezone: true,
})
.notNull()
.defaultNow(),
userId: text("user_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
});
export const savedInsights = pgTable(
"saved_insights",
{
id: uuid("id")
.primaryKey()
.default(sql`gen_random_uuid()`),
userId: text("user_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
period: text("period").notNull(),
modelId: text("model_id").notNull(),
data: text("data").notNull(), // JSON stringificado com as análises
createdAt: timestamp("created_at", {
mode: "date",
withTimezone: true,
})
.notNull()
.defaultNow(),
updatedAt: timestamp("updated_at", {
mode: "date",
withTimezone: true,
})
.notNull()
.defaultNow(),
},
(table) => ({
userPeriodIdx: uniqueIndex("saved_insights_user_period_idx").on(
table.userId,
table.period
),
})
);
export const installmentAnticipations = pgTable(
"installment_anticipations",
{
id: uuid("id")
.primaryKey()
.default(sql`gen_random_uuid()`),
seriesId: uuid("series_id").notNull(),
anticipationPeriod: text("periodo_antecipacao").notNull(),
anticipationDate: date("data_antecipacao", { mode: "date" }).notNull(),
anticipatedInstallmentIds: jsonb("parcelas_antecipadas")
.notNull()
.$type<string[]>(),
totalAmount: numeric("valor_total", { precision: 12, scale: 2 }).notNull(),
installmentCount: smallint("qtde_parcelas").notNull(),
discount: numeric("desconto", { precision: 12, scale: 2 })
.notNull()
.default("0"),
lancamentoId: uuid("lancamento_id")
.notNull()
.references(() => lancamentos.id, { onDelete: "cascade" }),
pagadorId: uuid("pagador_id").references(() => pagadores.id, {
onDelete: "cascade",
}),
categoriaId: uuid("categoria_id").references(() => categorias.id, {
onDelete: "cascade",
}),
note: text("anotacao"),
userId: text("user_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
createdAt: timestamp("created_at", {
mode: "date",
withTimezone: true,
})
.notNull()
.defaultNow(),
},
(table) => ({
seriesIdIdx: index("installment_anticipations_series_id_idx").on(
table.seriesId
),
userIdIdx: index("installment_anticipations_user_id_idx").on(table.userId),
})
);
export const lancamentos = pgTable(
"lancamentos",
{
id: uuid("id")
.primaryKey()
.default(sql`gen_random_uuid()`),
condition: text("condicao").notNull(),
name: text("nome").notNull(),
paymentMethod: text("forma_pagamento").notNull(),
note: text("anotacao"),
amount: numeric("valor", { precision: 12, scale: 2 }).notNull(),
purchaseDate: date("data_compra", { mode: "date" }).notNull(),
transactionType: text("tipo_transacao").notNull(),
installmentCount: smallint("qtde_parcela"),
period: text("periodo").notNull(),
currentInstallment: smallint("parcela_atual"),
recurrenceCount: integer("qtde_recorrencia"),
dueDate: date("data_vencimento", { mode: "date" }),
boletoPaymentDate: date("dt_pagamento_boleto", { mode: "date" }),
isSettled: boolean("realizado").default(false),
isDivided: boolean("dividido").default(false),
isAnticipated: boolean("antecipado").default(false),
anticipationId: uuid("antecipacao_id").references(
() => installmentAnticipations.id,
{ onDelete: "set null" }
),
createdAt: timestamp("created_at", {
mode: "date",
withTimezone: true,
})
.notNull()
.defaultNow(),
userId: text("user_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
cartaoId: uuid("cartao_id").references(() => cartoes.id, {
onDelete: "cascade",
onUpdate: "cascade",
}),
contaId: uuid("conta_id").references(() => contas.id, {
onDelete: "cascade",
onUpdate: "cascade",
}),
categoriaId: uuid("categoria_id").references(() => categorias.id, {
onDelete: "cascade",
onUpdate: "cascade",
}),
pagadorId: uuid("pagador_id").references(() => pagadores.id, {
onDelete: "cascade",
onUpdate: "cascade",
}),
seriesId: uuid("series_id"),
transferId: uuid("transfer_id"),
},
(table) => ({
// Índice composto mais importante: userId + period (usado em quase todas as queries do dashboard)
userIdPeriodIdx: index("lancamentos_user_id_period_idx").on(
table.userId,
table.period
),
// Índice para queries ordenadas por data de compra
userIdPurchaseDateIdx: index("lancamentos_user_id_purchase_date_idx").on(
table.userId,
table.purchaseDate
),
// Índice para buscar parcelas de uma série
seriesIdIdx: index("lancamentos_series_id_idx").on(table.seriesId),
// Índice para buscar transferências relacionadas
transferIdIdx: index("lancamentos_transfer_id_idx").on(table.transferId),
// Índice para filtrar por condição (aberto, realizado, cancelado)
userIdConditionIdx: index("lancamentos_user_id_condition_idx").on(
table.userId,
table.condition
),
// Índice para queries de cartão específico
cartaoIdPeriodIdx: index("lancamentos_cartao_id_period_idx").on(
table.cartaoId,
table.period
),
})
);
export const userRelations = relations(user, ({ many, one }) => ({
accounts: many(account),
sessions: many(session),
anotacoes: many(anotacoes),
cartoes: many(cartoes),
categorias: many(categorias),
contas: many(contas),
faturas: many(faturas),
lancamentos: many(lancamentos),
orcamentos: many(orcamentos),
pagadores: many(pagadores),
installmentAnticipations: many(installmentAnticipations),
}));
export const accountRelations = relations(account, ({ one }) => ({
user: one(user, {
fields: [account.userId],
references: [user.id],
}),
}));
export const sessionRelations = relations(session, ({ one }) => ({
user: one(user, {
fields: [session.userId],
references: [user.id],
}),
}));
export const contasRelations = relations(contas, ({ one, many }) => ({
user: one(user, {
fields: [contas.userId],
references: [user.id],
}),
cartoes: many(cartoes),
lancamentos: many(lancamentos),
}));
export const categoriasRelations = relations(categorias, ({ one, many }) => ({
user: one(user, {
fields: [categorias.userId],
references: [user.id],
}),
lancamentos: many(lancamentos),
orcamentos: many(orcamentos),
}));
export const pagadoresRelations = relations(pagadores, ({ one, many }) => ({
user: one(user, {
fields: [pagadores.userId],
references: [user.id],
}),
lancamentos: many(lancamentos),
shares: many(pagadorShares),
}));
export const pagadorSharesRelations = relations(pagadorShares, ({ one }) => ({
pagador: one(pagadores, {
fields: [pagadorShares.pagadorId],
references: [pagadores.id],
}),
sharedWithUser: one(user, {
fields: [pagadorShares.sharedWithUserId],
references: [user.id],
}),
createdByUser: one(user, {
fields: [pagadorShares.createdByUserId],
references: [user.id],
}),
}));
export const cartoesRelations = relations(cartoes, ({ one, many }) => ({
user: one(user, {
fields: [cartoes.userId],
references: [user.id],
}),
conta: one(contas, {
fields: [cartoes.contaId],
references: [contas.id],
}),
faturas: many(faturas),
lancamentos: many(lancamentos),
}));
export const faturasRelations = relations(faturas, ({ one }) => ({
user: one(user, {
fields: [faturas.userId],
references: [user.id],
}),
cartao: one(cartoes, {
fields: [faturas.cartaoId],
references: [cartoes.id],
}),
}));
export const orcamentosRelations = relations(orcamentos, ({ one }) => ({
user: one(user, {
fields: [orcamentos.userId],
references: [user.id],
}),
categoria: one(categorias, {
fields: [orcamentos.categoriaId],
references: [categorias.id],
}),
}));
export const anotacoesRelations = relations(anotacoes, ({ one }) => ({
user: one(user, {
fields: [anotacoes.userId],
references: [user.id],
}),
}));
export const savedInsightsRelations = relations(savedInsights, ({ one }) => ({
user: one(user, {
fields: [savedInsights.userId],
references: [user.id],
}),
}));
export const lancamentosRelations = relations(lancamentos, ({ one }) => ({
user: one(user, {
fields: [lancamentos.userId],
references: [user.id],
}),
cartao: one(cartoes, {
fields: [lancamentos.cartaoId],
references: [cartoes.id],
}),
conta: one(contas, {
fields: [lancamentos.contaId],
references: [contas.id],
}),
categoria: one(categorias, {
fields: [lancamentos.categoriaId],
references: [categorias.id],
}),
pagador: one(pagadores, {
fields: [lancamentos.pagadorId],
references: [pagadores.id],
}),
anticipation: one(installmentAnticipations, {
fields: [lancamentos.anticipationId],
references: [installmentAnticipations.id],
}),
}));
export const installmentAnticipationsRelations = relations(
installmentAnticipations,
({ one, many }) => ({
user: one(user, {
fields: [installmentAnticipations.userId],
references: [user.id],
}),
lancamento: one(lancamentos, {
fields: [installmentAnticipations.lancamentoId],
references: [lancamentos.id],
}),
pagador: one(pagadores, {
fields: [installmentAnticipations.pagadorId],
references: [pagadores.id],
}),
categoria: one(categorias, {
fields: [installmentAnticipations.categoriaId],
references: [categorias.id],
}),
})
);
export type User = typeof user.$inferSelect;
export type NewUser = typeof user.$inferInsert;
export type Account = typeof account.$inferSelect;
export type Session = typeof session.$inferSelect;
export type Verification = typeof verification.$inferSelect;
export type UserPreferences = typeof userPreferences.$inferSelect;
export type NewUserPreferences = typeof userPreferences.$inferInsert;
export type Conta = typeof contas.$inferSelect;
export type Categoria = typeof categorias.$inferSelect;
export type Pagador = typeof pagadores.$inferSelect;
export type Cartao = typeof cartoes.$inferSelect;
export type Fatura = typeof faturas.$inferSelect;
export type Orcamento = typeof orcamentos.$inferSelect;
export type Anotacao = typeof anotacoes.$inferSelect;
export type SavedInsight = typeof savedInsights.$inferSelect;
export type Lancamento = typeof lancamentos.$inferSelect;
export type InstallmentAnticipation =
typeof installmentAnticipations.$inferSelect;