mirror of
https://github.com/felipegcoutinho/openmonetis.git
synced 2026-05-09 02:51:46 +00:00
Move a geração do share_code do PostgreSQL para a camada de aplicação, eliminando a dependência da extensão pgcrypto no setup do banco. - schema: drop default substr(encode(gen_random_bytes(24), 'base64'), 1, 24) da coluna share_code em pagadores (continua NOT NULL) - nova util generateShareCode() em shared/lib/payers/share-code.ts (server-only, usa crypto.randomBytes do Node) - chamadas explícitas em createPayerAction, ensureDefaultPagadorForUser, resetUserAppData e mock-data ao inserir pagadores - migration 0028_fancy_reaper renumerada (0027 já estava ocupado por arquivo órfão); journal e snapshot atualizados - remove etapa de habilitação de pgcrypto do docker-entrypoint.sh - remove scripts/postgres/ (init.sql e enable-extensions.ts) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1816 lines
48 KiB
TypeScript
1816 lines
48 KiB
TypeScript
#!/usr/bin/env tsx
|
|
|
|
import { randomUUID } from "node:crypto";
|
|
import { config } from "dotenv";
|
|
import { and, eq, ne, sql } from "drizzle-orm";
|
|
import {
|
|
budgets,
|
|
cards,
|
|
categories,
|
|
financialAccounts,
|
|
inboxItems,
|
|
invoices,
|
|
notes,
|
|
payers,
|
|
transactions,
|
|
user,
|
|
} from "@/db/schema";
|
|
import { loadAvatarOptions } from "@/features/payers/lib/avatar-options";
|
|
import type { TransactionInsert } from "@/features/transactions/actions/core";
|
|
import type {
|
|
PAYMENT_METHODS,
|
|
TRANSACTION_CONDITIONS,
|
|
TRANSACTION_TYPES,
|
|
} from "@/features/transactions/constants";
|
|
import {
|
|
buildInvoicePaymentNote,
|
|
INITIAL_BALANCE_CATEGORY_NAME,
|
|
INITIAL_BALANCE_CONDITION,
|
|
INITIAL_BALANCE_NOTE,
|
|
INITIAL_BALANCE_PAYMENT_METHOD,
|
|
INITIAL_BALANCE_TRANSACTION_TYPE,
|
|
} from "@/shared/lib/accounts/constants";
|
|
import {
|
|
DEFAULT_CARD_BRANDS,
|
|
DEFAULT_CARD_STATUS,
|
|
} from "@/shared/lib/cards/constants";
|
|
import { DEFAULT_CATEGORIES } from "@/shared/lib/categories/defaults";
|
|
import { db } from "@/shared/lib/db";
|
|
import { INVOICE_PAYMENT_STATUS } from "@/shared/lib/invoices";
|
|
import { loadLogoOptions } from "@/shared/lib/logo/options";
|
|
import {
|
|
DEFAULT_PAYER_AVATAR,
|
|
PAYER_ROLE_ADMIN,
|
|
PAYER_ROLE_THIRD_PARTY,
|
|
PAYER_STATUS_OPTIONS,
|
|
} from "@/shared/lib/payers/constants";
|
|
import { generateShareCode } from "@/shared/lib/payers/share-code";
|
|
import { normalizeNameFromEmail } from "@/shared/lib/payers/utils";
|
|
import {
|
|
addMonthsToDate,
|
|
buildDateOnlyStringFromPeriodDay,
|
|
compareDateOnly,
|
|
getBusinessTodayInfo,
|
|
parseLocalDateString,
|
|
} from "@/shared/utils/date";
|
|
import {
|
|
addMonthsToPeriod,
|
|
comparePeriods,
|
|
derivePeriodFromDate,
|
|
getNextPeriod,
|
|
parsePeriod,
|
|
} from "@/shared/utils/period";
|
|
|
|
config();
|
|
|
|
const DEFAULT_MONTHS = 6;
|
|
const MIN_MONTHS = 3;
|
|
const MAX_MONTHS = 24;
|
|
|
|
const ACCOUNT_TYPES = {
|
|
CHECKING: "Conta Corrente",
|
|
DIGITAL_WALLET: "Carteira Digital",
|
|
} as const;
|
|
|
|
const ACCOUNT_STATUS = {
|
|
ACTIVE: "Ativa",
|
|
} as const;
|
|
|
|
const CARD_STATUS = DEFAULT_CARD_STATUS[0];
|
|
const CARD_BRANDS = {
|
|
MASTERCARD: DEFAULT_CARD_BRANDS[1] ?? "Mastercard",
|
|
VISA: DEFAULT_CARD_BRANDS[0] ?? "Visa",
|
|
} as const;
|
|
|
|
const PAYER_STATUS = PAYER_STATUS_OPTIONS[0];
|
|
|
|
type CliOptions = {
|
|
userId: string;
|
|
startPeriod: string;
|
|
months: number;
|
|
};
|
|
|
|
type SeedTransactionInput = {
|
|
name: string;
|
|
amount: number;
|
|
purchaseDate: string;
|
|
transactionType: (typeof TRANSACTION_TYPES)[number];
|
|
condition: (typeof TRANSACTION_CONDITIONS)[number];
|
|
paymentMethod: (typeof PAYMENT_METHODS)[number];
|
|
accountId?: string | null;
|
|
cardId?: string | null;
|
|
categoryId?: string | null;
|
|
payerId?: string | null;
|
|
secondaryPayerId?: string | null;
|
|
isSplit?: boolean;
|
|
primarySplitAmount?: number;
|
|
secondarySplitAmount?: number;
|
|
installmentCount?: number;
|
|
recurrenceCount?: number;
|
|
dueDate?: string | null;
|
|
note?: string | null;
|
|
settlementBehavior?: "auto" | "open" | "settled";
|
|
cardMeta?: {
|
|
closingDay: string;
|
|
dueDay: string;
|
|
};
|
|
};
|
|
|
|
type Share = {
|
|
payerId: string | null;
|
|
amountCents: number;
|
|
};
|
|
|
|
type SeedSummary = {
|
|
payers: number;
|
|
accounts: number;
|
|
cards: number;
|
|
notes: number;
|
|
budgets: number;
|
|
transactions: number;
|
|
invoices: number;
|
|
invoicePayments: number;
|
|
inboxItems: number;
|
|
};
|
|
|
|
function printUsage() {
|
|
console.log(`
|
|
Uso:
|
|
pnpm mockup -- --userId=<id> --startPeriod=YYYY-MM [--months=${DEFAULT_MONTHS}]
|
|
|
|
Exemplos:
|
|
pnpm mockup -- --userId=user_123 --startPeriod=2026-01
|
|
pnpm mockup -- --userId=user_123 --startPeriod=2025-10 --months=8
|
|
`);
|
|
}
|
|
|
|
function parseArgs(argv: string[]): CliOptions {
|
|
let userId = "";
|
|
let startPeriod = "";
|
|
let months = DEFAULT_MONTHS;
|
|
|
|
for (const arg of argv) {
|
|
if (arg === "--help" || arg === "-h") {
|
|
printUsage();
|
|
process.exit(0);
|
|
}
|
|
|
|
if (arg.startsWith("--userId=") || arg.startsWith("--user-id=")) {
|
|
userId = arg.split("=")[1] ?? "";
|
|
continue;
|
|
}
|
|
|
|
if (arg.startsWith("--startPeriod=") || arg.startsWith("--start-period=")) {
|
|
startPeriod = arg.split("=")[1] ?? "";
|
|
continue;
|
|
}
|
|
|
|
if (arg.startsWith("--months=")) {
|
|
const rawValue = arg.split("=")[1] ?? "";
|
|
months = Number.parseInt(rawValue, 10);
|
|
}
|
|
}
|
|
|
|
if (!userId.trim()) {
|
|
throw new Error("Informe o `--userId` do usuário que receberá os dados.");
|
|
}
|
|
|
|
if (!startPeriod.trim()) {
|
|
throw new Error("Informe o `--startPeriod` no formato `YYYY-MM`.");
|
|
}
|
|
|
|
parsePeriod(startPeriod);
|
|
|
|
if (Number.isNaN(months) || months < MIN_MONTHS || months > MAX_MONTHS) {
|
|
throw new Error(
|
|
`O parâmetro \`--months\` deve ficar entre ${MIN_MONTHS} e ${MAX_MONTHS}.`,
|
|
);
|
|
}
|
|
|
|
return {
|
|
userId: userId.trim(),
|
|
startPeriod: startPeriod.trim(),
|
|
months,
|
|
};
|
|
}
|
|
|
|
function centsToDecimalString(value: number) {
|
|
const decimal = value / 100;
|
|
const formatted = decimal.toFixed(2);
|
|
return Object.is(decimal, -0) ? "0.00" : formatted;
|
|
}
|
|
|
|
function splitAmount(totalCents: number, parts: number) {
|
|
if (parts <= 0) {
|
|
return [];
|
|
}
|
|
|
|
const base = Math.trunc(totalCents / parts);
|
|
const remainder = totalCents % parts;
|
|
|
|
return Array.from(
|
|
{ length: parts },
|
|
(_, index) => base + (index < remainder ? 1 : 0),
|
|
);
|
|
}
|
|
|
|
function buildShares(input: SeedTransactionInput): Share[] {
|
|
const totalCents = Math.round(Math.abs(input.amount) * 100);
|
|
|
|
if (!input.isSplit) {
|
|
return [{ payerId: input.payerId ?? null, amountCents: totalCents }];
|
|
}
|
|
|
|
if (!input.payerId || !input.secondaryPayerId) {
|
|
throw new Error(`Divisão inválida para o lançamento "${input.name}".`);
|
|
}
|
|
|
|
if (
|
|
input.primarySplitAmount !== undefined &&
|
|
input.secondarySplitAmount !== undefined
|
|
) {
|
|
return [
|
|
{
|
|
payerId: input.payerId,
|
|
amountCents: Math.round(input.primarySplitAmount * 100),
|
|
},
|
|
{
|
|
payerId: input.secondaryPayerId,
|
|
amountCents: Math.round(input.secondarySplitAmount * 100),
|
|
},
|
|
];
|
|
}
|
|
|
|
const [primaryAmount, secondaryAmount] = splitAmount(totalCents, 2);
|
|
|
|
return [
|
|
{ payerId: input.payerId, amountCents: primaryAmount ?? 0 },
|
|
{
|
|
payerId: input.secondaryPayerId,
|
|
amountCents: secondaryAmount ?? 0,
|
|
},
|
|
];
|
|
}
|
|
|
|
function deriveCreditCardPeriod(
|
|
purchaseDate: string,
|
|
closingDay: string | null | undefined,
|
|
dueDay?: string | null | undefined,
|
|
) {
|
|
const basePeriod = derivePeriodFromDate(purchaseDate);
|
|
if (!closingDay) return basePeriod;
|
|
|
|
const closingDayNum = Number.parseInt(closingDay, 10);
|
|
if (Number.isNaN(closingDayNum)) return basePeriod;
|
|
|
|
const dayPart = purchaseDate.split("-")[2];
|
|
const purchaseDayNum = Number.parseInt(dayPart ?? "1", 10);
|
|
|
|
let period = basePeriod;
|
|
|
|
if (purchaseDayNum >= closingDayNum) {
|
|
period = getNextPeriod(period);
|
|
}
|
|
|
|
const dueDayNum = Number.parseInt(dueDay ?? "", 10);
|
|
if (!Number.isNaN(dueDayNum) && dueDayNum < closingDayNum) {
|
|
period = getNextPeriod(period);
|
|
}
|
|
|
|
return period;
|
|
}
|
|
|
|
function pickOption(options: string[], candidates: string[], fallback: string) {
|
|
const normalizedOptions = new Set(options);
|
|
|
|
for (const candidate of candidates) {
|
|
if (normalizedOptions.has(candidate)) {
|
|
return candidate;
|
|
}
|
|
}
|
|
|
|
return options[0] ?? fallback;
|
|
}
|
|
|
|
function dateForPeriodDay(period: string, day: number) {
|
|
const value = buildDateOnlyStringFromPeriodDay(period, day);
|
|
if (!value) {
|
|
throw new Error(`Não foi possível montar a data ${period}/${day}.`);
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
function resolveSeedSettlement(
|
|
input: SeedTransactionInput,
|
|
referenceDate: Date,
|
|
todayDate: Date,
|
|
) {
|
|
if (input.paymentMethod === "Cartão de crédito") {
|
|
return null;
|
|
}
|
|
|
|
if (input.settlementBehavior === "settled") {
|
|
return true;
|
|
}
|
|
|
|
if (input.settlementBehavior === "open") {
|
|
return false;
|
|
}
|
|
|
|
return compareDateOnly(referenceDate, todayDate) <= 0;
|
|
}
|
|
|
|
function createTransactionRecords(
|
|
input: SeedTransactionInput,
|
|
userId: string,
|
|
todayDate: Date,
|
|
): TransactionInsert[] {
|
|
const purchaseDate = parseLocalDateString(input.purchaseDate);
|
|
if (Number.isNaN(purchaseDate.getTime())) {
|
|
throw new Error(`Data inválida no lançamento "${input.name}".`);
|
|
}
|
|
|
|
const dueDate = input.dueDate ? parseLocalDateString(input.dueDate) : null;
|
|
const shares = buildShares(input);
|
|
const amountSign: 1 | -1 = input.transactionType === "Despesa" ? -1 : 1;
|
|
const isSeries =
|
|
input.condition === "Parcelado" || input.condition === "Recorrente";
|
|
const seriesId = isSeries ? randomUUID() : null;
|
|
const initialPeriod =
|
|
input.cardId && input.cardMeta
|
|
? deriveCreditCardPeriod(
|
|
input.purchaseDate,
|
|
input.cardMeta.closingDay,
|
|
input.cardMeta.dueDay,
|
|
)
|
|
: derivePeriodFromDate(input.purchaseDate);
|
|
|
|
const basePayload = {
|
|
name: input.name,
|
|
transactionType: input.transactionType,
|
|
condition: input.condition,
|
|
paymentMethod: input.paymentMethod,
|
|
note: input.note ?? null,
|
|
accountId: input.accountId ?? null,
|
|
cardId: input.cardId ?? null,
|
|
categoryId: input.categoryId ?? null,
|
|
isDivided: input.isSplit ?? false,
|
|
userId,
|
|
seriesId,
|
|
};
|
|
|
|
const records: TransactionInsert[] = [];
|
|
|
|
if (input.condition === "Parcelado") {
|
|
const installmentTotal = input.installmentCount ?? 0;
|
|
const amountsByShare = shares.map((share) =>
|
|
splitAmount(share.amountCents, installmentTotal),
|
|
);
|
|
|
|
for (
|
|
let installmentIndex = 0;
|
|
installmentIndex < installmentTotal;
|
|
installmentIndex += 1
|
|
) {
|
|
const period = addMonthsToPeriod(initialPeriod, installmentIndex);
|
|
const installmentDueDate = dueDate
|
|
? addMonthsToDate(dueDate, installmentIndex)
|
|
: null;
|
|
const settlementReferenceDate =
|
|
installmentDueDate ?? addMonthsToDate(purchaseDate, installmentIndex);
|
|
|
|
shares.forEach((share, shareIndex) => {
|
|
const amountCents = amountsByShare[shareIndex]?.[installmentIndex] ?? 0;
|
|
const isSettled = resolveSeedSettlement(
|
|
input,
|
|
settlementReferenceDate,
|
|
todayDate,
|
|
);
|
|
|
|
records.push({
|
|
...basePayload,
|
|
amount: centsToDecimalString(amountCents * amountSign),
|
|
payerId: share.payerId,
|
|
purchaseDate,
|
|
period,
|
|
isSettled,
|
|
installmentCount: installmentTotal,
|
|
currentInstallment: installmentIndex + 1,
|
|
recurrenceCount: null,
|
|
dueDate: installmentDueDate,
|
|
boletoPaymentDate:
|
|
input.paymentMethod === "Boleto" && isSettled
|
|
? (installmentDueDate ?? settlementReferenceDate)
|
|
: null,
|
|
});
|
|
});
|
|
}
|
|
|
|
return records;
|
|
}
|
|
|
|
if (input.condition === "Recorrente") {
|
|
const recurrenceTotal = input.recurrenceCount ?? 0;
|
|
|
|
for (let index = 0; index < recurrenceTotal; index += 1) {
|
|
const period = addMonthsToPeriod(initialPeriod, index);
|
|
const recurrencePurchaseDate = addMonthsToDate(purchaseDate, index);
|
|
const recurrenceDueDate = dueDate
|
|
? addMonthsToDate(dueDate, index)
|
|
: null;
|
|
const settlementReferenceDate =
|
|
recurrenceDueDate ?? recurrencePurchaseDate;
|
|
|
|
shares.forEach((share) => {
|
|
const isSettled = resolveSeedSettlement(
|
|
input,
|
|
settlementReferenceDate,
|
|
todayDate,
|
|
);
|
|
|
|
records.push({
|
|
...basePayload,
|
|
amount: centsToDecimalString(share.amountCents * amountSign),
|
|
payerId: share.payerId,
|
|
purchaseDate: recurrencePurchaseDate,
|
|
period,
|
|
isSettled,
|
|
installmentCount: null,
|
|
currentInstallment: null,
|
|
recurrenceCount: recurrenceTotal,
|
|
dueDate: recurrenceDueDate,
|
|
boletoPaymentDate:
|
|
input.paymentMethod === "Boleto" && isSettled
|
|
? (recurrenceDueDate ?? recurrencePurchaseDate)
|
|
: null,
|
|
});
|
|
});
|
|
}
|
|
|
|
return records;
|
|
}
|
|
|
|
shares.forEach((share) => {
|
|
const settlementReferenceDate = dueDate ?? purchaseDate;
|
|
const isSettled = resolveSeedSettlement(
|
|
input,
|
|
settlementReferenceDate,
|
|
todayDate,
|
|
);
|
|
|
|
records.push({
|
|
...basePayload,
|
|
amount: centsToDecimalString(share.amountCents * amountSign),
|
|
payerId: share.payerId,
|
|
purchaseDate,
|
|
period: initialPeriod,
|
|
isSettled,
|
|
installmentCount: null,
|
|
currentInstallment: null,
|
|
recurrenceCount: null,
|
|
dueDate,
|
|
boletoPaymentDate:
|
|
input.paymentMethod === "Boleto" && isSettled
|
|
? (dueDate ?? purchaseDate)
|
|
: null,
|
|
});
|
|
});
|
|
|
|
return records;
|
|
}
|
|
|
|
async function ensureCategories(userId: string) {
|
|
const existing = await db.query.categories.findMany({
|
|
columns: { id: true, name: true },
|
|
where: eq(categories.userId, userId),
|
|
});
|
|
|
|
const existingNames = new Set(existing.map((item) => item.name));
|
|
const missingDefaults = DEFAULT_CATEGORIES.filter(
|
|
(category) => !existingNames.has(category.name),
|
|
);
|
|
|
|
if (missingDefaults.length > 0) {
|
|
await db.insert(categories).values(
|
|
missingDefaults.map((category) => ({
|
|
name: category.name,
|
|
type: category.type,
|
|
icon: category.icon,
|
|
userId,
|
|
})),
|
|
);
|
|
}
|
|
|
|
const refreshed = await db.query.categories.findMany({
|
|
columns: { id: true, name: true },
|
|
where: eq(categories.userId, userId),
|
|
});
|
|
|
|
return new Map(refreshed.map((category) => [category.name, category.id]));
|
|
}
|
|
|
|
async function ensureAdminPayer(targetUser: typeof user.$inferSelect) {
|
|
const existingAdmin = await db.query.payers.findFirst({
|
|
columns: { id: true, name: true },
|
|
where: and(
|
|
eq(payers.userId, targetUser.id),
|
|
eq(payers.role, PAYER_ROLE_ADMIN),
|
|
),
|
|
});
|
|
|
|
if (existingAdmin) {
|
|
return existingAdmin;
|
|
}
|
|
|
|
const name =
|
|
targetUser.name?.trim() ||
|
|
normalizeNameFromEmail(targetUser.email) ||
|
|
"Admin";
|
|
|
|
const [created] = await db
|
|
.insert(payers)
|
|
.values({
|
|
name,
|
|
email: targetUser.email ?? null,
|
|
avatarUrl: targetUser.image ?? DEFAULT_PAYER_AVATAR,
|
|
status: PAYER_STATUS,
|
|
note: null,
|
|
role: PAYER_ROLE_ADMIN,
|
|
isAutoSend: false,
|
|
shareCode: generateShareCode(),
|
|
userId: targetUser.id,
|
|
})
|
|
.returning({ id: payers.id, name: payers.name });
|
|
|
|
if (!created) {
|
|
throw new Error("Não foi possível criar o pagador admin do usuário.");
|
|
}
|
|
|
|
return created;
|
|
}
|
|
|
|
async function countByUserId(
|
|
table:
|
|
| typeof financialAccounts
|
|
| typeof cards
|
|
| typeof budgets
|
|
| typeof notes
|
|
| typeof invoices
|
|
| typeof transactions
|
|
| typeof inboxItems,
|
|
userColumn:
|
|
| typeof financialAccounts.userId
|
|
| typeof cards.userId
|
|
| typeof budgets.userId
|
|
| typeof notes.userId
|
|
| typeof invoices.userId
|
|
| typeof transactions.userId
|
|
| typeof inboxItems.userId,
|
|
userId: string,
|
|
) {
|
|
const [result] = await db
|
|
.select({ count: sql<number>`count(*)` })
|
|
.from(table)
|
|
.where(eq(userColumn, userId));
|
|
|
|
return Number(result?.count ?? 0);
|
|
}
|
|
|
|
async function assertFinancialSpaceIsEmpty(userId: string) {
|
|
const [
|
|
accountCount,
|
|
cardCount,
|
|
budgetCount,
|
|
noteCount,
|
|
invoiceCount,
|
|
transactionCount,
|
|
inboxCount,
|
|
extraPayerCount,
|
|
] = await Promise.all([
|
|
countByUserId(financialAccounts, financialAccounts.userId, userId),
|
|
countByUserId(cards, cards.userId, userId),
|
|
countByUserId(budgets, budgets.userId, userId),
|
|
countByUserId(notes, notes.userId, userId),
|
|
countByUserId(invoices, invoices.userId, userId),
|
|
countByUserId(transactions, transactions.userId, userId),
|
|
countByUserId(inboxItems, inboxItems.userId, userId),
|
|
db
|
|
.select({ count: sql<number>`count(*)` })
|
|
.from(payers)
|
|
.where(and(eq(payers.userId, userId), ne(payers.role, PAYER_ROLE_ADMIN)))
|
|
.then((rows) => Number(rows[0]?.count ?? 0)),
|
|
]);
|
|
|
|
const blockers = [
|
|
accountCount > 0 ? `${accountCount} conta(s)` : null,
|
|
cardCount > 0 ? `${cardCount} cartao(oes)` : null,
|
|
transactionCount > 0 ? `${transactionCount} lancamento(s)` : null,
|
|
invoiceCount > 0 ? `${invoiceCount} fatura(s)` : null,
|
|
budgetCount > 0 ? `${budgetCount} orcamento(s)` : null,
|
|
noteCount > 0 ? `${noteCount} anotacao(oes)` : null,
|
|
extraPayerCount > 0 ? `${extraPayerCount} pagador(es) extra(s)` : null,
|
|
inboxCount > 0 ? `${inboxCount} pre-lancamento(s)` : null,
|
|
].filter(Boolean);
|
|
|
|
if (blockers.length > 0) {
|
|
throw new Error(
|
|
`O usuário ${userId} não está com a conta zerada. Itens encontrados: ${blockers.join(", ")}.`,
|
|
);
|
|
}
|
|
}
|
|
|
|
async function seedInvoicesForCards(params: {
|
|
userId: string;
|
|
adminPayerId: string;
|
|
cardsByKey: Record<
|
|
string,
|
|
{
|
|
id: string;
|
|
name: string;
|
|
accountId: string;
|
|
dueDay: string;
|
|
closingDay: string;
|
|
}
|
|
>;
|
|
paymentCategoryId: string | undefined;
|
|
insertedTransactionRecords: TransactionInsert[];
|
|
}) {
|
|
const { userId, adminPayerId, cardsByKey, paymentCategoryId } = params;
|
|
const todayInfo = getBusinessTodayInfo();
|
|
const cardEntries = Object.entries(cardsByKey);
|
|
|
|
let createdInvoices = 0;
|
|
let createdInvoicePayments = 0;
|
|
|
|
await db.transaction(async (tx) => {
|
|
for (const [cardKey, card] of cardEntries) {
|
|
const cardPeriods = Array.from(
|
|
new Set(
|
|
params.insertedTransactionRecords
|
|
.filter((record) => record.cardId === card.id)
|
|
.map((record) => record.period ?? "")
|
|
.filter(Boolean),
|
|
),
|
|
).sort(comparePeriods);
|
|
|
|
const historicalPeriods = cardPeriods.filter(
|
|
(period) => comparePeriods(period, todayInfo.period) <= 0,
|
|
);
|
|
|
|
if (historicalPeriods.length === 0) {
|
|
continue;
|
|
}
|
|
|
|
const latestHistoricalPeriod =
|
|
historicalPeriods[historicalPeriods.length - 1];
|
|
|
|
for (const period of historicalPeriods) {
|
|
const shouldLeavePending =
|
|
cardKey === "ultravioleta" && period === latestHistoricalPeriod;
|
|
const status = shouldLeavePending
|
|
? INVOICE_PAYMENT_STATUS.PENDING
|
|
: INVOICE_PAYMENT_STATUS.PAID;
|
|
|
|
const existingInvoice = await tx.query.invoices.findFirst({
|
|
columns: { id: true },
|
|
where: and(
|
|
eq(invoices.userId, userId),
|
|
eq(invoices.cardId, card.id),
|
|
eq(invoices.period, period),
|
|
),
|
|
});
|
|
|
|
if (existingInvoice) {
|
|
await tx
|
|
.update(invoices)
|
|
.set({
|
|
paymentStatus: status,
|
|
})
|
|
.where(eq(invoices.id, existingInvoice.id));
|
|
} else {
|
|
await tx.insert(invoices).values({
|
|
cardId: card.id,
|
|
userId,
|
|
period,
|
|
paymentStatus: status,
|
|
});
|
|
}
|
|
|
|
await tx
|
|
.update(transactions)
|
|
.set({
|
|
isSettled: status === INVOICE_PAYMENT_STATUS.PAID,
|
|
})
|
|
.where(
|
|
and(
|
|
eq(transactions.userId, userId),
|
|
eq(transactions.cardId, card.id),
|
|
eq(transactions.period, period),
|
|
),
|
|
);
|
|
|
|
createdInvoices += 1;
|
|
|
|
if (status !== INVOICE_PAYMENT_STATUS.PAID) {
|
|
continue;
|
|
}
|
|
|
|
const [adminShareRow] = await tx
|
|
.select({
|
|
total: sql<number>`coalesce(sum(${transactions.amount}), 0)`,
|
|
})
|
|
.from(transactions)
|
|
.where(
|
|
and(
|
|
eq(transactions.userId, userId),
|
|
eq(transactions.cardId, card.id),
|
|
eq(transactions.period, period),
|
|
eq(transactions.payerId, adminPayerId),
|
|
),
|
|
);
|
|
|
|
const adminShare = Number(adminShareRow?.total ?? 0);
|
|
const adminPayableAmount = Math.abs(Math.min(adminShare, 0));
|
|
|
|
if (adminPayableAmount <= 0) {
|
|
continue;
|
|
}
|
|
|
|
const paymentDate = dateForPeriodDay(period, Number(card.dueDay));
|
|
const paymentNote = buildInvoicePaymentNote(card.id, period);
|
|
|
|
await tx.insert(transactions).values({
|
|
condition: "À vista",
|
|
name: `Pagamento fatura - ${card.name}`,
|
|
paymentMethod: "Pix",
|
|
note: paymentNote,
|
|
amount: `-${adminPayableAmount.toFixed(2)}`,
|
|
purchaseDate: parseLocalDateString(paymentDate),
|
|
transactionType: "Despesa",
|
|
period,
|
|
isSettled: true,
|
|
userId,
|
|
accountId: card.accountId,
|
|
categoryId: paymentCategoryId ?? null,
|
|
payerId: adminPayerId,
|
|
});
|
|
|
|
createdInvoicePayments += 1;
|
|
}
|
|
}
|
|
});
|
|
|
|
return {
|
|
createdInvoices,
|
|
createdInvoicePayments,
|
|
};
|
|
}
|
|
|
|
async function main() {
|
|
const options = parseArgs(process.argv.slice(2));
|
|
|
|
if (!process.env.DATABASE_URL) {
|
|
throw new Error("DATABASE_URL não está configurada no ambiente.");
|
|
}
|
|
|
|
const logoOptions = await loadLogoOptions();
|
|
const avatarOptions = await loadAvatarOptions();
|
|
const businessToday = getBusinessTodayInfo();
|
|
const summary: SeedSummary = {
|
|
payers: 0,
|
|
accounts: 0,
|
|
cards: 0,
|
|
notes: 0,
|
|
budgets: 0,
|
|
transactions: 0,
|
|
invoices: 0,
|
|
invoicePayments: 0,
|
|
inboxItems: 0,
|
|
};
|
|
|
|
const targetUser = await db.query.user.findFirst({
|
|
where: eq(user.id, options.userId),
|
|
});
|
|
|
|
if (!targetUser) {
|
|
throw new Error(`Usuário ${options.userId} não foi encontrado.`);
|
|
}
|
|
|
|
await assertFinancialSpaceIsEmpty(targetUser.id);
|
|
const adminPayer = await ensureAdminPayer(targetUser);
|
|
|
|
const categoriesByName = await ensureCategories(targetUser.id);
|
|
|
|
const getCategoryId = (name: string) => {
|
|
const categoryId = categoriesByName.get(name);
|
|
if (!categoryId) {
|
|
throw new Error(`Categoria obrigatória não encontrada: ${name}.`);
|
|
}
|
|
return categoryId;
|
|
};
|
|
|
|
const periods = Array.from({ length: options.months }, (_, index) =>
|
|
addMonthsToPeriod(options.startPeriod, index),
|
|
);
|
|
|
|
const firstPeriod = periods[0];
|
|
const secondPeriod = periods[1] ?? periods[0];
|
|
const thirdPeriod = periods[2] ?? periods[periods.length - 1] ?? periods[0];
|
|
const middlePeriod = periods[Math.floor(periods.length / 2)] ?? periods[0];
|
|
const lastPeriod = periods[periods.length - 1] ?? periods[0];
|
|
const firstDayOfSeed = dateForPeriodDay(firstPeriod, 1);
|
|
|
|
const preferredAvatar = (candidates: string[]) =>
|
|
pickOption(avatarOptions, candidates, DEFAULT_PAYER_AVATAR);
|
|
const preferredLogo = (candidates: string[], fallback: string) =>
|
|
pickOption(logoOptions, candidates, fallback);
|
|
|
|
const createdPayers: Record<string, string> = {
|
|
admin: adminPayer.id,
|
|
};
|
|
const createdAccounts: Record<string, string> = {};
|
|
const createdCards: Record<
|
|
string,
|
|
{
|
|
id: string;
|
|
name: string;
|
|
accountId: string;
|
|
closingDay: string;
|
|
dueDay: string;
|
|
}
|
|
> = {};
|
|
|
|
await db.transaction(async (tx) => {
|
|
const extraPayerDefinitions = [
|
|
{
|
|
key: "marina",
|
|
name: "Marina Oliveira",
|
|
email: "marina.oliveira@exemplo.com",
|
|
avatarUrl: preferredAvatar(["4825038.png", "4825051.png"]),
|
|
note: "Divide as despesas da casa e do mercado.",
|
|
isAutoSend: true,
|
|
},
|
|
{
|
|
key: "eduardo",
|
|
name: "Eduardo Lima",
|
|
email: "eduardo.lima@exemplo.com",
|
|
avatarUrl: preferredAvatar(["4825108.png", "4825123.png"]),
|
|
note: "Costuma rachar viagens e presentes em familia.",
|
|
isAutoSend: false,
|
|
},
|
|
] as const;
|
|
|
|
for (const definition of extraPayerDefinitions) {
|
|
const [created] = await tx
|
|
.insert(payers)
|
|
.values({
|
|
name: definition.name,
|
|
email: definition.email,
|
|
avatarUrl: definition.avatarUrl,
|
|
status: PAYER_STATUS,
|
|
note: definition.note,
|
|
role: PAYER_ROLE_THIRD_PARTY,
|
|
isAutoSend: definition.isAutoSend,
|
|
shareCode: generateShareCode(),
|
|
userId: targetUser.id,
|
|
})
|
|
.returning({ id: payers.id });
|
|
|
|
if (!created) {
|
|
throw new Error(`Falha ao criar o pagador ${definition.name}.`);
|
|
}
|
|
|
|
createdPayers[definition.key] = created.id;
|
|
summary.payers += 1;
|
|
}
|
|
|
|
const accountDefinitions = [
|
|
{
|
|
key: "nubank",
|
|
name: "Nubank",
|
|
accountType: ACCOUNT_TYPES.DIGITAL_WALLET,
|
|
status: ACCOUNT_STATUS.ACTIVE,
|
|
logo: preferredLogo(["nubank.png"], "nubank.png"),
|
|
note: "Conta principal para salario, mercado e despesas do dia a dia.",
|
|
initialBalance: "4200.00",
|
|
},
|
|
{
|
|
key: "itau",
|
|
name: "Itaú Personnalité",
|
|
accountType: ACCOUNT_TYPES.CHECKING,
|
|
status: ACCOUNT_STATUS.ACTIVE,
|
|
logo: preferredLogo(["itaupersonnalite.png", "itau.png"], "itau.png"),
|
|
note: "Conta de apoio para boletos, investimentos e pagamentos maiores.",
|
|
initialBalance: "1850.00",
|
|
},
|
|
{
|
|
key: "mercado-pago",
|
|
name: "Mercado Pago",
|
|
accountType: ACCOUNT_TYPES.DIGITAL_WALLET,
|
|
status: ACCOUNT_STATUS.ACTIVE,
|
|
logo: preferredLogo(["mercadopago.png"], "mercadopago.png"),
|
|
note: "Carteira usada para corridas, pequenos gastos e pix instantaneo.",
|
|
initialBalance: "350.00",
|
|
},
|
|
] as const;
|
|
|
|
for (const definition of accountDefinitions) {
|
|
const [created] = await tx
|
|
.insert(financialAccounts)
|
|
.values({
|
|
name: definition.name,
|
|
accountType: definition.accountType,
|
|
status: definition.status,
|
|
note: definition.note,
|
|
logo: definition.logo,
|
|
initialBalance: definition.initialBalance,
|
|
excludeFromBalance: false,
|
|
excludeInitialBalanceFromIncome: true,
|
|
userId: targetUser.id,
|
|
})
|
|
.returning({ id: financialAccounts.id });
|
|
|
|
if (!created) {
|
|
throw new Error(`Falha ao criar a conta ${definition.name}.`);
|
|
}
|
|
|
|
createdAccounts[definition.key] = created.id;
|
|
summary.accounts += 1;
|
|
|
|
await tx.insert(transactions).values({
|
|
condition: INITIAL_BALANCE_CONDITION,
|
|
name: `Saldo inicial - ${definition.name}`,
|
|
paymentMethod: INITIAL_BALANCE_PAYMENT_METHOD,
|
|
note: INITIAL_BALANCE_NOTE,
|
|
amount: definition.initialBalance,
|
|
purchaseDate: parseLocalDateString(firstDayOfSeed),
|
|
transactionType: INITIAL_BALANCE_TRANSACTION_TYPE,
|
|
period: firstPeriod,
|
|
isSettled: true,
|
|
userId: targetUser.id,
|
|
accountId: created.id,
|
|
categoryId: getCategoryId(INITIAL_BALANCE_CATEGORY_NAME),
|
|
payerId: adminPayer.id,
|
|
});
|
|
|
|
summary.transactions += 1;
|
|
}
|
|
|
|
const cardDefinitions = [
|
|
{
|
|
key: "ultravioleta",
|
|
name: "Nubank Ultravioleta",
|
|
brand: CARD_BRANDS.MASTERCARD,
|
|
status: CARD_STATUS,
|
|
closingDay: "25",
|
|
dueDay: "03",
|
|
note: "Cartao principal para assinaturas, delivery e compras parceladas.",
|
|
limit: "12000.00",
|
|
logo: preferredLogo(
|
|
["nubank-ultravioleta.png", "nubank.png"],
|
|
"nubank.png",
|
|
),
|
|
accountId: createdAccounts.nubank,
|
|
},
|
|
{
|
|
key: "itaucard",
|
|
name: "Pão de Açúcar Itaucard",
|
|
brand: CARD_BRANDS.VISA,
|
|
status: CARD_STATUS,
|
|
closingDay: "15",
|
|
dueDay: "22",
|
|
note: "Cartao usado para mercado, compras maiores e viagens.",
|
|
limit: "8500.00",
|
|
logo: preferredLogo(["pao-acucar.png", "itau.png"], "itau.png"),
|
|
accountId: createdAccounts.itau,
|
|
},
|
|
] as const;
|
|
|
|
for (const definition of cardDefinitions) {
|
|
const [created] = await tx
|
|
.insert(cards)
|
|
.values({
|
|
name: definition.name,
|
|
brand: definition.brand,
|
|
status: definition.status,
|
|
closingDay: definition.closingDay,
|
|
dueDay: definition.dueDay,
|
|
note: definition.note,
|
|
limit: definition.limit,
|
|
logo: definition.logo,
|
|
accountId: definition.accountId,
|
|
userId: targetUser.id,
|
|
})
|
|
.returning({ id: cards.id });
|
|
|
|
if (!created) {
|
|
throw new Error(`Falha ao criar o cartao ${definition.name}.`);
|
|
}
|
|
|
|
createdCards[definition.key] = {
|
|
id: created.id,
|
|
name: definition.name,
|
|
accountId: definition.accountId,
|
|
closingDay: definition.closingDay,
|
|
dueDay: definition.dueDay,
|
|
};
|
|
summary.cards += 1;
|
|
}
|
|
|
|
const noteDefinitions = [
|
|
{
|
|
title: "Planejar viagem de julho",
|
|
type: "nota" as const,
|
|
description:
|
|
"Separar hospedagem, passagens e gastos previstos da viagem para Salvador. Meta: manter tudo abaixo de R$ 4.500.",
|
|
tasks: null,
|
|
archived: false,
|
|
},
|
|
{
|
|
title: "Pendencias do apartamento",
|
|
type: "tarefa" as const,
|
|
description: null,
|
|
tasks: JSON.stringify([
|
|
{
|
|
id: randomUUID(),
|
|
text: "Revisar reajuste do aluguel",
|
|
completed: true,
|
|
},
|
|
{
|
|
id: randomUUID(),
|
|
text: "Separar comprovantes do condominio",
|
|
completed: false,
|
|
},
|
|
{
|
|
id: randomUUID(),
|
|
text: "Confirmar vistoria do ar-condicionado",
|
|
completed: false,
|
|
},
|
|
]),
|
|
archived: false,
|
|
},
|
|
{
|
|
title: "Renegociar seguro do carro",
|
|
type: "nota" as const,
|
|
description:
|
|
"Pesquisar entre Porto, Azul e Youse antes do vencimento. Prioridade: franquia menor e assistencia 24h.",
|
|
tasks: null,
|
|
archived: false,
|
|
},
|
|
] as const;
|
|
|
|
await tx.insert(notes).values(
|
|
noteDefinitions.map((noteItem) => ({
|
|
title: noteItem.title,
|
|
type: noteItem.type,
|
|
description: noteItem.description,
|
|
tasks: noteItem.tasks,
|
|
archived: noteItem.archived,
|
|
userId: targetUser.id,
|
|
})),
|
|
);
|
|
summary.notes += noteDefinitions.length;
|
|
|
|
const budgetDefinitions = [
|
|
{ categoryName: "Mercado", baseAmount: 1500 },
|
|
{ categoryName: "Restaurantes", baseAmount: 480 },
|
|
{ categoryName: "Transporte", baseAmount: 620 },
|
|
{ categoryName: "Moradia", baseAmount: 3200 },
|
|
{ categoryName: "Lazer", baseAmount: 420 },
|
|
{ categoryName: "Assinaturas", baseAmount: 240 },
|
|
] as const;
|
|
|
|
const budgetRows = periods.flatMap((period, index) =>
|
|
budgetDefinitions.map((budgetItem) => ({
|
|
amount: (budgetItem.baseAmount + index * 15).toFixed(2),
|
|
period,
|
|
userId: targetUser.id,
|
|
categoryId: getCategoryId(budgetItem.categoryName),
|
|
})),
|
|
);
|
|
|
|
await tx.insert(budgets).values(budgetRows);
|
|
summary.budgets += budgetRows.length;
|
|
});
|
|
|
|
const seedTransactionRecords: TransactionInsert[] = [];
|
|
|
|
const createRecords = (input: SeedTransactionInput) => {
|
|
seedTransactionRecords.push(
|
|
...createTransactionRecords(input, targetUser.id, businessToday.date),
|
|
);
|
|
};
|
|
|
|
createRecords({
|
|
name: "Salário - OpenMonetis Labs",
|
|
amount: 7800,
|
|
purchaseDate: dateForPeriodDay(firstPeriod, 5),
|
|
transactionType: "Receita",
|
|
condition: "Recorrente",
|
|
paymentMethod: "Transferência bancária",
|
|
accountId: createdAccounts.nubank,
|
|
categoryId: getCategoryId("Salário"),
|
|
payerId: adminPayer.id,
|
|
recurrenceCount: options.months,
|
|
note: "Salario mensal recebido via TED.",
|
|
});
|
|
|
|
createRecords({
|
|
name: "Aluguel - Edifício Aurora",
|
|
amount: 2800,
|
|
purchaseDate: dateForPeriodDay(firstPeriod, 5),
|
|
transactionType: "Despesa",
|
|
condition: "Recorrente",
|
|
paymentMethod: "Pix",
|
|
accountId: createdAccounts.nubank,
|
|
categoryId: getCategoryId("Moradia"),
|
|
payerId: adminPayer.id,
|
|
secondaryPayerId: createdPayers.marina,
|
|
isSplit: true,
|
|
primarySplitAmount: 1400,
|
|
secondarySplitAmount: 1400,
|
|
recurrenceCount: options.months,
|
|
dueDate: dateForPeriodDay(firstPeriod, 8),
|
|
note: "Despesa fixa dividida com Marina.",
|
|
});
|
|
|
|
createRecords({
|
|
name: "Vivo Fibra",
|
|
amount: 129.9,
|
|
purchaseDate: dateForPeriodDay(firstPeriod, 2),
|
|
transactionType: "Despesa",
|
|
condition: "Recorrente",
|
|
paymentMethod: "Boleto",
|
|
accountId: createdAccounts.itau,
|
|
categoryId: getCategoryId("Internet"),
|
|
payerId: adminPayer.id,
|
|
recurrenceCount: options.months,
|
|
dueDate: dateForPeriodDay(firstPeriod, 12),
|
|
note: "Internet da casa.",
|
|
});
|
|
|
|
createRecords({
|
|
name: "Conta de luz - Enel",
|
|
amount: 186.4,
|
|
purchaseDate: dateForPeriodDay(firstPeriod, 3),
|
|
transactionType: "Despesa",
|
|
condition: "Recorrente",
|
|
paymentMethod: "Boleto",
|
|
accountId: createdAccounts.itau,
|
|
categoryId: getCategoryId("Energia e água"),
|
|
payerId: adminPayer.id,
|
|
recurrenceCount: options.months,
|
|
dueDate: dateForPeriodDay(firstPeriod, 18),
|
|
note: "Conta mensal de energia.",
|
|
});
|
|
|
|
createRecords({
|
|
name: "Netflix",
|
|
amount: 55.9,
|
|
purchaseDate: dateForPeriodDay(firstPeriod, 9),
|
|
transactionType: "Despesa",
|
|
condition: "Recorrente",
|
|
paymentMethod: "Cartão de crédito",
|
|
cardId: createdCards.ultravioleta.id,
|
|
categoryId: getCategoryId("Assinaturas"),
|
|
payerId: adminPayer.id,
|
|
recurrenceCount: options.months,
|
|
cardMeta: createdCards.ultravioleta,
|
|
note: "Assinatura recorrente.",
|
|
});
|
|
|
|
createRecords({
|
|
name: "Smart Fit",
|
|
amount: 129.9,
|
|
purchaseDate: dateForPeriodDay(firstPeriod, 11),
|
|
transactionType: "Despesa",
|
|
condition: "Recorrente",
|
|
paymentMethod: "Cartão de crédito",
|
|
cardId: createdCards.ultravioleta.id,
|
|
categoryId: getCategoryId("Saúde"),
|
|
payerId: adminPayer.id,
|
|
recurrenceCount: options.months,
|
|
cardMeta: createdCards.ultravioleta,
|
|
note: "Plano black mensal.",
|
|
});
|
|
|
|
createRecords({
|
|
name: "Spotify",
|
|
amount: 21.9,
|
|
purchaseDate: dateForPeriodDay(firstPeriod, 8),
|
|
transactionType: "Despesa",
|
|
condition: "Recorrente",
|
|
paymentMethod: "Cartão de crédito",
|
|
cardId: createdCards.ultravioleta.id,
|
|
categoryId: getCategoryId("Assinaturas"),
|
|
payerId: adminPayer.id,
|
|
recurrenceCount: options.months,
|
|
cardMeta: createdCards.ultravioleta,
|
|
note: "Assinatura premium individual.",
|
|
});
|
|
|
|
createRecords({
|
|
name: "Plano de saúde - Amil",
|
|
amount: 389,
|
|
purchaseDate: dateForPeriodDay(firstPeriod, 4),
|
|
transactionType: "Despesa",
|
|
condition: "Recorrente",
|
|
paymentMethod: "Boleto",
|
|
accountId: createdAccounts.itau,
|
|
categoryId: getCategoryId("Saúde"),
|
|
payerId: adminPayer.id,
|
|
recurrenceCount: options.months,
|
|
dueDate: dateForPeriodDay(firstPeriod, 10),
|
|
note: "Plano coletivo com coparticipação.",
|
|
});
|
|
|
|
createRecords({
|
|
name: "Seguro auto - Porto Seguro",
|
|
amount: 278.5,
|
|
purchaseDate: dateForPeriodDay(firstPeriod, 6),
|
|
transactionType: "Despesa",
|
|
condition: "Recorrente",
|
|
paymentMethod: "Boleto",
|
|
accountId: createdAccounts.itau,
|
|
categoryId: getCategoryId("Transporte"),
|
|
payerId: adminPayer.id,
|
|
recurrenceCount: options.months,
|
|
dueDate: dateForPeriodDay(firstPeriod, 15),
|
|
note: "Parcela mensal do seguro do carro.",
|
|
});
|
|
|
|
createRecords({
|
|
name: "Condomínio - Edifício Aurora",
|
|
amount: 680,
|
|
purchaseDate: dateForPeriodDay(firstPeriod, 1),
|
|
transactionType: "Despesa",
|
|
condition: "Recorrente",
|
|
paymentMethod: "Pix",
|
|
accountId: createdAccounts.nubank,
|
|
categoryId: getCategoryId("Moradia"),
|
|
payerId: adminPayer.id,
|
|
recurrenceCount: options.months,
|
|
dueDate: dateForPeriodDay(firstPeriod, 10),
|
|
note: "Taxa condominial mensal.",
|
|
});
|
|
|
|
for (const [index, period] of periods.entries()) {
|
|
const marketAmount = 420 + index * 37.5;
|
|
const uberAmount = 32 + index * 4.75;
|
|
const fuelAmount = 170 + index * 18.2;
|
|
const ifoodAmount = 62 + index * 7.3;
|
|
const investmentYield = 68 + index * 4.4;
|
|
|
|
createRecords({
|
|
name: "Assaí Atacadista",
|
|
amount: marketAmount,
|
|
purchaseDate: dateForPeriodDay(period, 6),
|
|
transactionType: "Despesa",
|
|
condition: "À vista",
|
|
paymentMethod: "Pix",
|
|
accountId: createdAccounts.nubank,
|
|
categoryId: getCategoryId("Mercado"),
|
|
payerId: adminPayer.id,
|
|
secondaryPayerId: createdPayers.marina,
|
|
isSplit: true,
|
|
primarySplitAmount: marketAmount / 2,
|
|
secondarySplitAmount: marketAmount / 2,
|
|
note: "Compra grande do mes dividida com Marina.",
|
|
});
|
|
|
|
createRecords({
|
|
name: "Uber",
|
|
amount: uberAmount,
|
|
purchaseDate: dateForPeriodDay(period, 14),
|
|
transactionType: "Despesa",
|
|
condition: "À vista",
|
|
paymentMethod: "Pix",
|
|
accountId: createdAccounts["mercado-pago"],
|
|
categoryId: getCategoryId("Transporte"),
|
|
payerId: adminPayer.id,
|
|
note: "Corridas do dia a dia.",
|
|
});
|
|
|
|
createRecords({
|
|
name: "Posto Shell",
|
|
amount: fuelAmount,
|
|
purchaseDate: dateForPeriodDay(period, 18),
|
|
transactionType: "Despesa",
|
|
condition: "À vista",
|
|
paymentMethod: "Cartão de débito",
|
|
accountId: createdAccounts.itau,
|
|
categoryId: getCategoryId("Transporte"),
|
|
payerId: adminPayer.id,
|
|
note: "Abastecimento mensal.",
|
|
});
|
|
|
|
createRecords({
|
|
name: "iFood",
|
|
amount: ifoodAmount,
|
|
purchaseDate: dateForPeriodDay(period, 20),
|
|
transactionType: "Despesa",
|
|
condition: "À vista",
|
|
paymentMethod: "Cartão de crédito",
|
|
cardId: createdCards.ultravioleta.id,
|
|
categoryId: getCategoryId("Delivery"),
|
|
payerId: adminPayer.id,
|
|
cardMeta: createdCards.ultravioleta,
|
|
note: "Pedidos de jantar.",
|
|
});
|
|
|
|
createRecords({
|
|
name: "Rendimento CDB Itaú",
|
|
amount: investmentYield,
|
|
purchaseDate: dateForPeriodDay(period, 27),
|
|
transactionType: "Receita",
|
|
condition: "À vista",
|
|
paymentMethod: "Transferência bancária",
|
|
accountId: createdAccounts.itau,
|
|
categoryId: getCategoryId("Investimentos"),
|
|
payerId: adminPayer.id,
|
|
note: "Rendimento liquido do CDB.",
|
|
});
|
|
|
|
if (index % 2 === 0) {
|
|
createRecords({
|
|
name: "Amazon Brasil",
|
|
amount: 148 + index * 12.8,
|
|
purchaseDate: dateForPeriodDay(period, 22),
|
|
transactionType: "Despesa",
|
|
condition: "À vista",
|
|
paymentMethod: "Cartão de crédito",
|
|
cardId: createdCards.itaucard.id,
|
|
categoryId: getCategoryId("Compras"),
|
|
payerId: adminPayer.id,
|
|
cardMeta: createdCards.itaucard,
|
|
note: "Itens de casa e pequenos eletrônicos.",
|
|
});
|
|
}
|
|
|
|
if (index % 2 === 1) {
|
|
createRecords({
|
|
name: "Freela - Clínica Aurora",
|
|
amount: 1450 + index * 120,
|
|
purchaseDate: dateForPeriodDay(period, 24),
|
|
transactionType: "Receita",
|
|
condition: "À vista",
|
|
paymentMethod: "Pix",
|
|
accountId: createdAccounts.nubank,
|
|
categoryId: getCategoryId("Freelance"),
|
|
payerId: adminPayer.id,
|
|
note: "Projeto extra de landing page.",
|
|
});
|
|
}
|
|
|
|
if (index % 3 === 0) {
|
|
createRecords({
|
|
name: "Coco Bambu",
|
|
amount: 212 + index * 9.5,
|
|
purchaseDate: dateForPeriodDay(period, 25),
|
|
transactionType: "Despesa",
|
|
condition: "À vista",
|
|
paymentMethod: "Cartão de crédito",
|
|
cardId: createdCards.itaucard.id,
|
|
categoryId: getCategoryId("Restaurantes"),
|
|
payerId: adminPayer.id,
|
|
cardMeta: createdCards.itaucard,
|
|
note: "Jantar de fim de semana.",
|
|
});
|
|
}
|
|
|
|
if (index % 2 === 0) {
|
|
createRecords({
|
|
name: "Drogasil",
|
|
amount: 46 + index * 6.25,
|
|
purchaseDate: dateForPeriodDay(period, 12),
|
|
transactionType: "Despesa",
|
|
condition: "À vista",
|
|
paymentMethod: "Cartão de débito",
|
|
accountId: createdAccounts.nubank,
|
|
categoryId: getCategoryId("Saúde"),
|
|
payerId: adminPayer.id,
|
|
note: "Remédios e itens de farmácia.",
|
|
});
|
|
}
|
|
|
|
if (index % 2 === 1) {
|
|
createRecords({
|
|
name: "Ultrafarma",
|
|
amount: 52 + index * 5.5,
|
|
purchaseDate: dateForPeriodDay(period, 16),
|
|
transactionType: "Despesa",
|
|
condition: "À vista",
|
|
paymentMethod: "Cartão de débito",
|
|
accountId: createdAccounts.nubank,
|
|
categoryId: getCategoryId("Saúde"),
|
|
payerId: adminPayer.id,
|
|
note: "Medicamentos e suplementos.",
|
|
});
|
|
}
|
|
|
|
createRecords({
|
|
name: "Pão de Açúcar",
|
|
amount: 280 + index * 22.4,
|
|
purchaseDate: dateForPeriodDay(period, 10),
|
|
transactionType: "Despesa",
|
|
condition: "À vista",
|
|
paymentMethod: "Cartão de crédito",
|
|
cardId: createdCards.itaucard.id,
|
|
categoryId: getCategoryId("Mercado"),
|
|
payerId: adminPayer.id,
|
|
cardMeta: createdCards.itaucard,
|
|
note: "Compra semanal de hortifruti e frios.",
|
|
});
|
|
|
|
createRecords({
|
|
name: "McDonald's",
|
|
amount: 42 + index * 3.8,
|
|
purchaseDate: dateForPeriodDay(period, 16),
|
|
transactionType: "Despesa",
|
|
condition: "À vista",
|
|
paymentMethod: "Pré-Pago | VR/VA",
|
|
accountId: createdAccounts["mercado-pago"],
|
|
categoryId: getCategoryId("Alimentação"),
|
|
payerId: adminPayer.id,
|
|
note: "Almoço rápido no intervalo.",
|
|
});
|
|
|
|
if (index % 2 === 0) {
|
|
createRecords({
|
|
name: "Cinemark",
|
|
amount: 130 + index * 8.5,
|
|
purchaseDate: dateForPeriodDay(period, 21),
|
|
transactionType: "Despesa",
|
|
condition: "À vista",
|
|
paymentMethod: "Cartão de crédito",
|
|
cardId: createdCards.itaucard.id,
|
|
categoryId: getCategoryId("Lazer"),
|
|
payerId: adminPayer.id,
|
|
cardMeta: createdCards.itaucard,
|
|
note: "Ingresso + pipoca pra dois.",
|
|
});
|
|
}
|
|
}
|
|
|
|
createRecords({
|
|
name: "Notebook Dell Inspiron",
|
|
amount: 7199.2,
|
|
purchaseDate: dateForPeriodDay(firstPeriod, 28),
|
|
transactionType: "Despesa",
|
|
condition: "Parcelado",
|
|
paymentMethod: "Cartão de crédito",
|
|
cardId: createdCards.itaucard.id,
|
|
categoryId: getCategoryId("Compras"),
|
|
payerId: adminPayer.id,
|
|
installmentCount: 8,
|
|
cardMeta: createdCards.itaucard,
|
|
note: "Troca do notebook do home office.",
|
|
});
|
|
|
|
createRecords({
|
|
name: "Ar-condicionado Springer Midea",
|
|
amount: 2899.8,
|
|
purchaseDate: dateForPeriodDay(secondPeriod, 26),
|
|
transactionType: "Despesa",
|
|
condition: "Parcelado",
|
|
paymentMethod: "Cartão de crédito",
|
|
cardId: createdCards.ultravioleta.id,
|
|
categoryId: getCategoryId("Moradia"),
|
|
payerId: adminPayer.id,
|
|
installmentCount: 6,
|
|
cardMeta: createdCards.ultravioleta,
|
|
note: "Compra feita para o quarto do casal.",
|
|
});
|
|
|
|
createRecords({
|
|
name: "Passagem LATAM - Salvador",
|
|
amount: 2140.5,
|
|
purchaseDate: dateForPeriodDay(thirdPeriod, 16),
|
|
transactionType: "Despesa",
|
|
condition: "Parcelado",
|
|
paymentMethod: "Cartão de crédito",
|
|
cardId: createdCards.ultravioleta.id,
|
|
categoryId: getCategoryId("Viagem"),
|
|
payerId: adminPayer.id,
|
|
secondaryPayerId: createdPayers.eduardo,
|
|
isSplit: true,
|
|
primarySplitAmount: 1498.35,
|
|
secondarySplitAmount: 642.15,
|
|
installmentCount: 5,
|
|
cardMeta: createdCards.ultravioleta,
|
|
note: "Viagem dividida com Eduardo.",
|
|
});
|
|
|
|
createRecords({
|
|
name: "Reembolso plano de saúde",
|
|
amount: 185.4,
|
|
purchaseDate: dateForPeriodDay(middlePeriod, 26),
|
|
transactionType: "Receita",
|
|
condition: "À vista",
|
|
paymentMethod: "Pix",
|
|
accountId: createdAccounts.nubank,
|
|
categoryId: getCategoryId("Reembolso"),
|
|
payerId: adminPayer.id,
|
|
note: "Reembolso de consulta realizada no mes anterior.",
|
|
});
|
|
|
|
createRecords({
|
|
name: "Consulta dentista - Dra. Ana",
|
|
amount: 240,
|
|
purchaseDate: dateForPeriodDay(middlePeriod, 9),
|
|
transactionType: "Despesa",
|
|
condition: "À vista",
|
|
paymentMethod: "Boleto",
|
|
accountId: createdAccounts.itau,
|
|
categoryId: getCategoryId("Saúde"),
|
|
payerId: adminPayer.id,
|
|
dueDate: dateForPeriodDay(middlePeriod, 17),
|
|
note: "Procedimento odontológico.",
|
|
});
|
|
|
|
createRecords({
|
|
name: "IPVA 2026 - parcela única",
|
|
amount: 684.32,
|
|
purchaseDate: dateForPeriodDay(lastPeriod, 10),
|
|
transactionType: "Despesa",
|
|
condition: "À vista",
|
|
paymentMethod: "Boleto",
|
|
accountId: createdAccounts.itau,
|
|
categoryId: getCategoryId("Transporte"),
|
|
payerId: adminPayer.id,
|
|
dueDate: dateForPeriodDay(lastPeriod, 25),
|
|
note: "Mantido em aberto para testar lembretes e listagem de boletos.",
|
|
settlementBehavior: "open",
|
|
});
|
|
|
|
createRecords({
|
|
name: "iPhone 16 Pro",
|
|
amount: 9299.1,
|
|
purchaseDate: dateForPeriodDay(secondPeriod, 14),
|
|
transactionType: "Despesa",
|
|
condition: "Parcelado",
|
|
paymentMethod: "Cartão de crédito",
|
|
cardId: createdCards.itaucard.id,
|
|
categoryId: getCategoryId("Compras"),
|
|
payerId: adminPayer.id,
|
|
installmentCount: 12,
|
|
cardMeta: createdCards.itaucard,
|
|
note: "Troca do celular, 12x sem juros.",
|
|
});
|
|
|
|
createRecords({
|
|
name: "Alura - assinatura anual",
|
|
amount: 1499.9,
|
|
purchaseDate: dateForPeriodDay(firstPeriod, 20),
|
|
transactionType: "Despesa",
|
|
condition: "À vista",
|
|
paymentMethod: "Cartão de crédito",
|
|
cardId: createdCards.ultravioleta.id,
|
|
categoryId: getCategoryId("Educação"),
|
|
payerId: adminPayer.id,
|
|
cardMeta: createdCards.ultravioleta,
|
|
note: "Plano anual para desenvolvimento web e mobile.",
|
|
});
|
|
|
|
createRecords({
|
|
name: "Presente - aniversário Eduardo",
|
|
amount: 189,
|
|
purchaseDate: dateForPeriodDay(thirdPeriod, 11),
|
|
transactionType: "Despesa",
|
|
condition: "À vista",
|
|
paymentMethod: "Pix",
|
|
accountId: createdAccounts.nubank,
|
|
categoryId: getCategoryId("Presentes"),
|
|
payerId: adminPayer.id,
|
|
note: "Presente de aniversário para o Eduardo.",
|
|
});
|
|
|
|
createRecords({
|
|
name: "Zara - jaqueta e calça",
|
|
amount: 599.8,
|
|
purchaseDate: dateForPeriodDay(middlePeriod, 17),
|
|
transactionType: "Despesa",
|
|
condition: "À vista",
|
|
paymentMethod: "Cartão de crédito",
|
|
cardId: createdCards.ultravioleta.id,
|
|
categoryId: getCategoryId("Vestuário"),
|
|
payerId: adminPayer.id,
|
|
cardMeta: createdCards.ultravioleta,
|
|
note: "Roupas de inverno.",
|
|
});
|
|
|
|
createRecords({
|
|
name: "Show Criolo - Audio Club",
|
|
amount: 320,
|
|
purchaseDate: dateForPeriodDay(middlePeriod, 8),
|
|
transactionType: "Despesa",
|
|
condition: "Parcelado",
|
|
paymentMethod: "Cartão de crédito",
|
|
cardId: createdCards.ultravioleta.id,
|
|
categoryId: getCategoryId("Lazer"),
|
|
payerId: adminPayer.id,
|
|
secondaryPayerId: createdPayers.marina,
|
|
isSplit: true,
|
|
primarySplitAmount: 160,
|
|
secondarySplitAmount: 160,
|
|
installmentCount: 2,
|
|
cardMeta: createdCards.ultravioleta,
|
|
note: "Ingressos divididos com a Marina.",
|
|
});
|
|
|
|
await db.insert(transactions).values(seedTransactionRecords);
|
|
summary.transactions += seedTransactionRecords.length;
|
|
|
|
const { createdInvoices, createdInvoicePayments } =
|
|
await seedInvoicesForCards({
|
|
userId: targetUser.id,
|
|
adminPayerId: adminPayer.id,
|
|
cardsByKey: createdCards,
|
|
paymentCategoryId: categoriesByName.get("Pagamentos"),
|
|
insertedTransactionRecords: seedTransactionRecords,
|
|
});
|
|
|
|
summary.invoices += createdInvoices;
|
|
summary.invoicePayments += createdInvoicePayments;
|
|
summary.transactions += createdInvoicePayments;
|
|
|
|
const inboxItemsData = [
|
|
{
|
|
userId: targetUser.id,
|
|
sourceApp: "com.nu.production",
|
|
sourceAppName: "Nubank",
|
|
originalTitle: "Compra aprovada",
|
|
originalText:
|
|
"Compra de R$ 73,90 aprovada no cartão Ultravioleta em RAPPI*RAPPI BR",
|
|
notificationTimestamp: parseLocalDateString(
|
|
dateForPeriodDay(lastPeriod, 3),
|
|
),
|
|
parsedName: "Rappi",
|
|
parsedAmount: "73.90",
|
|
status: "pending" as const,
|
|
transactionId: null,
|
|
processedAt: null,
|
|
discardedAt: null,
|
|
},
|
|
{
|
|
userId: targetUser.id,
|
|
sourceApp: "com.nu.production",
|
|
sourceAppName: "Nubank",
|
|
originalTitle: "Pix enviado",
|
|
originalText: "Você enviou R$ 210,00 via Pix para Marina Oliveira.",
|
|
notificationTimestamp: parseLocalDateString(
|
|
dateForPeriodDay(lastPeriod, 5),
|
|
),
|
|
parsedName: "Marina Oliveira",
|
|
parsedAmount: "210.00",
|
|
status: "pending" as const,
|
|
transactionId: null,
|
|
processedAt: null,
|
|
discardedAt: null,
|
|
},
|
|
{
|
|
userId: targetUser.id,
|
|
sourceApp: "br.com.itau.personnalite",
|
|
sourceAppName: "Itaú Personnalité",
|
|
originalTitle: "Débito em conta",
|
|
originalText: "Débito de R$45,80 realizado. PADARIA NOSSA SENHORA",
|
|
notificationTimestamp: parseLocalDateString(
|
|
dateForPeriodDay(lastPeriod, 7),
|
|
),
|
|
parsedName: "Padaria Nossa Senhora",
|
|
parsedAmount: "45.80",
|
|
status: "pending" as const,
|
|
transactionId: null,
|
|
processedAt: null,
|
|
discardedAt: null,
|
|
},
|
|
{
|
|
userId: targetUser.id,
|
|
sourceApp: "br.com.itau.personnalite",
|
|
sourceAppName: "Itaú Personnalité",
|
|
originalTitle: "Compra aprovada",
|
|
originalText:
|
|
"Compra de R$387,40 aprovada no cartão PÃO DE AÇÚCAR. Limite disponível: R$8.112,60",
|
|
notificationTimestamp: parseLocalDateString(
|
|
dateForPeriodDay(lastPeriod, 10),
|
|
),
|
|
parsedName: "Pão de Açúcar",
|
|
parsedAmount: "387.40",
|
|
status: "pending" as const,
|
|
transactionId: null,
|
|
processedAt: null,
|
|
discardedAt: null,
|
|
},
|
|
{
|
|
userId: targetUser.id,
|
|
sourceApp: "com.mercadopago.wallet",
|
|
sourceAppName: "Mercado Pago",
|
|
originalTitle: null,
|
|
originalText: "Pagamento de R$38,50 aprovado em 99APP*CORRIDA",
|
|
notificationTimestamp: parseLocalDateString(
|
|
dateForPeriodDay(lastPeriod, 13),
|
|
),
|
|
parsedName: "99App",
|
|
parsedAmount: "38.50",
|
|
status: "pending" as const,
|
|
transactionId: null,
|
|
processedAt: null,
|
|
discardedAt: null,
|
|
},
|
|
{
|
|
userId: targetUser.id,
|
|
sourceApp: "com.nu.production",
|
|
sourceAppName: "Nubank",
|
|
originalTitle: "Compra aprovada",
|
|
originalText:
|
|
"Compra de R$ 124,90 aprovada no cartão Ultravioleta em SHOPEE*SHOPEE BR",
|
|
notificationTimestamp: parseLocalDateString(
|
|
dateForPeriodDay(lastPeriod, 18),
|
|
),
|
|
parsedName: "Shopee",
|
|
parsedAmount: "124.90",
|
|
status: "pending" as const,
|
|
transactionId: null,
|
|
processedAt: null,
|
|
discardedAt: null,
|
|
},
|
|
{
|
|
userId: targetUser.id,
|
|
sourceApp: "com.nu.production",
|
|
sourceAppName: "Nubank",
|
|
originalTitle: "Compra aprovada",
|
|
originalText:
|
|
"Compra de R$ 199,90 aprovada no cartão Ultravioleta em AMERICANAS S.A",
|
|
notificationTimestamp: parseLocalDateString(
|
|
dateForPeriodDay(lastPeriod, 21),
|
|
),
|
|
parsedName: "Americanas",
|
|
parsedAmount: "199.90",
|
|
status: "pending" as const,
|
|
transactionId: null,
|
|
processedAt: null,
|
|
discardedAt: null,
|
|
},
|
|
{
|
|
userId: targetUser.id,
|
|
sourceApp: "com.mercadopago.wallet",
|
|
sourceAppName: "Mercado Pago",
|
|
originalTitle: null,
|
|
originalText: "Você pagou R$12,50 via Pix para POSTO IPIRANGA",
|
|
notificationTimestamp: parseLocalDateString(
|
|
dateForPeriodDay(lastPeriod, 22),
|
|
),
|
|
parsedName: "Posto Ipiranga",
|
|
parsedAmount: "12.50",
|
|
status: "pending" as const,
|
|
transactionId: null,
|
|
processedAt: null,
|
|
discardedAt: null,
|
|
},
|
|
];
|
|
|
|
await db.insert(inboxItems).values(inboxItemsData);
|
|
summary.inboxItems += inboxItemsData.length;
|
|
|
|
const finalPeriods = Array.from(
|
|
new Set(
|
|
seedTransactionRecords
|
|
.map((record) => record.period ?? "")
|
|
.filter(Boolean),
|
|
),
|
|
).sort(comparePeriods);
|
|
|
|
const seededFrom = finalPeriods[0] ?? options.startPeriod;
|
|
const seededTo = finalPeriods[finalPeriods.length - 1] ?? options.startPeriod;
|
|
|
|
console.log("Seed concluído com sucesso.");
|
|
console.log(
|
|
JSON.stringify(
|
|
{
|
|
userId: targetUser.id,
|
|
userName: targetUser.name,
|
|
startPeriod: options.startPeriod,
|
|
months: options.months,
|
|
seededFrom,
|
|
seededTo,
|
|
todayPeriod: businessToday.period,
|
|
summary,
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
);
|
|
}
|
|
|
|
void main()
|
|
.then(() => process.exit(0))
|
|
.catch((error) => {
|
|
console.error(
|
|
error instanceof Error
|
|
? error.message
|
|
: "Erro inesperado ao popular conta.",
|
|
);
|
|
process.exit(1);
|
|
});
|