aboutsummaryrefslogtreecommitdiff
path: root/api/src/db
diff options
context:
space:
mode:
authorLucas Faria Mendes <lucas.fariamo08@gmail.com>2026-08-16 23:17:42 +0000
committerLucas Faria Mendes <lucas.fariamo08@gmail.com>2026-08-16 23:17:42 +0000
commit375482e654a01615683867e732ac9e458c7cbe74 (patch)
tree12d1b31b8deb6bf8590bcdbc2cfb52dcc553e006 /api/src/db
parent2c2dfba45357bbd205a59145ae55a2779c911ae0 (diff)
downloadyaba-375482e654a01615683867e732ac9e458c7cbe74.tar.gz
yaba-375482e654a01615683867e732ac9e458c7cbe74.zip
feat: upload yaba srcHEADmaster
Signed-off-by: Lucas Faria Mendes <lucas.fariamo08@gmail.com>
Diffstat (limited to 'api/src/db')
-rw-r--r--api/src/db/client.ts23
-rw-r--r--api/src/db/schema/incomes.ts51
-rw-r--r--api/src/db/schema/index.ts11
-rw-r--r--api/src/db/schema/one-time-expenses.ts53
-rw-r--r--api/src/db/schema/recurring-expenses.ts69
-rw-r--r--api/src/db/schema/users.ts37
6 files changed, 244 insertions, 0 deletions
diff --git a/api/src/db/client.ts b/api/src/db/client.ts
new file mode 100644
index 0000000..5e9673f
--- /dev/null
+++ b/api/src/db/client.ts
@@ -0,0 +1,23 @@
+import { Database } from 'bun:sqlite';
+import { drizzle } from 'drizzle-orm/bun-sqlite';
+import * as schema from './schema';
+
+/**
+ * Instância única do banco de dados SQLite via Drizzle ORM.
+ * Utiliza o driver nativo do Bun para máxima performance.
+ *
+ * O caminho do arquivo pode ser configurado via variável de ambiente DATABASE_URL.
+ * Quando não definida, usa 'yaba.db' na raiz do projeto.
+ */
+const sqlite = new Database(process.env.DATABASE_URL ?? 'yaba.db');
+
+/**
+ * Habilita WAL mode para melhor performance em leituras concorrentes.
+ * @see https://www.sqlite.org/wal.html
+ */
+sqlite.exec('PRAGMA journal_mode = WAL;');
+sqlite.exec('PRAGMA foreign_keys = ON;');
+
+export const db = drizzle(sqlite, { schema });
+
+export type DB = typeof db;
diff --git a/api/src/db/schema/incomes.ts b/api/src/db/schema/incomes.ts
new file mode 100644
index 0000000..f502cb9
--- /dev/null
+++ b/api/src/db/schema/incomes.ts
@@ -0,0 +1,51 @@
+import { sql } from 'drizzle-orm';
+import { integer, real, sqliteTable, text } from 'drizzle-orm/sqlite-core';
+import { users } from './users';
+
+/**
+ * Tabela de rendas/salários.
+ * Armazena o histórico de salários por mês/ano, permitindo que o usuário
+ * tenha registros diferentes para cada período (ex: aumento de salário em julho).
+ */
+export const incomes = sqliteTable('incomes', {
+ id: integer('id').primaryKey({ autoIncrement: true }),
+
+ user_id: integer('user_id')
+ .notNull()
+ .references(() => users.id, { onDelete: 'cascade' }),
+
+ /** Salário bruto do período (em R$) */
+ gross_salary: real('gross_salary').notNull(),
+
+ /**
+ * Salário líquido calculado automaticamente pelo app (INSS + IRRF).
+ * Armazenado para fins de histórico e auditoria.
+ */
+ calculated_net_salary: real('calculated_net_salary').notNull(),
+
+ /**
+ * Salário líquido efetivo usado no cálculo do saldo.
+ * Pode diferir do calculado caso o usuário tenha feito um override manual.
+ */
+ effective_net_salary: real('effective_net_salary').notNull(),
+
+ /** Mês de referência (1-12) */
+ reference_month: integer('reference_month').notNull(),
+
+ /** Ano de referência (ex: 2025) */
+ reference_year: integer('reference_year').notNull(),
+
+ /** Observações opcionais (ex: "13º salário", "PLR") */
+ description: text('description'),
+
+ created_at: text('created_at')
+ .notNull()
+ .default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
+
+ updated_at: text('updated_at')
+ .notNull()
+ .default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
+});
+
+export type Income = typeof incomes.$inferSelect;
+export type NewIncome = typeof incomes.$inferInsert;
diff --git a/api/src/db/schema/index.ts b/api/src/db/schema/index.ts
new file mode 100644
index 0000000..72cf261
--- /dev/null
+++ b/api/src/db/schema/index.ts
@@ -0,0 +1,11 @@
+/**
+ * Ponto central de exportação de todos os schemas do Drizzle.
+ * Importe sempre a partir deste arquivo para evitar imports circulares.
+ *
+ * Uso:
+ * import { users, incomes, recurringExpenses, oneTimeExpenses } from '@/db/schema';
+ */
+export * from './users';
+export * from './incomes';
+export * from './recurring-expenses';
+export * from './one-time-expenses';
diff --git a/api/src/db/schema/one-time-expenses.ts b/api/src/db/schema/one-time-expenses.ts
new file mode 100644
index 0000000..10c9c01
--- /dev/null
+++ b/api/src/db/schema/one-time-expenses.ts
@@ -0,0 +1,53 @@
+import { sql } from 'drizzle-orm';
+import { integer, real, sqliteTable, text } from 'drizzle-orm/sqlite-core';
+import { users } from './users';
+
+/**
+ * Tabela de gastos avulsos (despesas pontuais).
+ * Ex: Cinema, iFood, Uber, Farmácia.
+ * Cada registro representa um gasto único em uma data específica.
+ * O valor é subtraído do saldo disponível do mês em que a despesa ocorreu.
+ */
+export const oneTimeExpenses = sqliteTable('one_time_expenses', {
+ id: integer('id').primaryKey({ autoIncrement: true }),
+
+ user_id: integer('user_id')
+ .notNull()
+ .references(() => users.id, { onDelete: 'cascade' }),
+
+ /** Nome/descrição do gasto (ex: "Cinema com a família", "iFood jantar") */
+ name: text('name').notNull(),
+
+ /** Valor do gasto (em R$) */
+ amount: real('amount').notNull(),
+
+ /**
+ * Categoria do gasto para organização e futuros relatórios.
+ * Ex: "alimentação", "lazer", "saúde", "transporte", "educação", "outros"
+ */
+ category: text('category').notNull().default('outros'),
+
+ /** Tipo do lançamento: 'expense' (despesa) ou 'income' (receita) */
+ type: text('type').notNull().default('expense'),
+
+ /**
+ * Data em que o gasto ocorreu.
+ * Formato ISO 8601 (YYYY-MM-DD). Usado para atribuir o gasto ao mês correto.
+ * Default: data atual no momento da inserção.
+ */
+ expense_date: text('expense_date').notNull().default(sql`(date('now'))`),
+
+ /** Observações opcionais (ex: "Split 50/50 com João") */
+ notes: text('notes'),
+
+ created_at: text('created_at')
+ .notNull()
+ .default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
+
+ updated_at: text('updated_at')
+ .notNull()
+ .default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
+});
+
+export type OneTimeExpense = typeof oneTimeExpenses.$inferSelect;
+export type NewOneTimeExpense = typeof oneTimeExpenses.$inferInsert;
diff --git a/api/src/db/schema/recurring-expenses.ts b/api/src/db/schema/recurring-expenses.ts
new file mode 100644
index 0000000..8a0a1df
--- /dev/null
+++ b/api/src/db/schema/recurring-expenses.ts
@@ -0,0 +1,69 @@
+import { sql } from 'drizzle-orm';
+import { integer, real, sqliteTable, text } from 'drizzle-orm/sqlite-core';
+import { users } from './users';
+
+/**
+ * Tabela de gastos recorrentes (despesas fixas mensais).
+ * Ex: Aluguel, Internet, Streaming, Academia.
+ * Estes gastos são deduzidos automaticamente do saldo todo mês,
+ * sem precisar recadastrar.
+ */
+export const recurringExpenses = sqliteTable('recurring_expenses', {
+ id: integer('id').primaryKey({ autoIncrement: true }),
+
+ user_id: integer('user_id')
+ .notNull()
+ .references(() => users.id, { onDelete: 'cascade' }),
+
+ /** Nome/descrição do gasto (ex: "Aluguel", "Netflix") */
+ name: text('name').notNull(),
+
+ /** Valor mensal do gasto (em R$) */
+ amount: real('amount').notNull(),
+
+ /**
+ * Categoria do gasto para organização e futuros relatórios.
+ * Ex: "moradia", "alimentação", "lazer", "saúde", "transporte", "educação", "outros"
+ */
+ category: text('category').notNull().default('outros'),
+
+ /** Tipo do lançamento: 'expense' (despesa) ou 'income' (receita) */
+ type: text('type').notNull().default('expense'),
+
+ /**
+ * Dia do mês em que a despesa costuma vencer (1-31).
+ * Opcional — usado apenas para informação ao usuário.
+ */
+ due_day: integer('due_day'),
+
+ /**
+ * Indica se o gasto está ativo.
+ * Quando false, não é deduzido do saldo (útil para pausar sem deletar).
+ * Armazenado como integer (0 = false, 1 = true) por limitação do SQLite.
+ */
+ is_active: integer('is_active', { mode: 'boolean' }).notNull().default(true),
+
+ /**
+ * Data a partir da qual este gasto passa a ser contabilizado.
+ * Formato ISO 8601 (YYYY-MM-DD).
+ */
+ starts_at: text('starts_at').notNull().default(sql`(date('now'))`),
+
+ /**
+ * Data em que este gasto deixa de ser contabilizado (opcional).
+ * Útil para despesas com prazo definido (ex: parcelamento de 12x).
+ * Formato ISO 8601 (YYYY-MM-DD).
+ */
+ ends_at: text('ends_at'),
+
+ created_at: text('created_at')
+ .notNull()
+ .default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
+
+ updated_at: text('updated_at')
+ .notNull()
+ .default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
+});
+
+export type RecurringExpense = typeof recurringExpenses.$inferSelect;
+export type NewRecurringExpense = typeof recurringExpenses.$inferInsert;
diff --git a/api/src/db/schema/users.ts b/api/src/db/schema/users.ts
new file mode 100644
index 0000000..636ad2d
--- /dev/null
+++ b/api/src/db/schema/users.ts
@@ -0,0 +1,37 @@
+import { sql } from 'drizzle-orm';
+import { integer, real, sqliteTable, text } from 'drizzle-orm/sqlite-core';
+
+/**
+ * Tabela de usuários.
+ * Por ora, suporta um único usuário local (single-user app).
+ * O campo `net_salary_override` permite que o usuário edite manualmente
+ * o salário líquido calculado, para cobrir descontos extras (plano de saúde, VT, etc.).
+ */
+export const users = sqliteTable('users', {
+ id: integer('id').primaryKey({ autoIncrement: true }),
+
+ name: text('name').notNull(),
+ email: text('email').unique().notNull(),
+ password_hash: text('password_hash').notNull(),
+
+ /** Salário bruto informado pelo usuário (em R$) */
+ gross_salary: real('gross_salary').notNull().default(0),
+
+ /**
+ * Override manual do salário líquido.
+ * Quando NULL, o app usa o valor calculado automaticamente (INSS + IRRF).
+ * Quando preenchido, este valor é usado como base para o saldo disponível.
+ */
+ net_salary_override: real('net_salary_override'),
+
+ created_at: text('created_at')
+ .notNull()
+ .default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
+
+ updated_at: text('updated_at')
+ .notNull()
+ .default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
+});
+
+export type User = typeof users.$inferSelect;
+export type NewUser = typeof users.$inferInsert;