mirror of
https://github.com/felipegcoutinho/openmonetis.git
synced 2026-05-09 19:01:47 +00:00
Refatoração estrutural sem mudanças funcionais. Saldo líquido: −428 linhas. Removido: - 14 funções/constantes mortas verificadas via grep no repo todo: validateCategoriaOwnership, getInstallmentAnticipationsAction, getAnticipationDetailsAction, formatDecimalForDb, currencyFormatterNoCents, optionalDecimalSchema, formatMonthLabel, getGoalProgressStatusColorClass, MONTH_PERIOD_PARAM, calculateRemainingInstallments, e 5 funções fetch* não usadas em inbox/queries.ts. - 1 tipo morto (ImportRow) + 2 órfãos consequentes (InstallmentAnticipationWithRelations, GoalProgressStatus convertido em interno). - ~30 export keywords desnecessários (símbolos usados apenas no próprio arquivo). - Re-exports mortos em barrels: EstablishmentLogoPicker, CategoryReportSkeleton, WidgetSkeleton, toNameKey. - Arquivo features/reports/types.ts (barrel inteiro era órfão). Padronizado (PT-BR→EN em identificadores expostos): - 4 constantes globais (LANCAMENTOS_* → TRANSACTIONS_*). - 12 tipos/interfaces (Lancamento*/Pagador*/Estabelecimento* → equivalentes EN). - 13 funções/components exportados (fetchPagador*, EstabelecimentoInput, PagadorInfoCard, etc.). - 5 props cross-file (preLancamentosCount → inboxPendingCount, pagadorAvatarUrl → payerAvatarUrl, etc.). - Mantidas em PT-BR conforme exceção do CLAUDE.md: variáveis locais (pagador, categoria, lancamento), accessor key pagadorName (persistida em preferências), strings de UI. Reorganizado: - transactions/: 14 helpers soltos na raiz movidos para lib/; barrel actions.ts reduzido de 76 linhas de wrappers para 14 linhas de re-exports puros; anticipation-actions.ts movido para actions/anticipation.ts. - dashboard/: 8 helpers soltos consolidados em dashboard/lib/. - reports/: 5 query files na raiz consolidados em reports/lib/. - payers/: detail-actions.ts (21KB) e detail-queries.ts movidos para payers/lib/. - shared/components/: 9 dos 16 componentes soltos agrupados em brand/, widgets/, feedback/. - shared/lib/fetch-json.ts movido para shared/utils/fetch-json.ts. Validação: pnpm exec tsc --noEmit (0 erros), biome check (0 issues), knip (sem unused). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2357 lines
62 KiB
TypeScript
2357 lines
62 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/lib/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 {
|
|
TRANSFER_CATEGORY_NAME,
|
|
TRANSFER_CONDITION,
|
|
TRANSFER_ESTABLISHMENT_ENTRADA,
|
|
TRANSFER_ESTABLISHMENT_SAIDA,
|
|
TRANSFER_PAYMENT_METHOD,
|
|
} from "@/shared/lib/transfers/constants";
|
|
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: "mario",
|
|
name: "Mario Oliveira",
|
|
email: "mario.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(["4825096.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: "itau",
|
|
name: "Itaú Personnalité",
|
|
accountType: ACCOUNT_TYPES.CHECKING,
|
|
status: ACCOUNT_STATUS.ACTIVE,
|
|
logo: preferredLogo(["itaupersonnalite.png", "itau.png"], "itau.png"),
|
|
note: "Conta principal onde cai o salário e saem boletos e investimentos.",
|
|
initialBalance: "8200.00",
|
|
},
|
|
{
|
|
key: "nubank",
|
|
name: "Nubank",
|
|
accountType: ACCOUNT_TYPES.DIGITAL_WALLET,
|
|
status: ACCOUNT_STATUS.ACTIVE,
|
|
logo: preferredLogo(["nubank.png"], "nubank.png"),
|
|
note: "Conta do dia a dia para mercado, transporte e despesas variáveis.",
|
|
initialBalance: "5500.00",
|
|
},
|
|
{
|
|
key: "inter",
|
|
name: "Banco Inter",
|
|
accountType: ACCOUNT_TYPES.CHECKING,
|
|
status: ACCOUNT_STATUS.ACTIVE,
|
|
logo: preferredLogo(["intermedium.png"], "intermedium.png"),
|
|
note: "Conta usada para reserva de emergência e investimentos curtos.",
|
|
initialBalance: "2300.00",
|
|
},
|
|
{
|
|
key: "mercado-pago",
|
|
name: "Mercado Pago",
|
|
accountType: ACCOUNT_TYPES.DIGITAL_WALLET,
|
|
status: ACCOUNT_STATUS.ACTIVE,
|
|
logo: preferredLogo(["mercadopago.png"], "mercadopago.png"),
|
|
note: "Carteira para corridas por aplicativo e pequenos pagamentos.",
|
|
initialBalance: "480.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: "Cartão principal para assinaturas, delivery e compras parceladas.",
|
|
limit: "18995.50",
|
|
logo: preferredLogo(
|
|
["nubank-ultravioleta.png", "nubank.png"],
|
|
"nubank.png",
|
|
),
|
|
accountId: createdAccounts.nubank,
|
|
},
|
|
{
|
|
key: "itaucard",
|
|
name: "Itaucard Visa Infinite",
|
|
brand: CARD_BRANDS.VISA,
|
|
status: CARD_STATUS,
|
|
closingDay: "15",
|
|
dueDay: "22",
|
|
note: "Cartão usado para compras grandes, eletrônicos e viagens.",
|
|
limit: "30000.00",
|
|
logo: preferredLogo(["itau.png"], "itau.png"),
|
|
accountId: createdAccounts.itau,
|
|
},
|
|
{
|
|
key: "interblack",
|
|
name: "Inter Black",
|
|
brand: CARD_BRANDS.MASTERCARD,
|
|
status: CARD_STATUS,
|
|
closingDay: "10",
|
|
dueDay: "17",
|
|
note: "Cartão complementar para mercado e farmácia com cashback.",
|
|
limit: "15000.00",
|
|
logo: preferredLogo(["intermedium.png"], "intermedium.png"),
|
|
accountId: createdAccounts.inter,
|
|
},
|
|
] 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 para Disney",
|
|
type: "nota" as const,
|
|
description:
|
|
"Separar passagens LATAM, hospedagem via Booking e ingressos. Meta: manter o pacote abaixo de R$ 18.000 dividido em 10x no Itaucard.",
|
|
tasks: null,
|
|
archived: false,
|
|
},
|
|
{
|
|
title: "Pendências financeiras do mês",
|
|
type: "tarefa" as const,
|
|
description: null,
|
|
tasks: JSON.stringify([
|
|
{
|
|
id: randomUUID(),
|
|
text: "Revisar fatura do Itaucard antes do fechamento",
|
|
completed: true,
|
|
},
|
|
{
|
|
id: randomUUID(),
|
|
text: "Migrar reserva de emergência para CDB do Inter",
|
|
completed: false,
|
|
},
|
|
{
|
|
id: randomUUID(),
|
|
text: "Cancelar trial do HBO Max se não usar",
|
|
completed: false,
|
|
},
|
|
{
|
|
id: randomUUID(),
|
|
text: "Pagar IPVA antes do vencimento",
|
|
completed: false,
|
|
},
|
|
]),
|
|
archived: false,
|
|
},
|
|
{
|
|
title: "Renegociar plano Vivo Fibra",
|
|
type: "nota" as const,
|
|
description:
|
|
"Comparar com Claro e TIM antes do reajuste anual. Verificar bônus de portabilidade e valor de instalação.",
|
|
tasks: null,
|
|
archived: false,
|
|
},
|
|
{
|
|
title: "Compras Black Friday 2025",
|
|
type: "tarefa" as const,
|
|
description: null,
|
|
tasks: JSON.stringify([
|
|
{
|
|
id: randomUUID(),
|
|
text: "Comprar iPhone 16 Pro na Apple",
|
|
completed: true,
|
|
},
|
|
{
|
|
id: randomUUID(),
|
|
text: "Smart TV Samsung 65 polegadas no Magazine Luiza",
|
|
completed: true,
|
|
},
|
|
{
|
|
id: randomUUID(),
|
|
text: "Notebook Dell XPS na Amazon",
|
|
completed: true,
|
|
},
|
|
]),
|
|
archived: true,
|
|
},
|
|
] 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: 1100 },
|
|
{ categoryName: "Restaurantes", baseAmount: 380 },
|
|
{ categoryName: "Transporte", baseAmount: 700 },
|
|
{ categoryName: "Moradia", baseAmount: 3600 },
|
|
{ categoryName: "Lazer", baseAmount: 450 },
|
|
{ categoryName: "Assinaturas", baseAmount: 480 },
|
|
{ categoryName: "Saúde", baseAmount: 750 },
|
|
{ categoryName: "Delivery", baseAmount: 250 },
|
|
{ categoryName: "Vestuário", baseAmount: 350 },
|
|
] as const;
|
|
|
|
const budgetRows = periods.flatMap((period, index) =>
|
|
budgetDefinitions.map((budgetItem) => ({
|
|
amount: (budgetItem.baseAmount + index * 10).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),
|
|
);
|
|
};
|
|
|
|
// ── Receitas recorrentes ──
|
|
createRecords({
|
|
name: "Globo",
|
|
amount: 9500,
|
|
purchaseDate: dateForPeriodDay(firstPeriod, 5),
|
|
transactionType: "Receita",
|
|
condition: "Recorrente",
|
|
paymentMethod: "Transferência bancária",
|
|
accountId: createdAccounts.itau,
|
|
categoryId: getCategoryId("Salário"),
|
|
payerId: adminPayer.id,
|
|
recurrenceCount: options.months,
|
|
note: "Salário mensal recebido via TED.",
|
|
});
|
|
|
|
// ── Despesas recorrentes (moradia, contas, assinaturas) ──
|
|
createRecords({
|
|
name: "Loft",
|
|
amount: 3200,
|
|
purchaseDate: dateForPeriodDay(firstPeriod, 5),
|
|
transactionType: "Despesa",
|
|
condition: "Recorrente",
|
|
paymentMethod: "Pix",
|
|
accountId: createdAccounts.itau,
|
|
categoryId: getCategoryId("Moradia"),
|
|
payerId: adminPayer.id,
|
|
secondaryPayerId: createdPayers.mario,
|
|
isSplit: true,
|
|
primarySplitAmount: 1600,
|
|
secondarySplitAmount: 1600,
|
|
recurrenceCount: options.months,
|
|
dueDate: dateForPeriodDay(firstPeriod, 8),
|
|
note: "Aluguel do apartamento dividido com mario.",
|
|
});
|
|
|
|
createRecords({
|
|
name: "Vivo Fibra",
|
|
amount: 139.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 700 Mega da casa.",
|
|
});
|
|
|
|
createRecords({
|
|
name: "Vivo",
|
|
amount: 79.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: "Plano celular pós-pago.",
|
|
});
|
|
|
|
createRecords({
|
|
name: "Enel",
|
|
amount: 195.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: "Sabesp",
|
|
amount: 95.6,
|
|
purchaseDate: dateForPeriodDay(firstPeriod, 4),
|
|
transactionType: "Despesa",
|
|
condition: "Recorrente",
|
|
paymentMethod: "Boleto",
|
|
accountId: createdAccounts.itau,
|
|
categoryId: getCategoryId("Energia e água"),
|
|
payerId: adminPayer.id,
|
|
recurrenceCount: options.months,
|
|
dueDate: dateForPeriodDay(firstPeriod, 20),
|
|
note: "Conta mensal de água.",
|
|
});
|
|
|
|
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: "Plano padrão com anúncios.",
|
|
});
|
|
|
|
createRecords({
|
|
name: "Spotify",
|
|
amount: 34.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: "Plano duo com mario.",
|
|
});
|
|
|
|
createRecords({
|
|
name: "Disney Plus",
|
|
amount: 33.9,
|
|
purchaseDate: dateForPeriodDay(firstPeriod, 12),
|
|
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: "Streaming Disney+.",
|
|
});
|
|
|
|
createRecords({
|
|
name: "HBO Max",
|
|
amount: 39.9,
|
|
purchaseDate: dateForPeriodDay(firstPeriod, 14),
|
|
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: "Streaming HBO Max.",
|
|
});
|
|
|
|
createRecords({
|
|
name: "YouTube",
|
|
amount: 28.9,
|
|
purchaseDate: dateForPeriodDay(firstPeriod, 7),
|
|
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: "YouTube Premium individual.",
|
|
});
|
|
|
|
createRecords({
|
|
name: "Amazon Prime",
|
|
amount: 14.9,
|
|
purchaseDate: dateForPeriodDay(firstPeriod, 10),
|
|
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: "Amazon Prime mensal.",
|
|
});
|
|
|
|
createRecords({
|
|
name: "OpenAI",
|
|
amount: 109.9,
|
|
purchaseDate: dateForPeriodDay(firstPeriod, 13),
|
|
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: "ChatGPT Plus.",
|
|
});
|
|
|
|
createRecords({
|
|
name: "Apple",
|
|
amount: 14.9,
|
|
purchaseDate: dateForPeriodDay(firstPeriod, 17),
|
|
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: "iCloud 200GB.",
|
|
});
|
|
|
|
createRecords({
|
|
name: "Notion",
|
|
amount: 49.9,
|
|
purchaseDate: dateForPeriodDay(firstPeriod, 19),
|
|
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: "Notion plus mensal.",
|
|
});
|
|
|
|
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: "Amil",
|
|
amount: 489,
|
|
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 de saúde com coparticipação.",
|
|
});
|
|
|
|
createRecords({
|
|
name: "Porto Seguro",
|
|
amount: 285,
|
|
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: "Seguro auto mensal.",
|
|
});
|
|
|
|
createRecords({
|
|
name: "Condomínio Loft",
|
|
amount: 720,
|
|
purchaseDate: dateForPeriodDay(firstPeriod, 1),
|
|
transactionType: "Despesa",
|
|
condition: "Recorrente",
|
|
paymentMethod: "Pix",
|
|
accountId: createdAccounts.itau,
|
|
categoryId: getCategoryId("Moradia"),
|
|
payerId: adminPayer.id,
|
|
recurrenceCount: options.months,
|
|
dueDate: dateForPeriodDay(firstPeriod, 10),
|
|
note: "Taxa condominial mensal.",
|
|
});
|
|
|
|
// ── Despesas e receitas variáveis (loop mensal) ──
|
|
for (const [index, period] of periods.entries()) {
|
|
const carrefourAmount = 580 + index * 24.5;
|
|
const paoAcucarAmount = 360 + index * 18.2;
|
|
const assaiAmount = 420 + index * 22;
|
|
const uberAmount = 165 + index * 8.4;
|
|
const noventaENoveAmount = 78 + index * 5.6;
|
|
const shellAmount = 280 + index * 12.5;
|
|
const ipirangaAmount = 195 + index * 9.8;
|
|
const ifoodAmount = 142 + index * 8.7;
|
|
const rappiAmount = 88 + index * 6.2;
|
|
const mcdonaldsAmount = 52 + index * 4.1;
|
|
const starbucksAmount = 78 + index * 5.3;
|
|
const burgerKingAmount = 48 + index * 3.6;
|
|
const drogasilAmount = 96 + index * 7.4;
|
|
const raiaAmount = 72 + index * 5.8;
|
|
const investmentYield = 84 + index * 6.4;
|
|
|
|
// Mercado — estoura o orçamento
|
|
createRecords({
|
|
name: "Carrefour",
|
|
amount: carrefourAmount,
|
|
purchaseDate: dateForPeriodDay(period, 6),
|
|
transactionType: "Despesa",
|
|
condition: "À vista",
|
|
paymentMethod: "Pix",
|
|
accountId: createdAccounts.nubank,
|
|
categoryId: getCategoryId("Mercado"),
|
|
payerId: adminPayer.id,
|
|
secondaryPayerId: createdPayers.mario,
|
|
isSplit: true,
|
|
primarySplitAmount: carrefourAmount / 2,
|
|
secondarySplitAmount: carrefourAmount / 2,
|
|
note: "Compra grande do mês dividida com mario.",
|
|
});
|
|
|
|
createRecords({
|
|
name: "Pão de Açúcar",
|
|
amount: paoAcucarAmount,
|
|
purchaseDate: dateForPeriodDay(period, 13),
|
|
transactionType: "Despesa",
|
|
condition: "À vista",
|
|
paymentMethod: "Cartão de crédito",
|
|
cardId: createdCards.interblack.id,
|
|
categoryId: getCategoryId("Mercado"),
|
|
payerId: adminPayer.id,
|
|
cardMeta: createdCards.interblack,
|
|
note: "Hortifruti e frios da semana.",
|
|
});
|
|
|
|
createRecords({
|
|
name: "Assaí",
|
|
amount: assaiAmount,
|
|
purchaseDate: dateForPeriodDay(period, 22),
|
|
transactionType: "Despesa",
|
|
condition: "À vista",
|
|
paymentMethod: "Cartão de crédito",
|
|
cardId: createdCards.interblack.id,
|
|
categoryId: getCategoryId("Mercado"),
|
|
payerId: adminPayer.id,
|
|
cardMeta: createdCards.interblack,
|
|
note: "Atacado mensal: bebidas e itens não-perecíveis.",
|
|
});
|
|
|
|
// Transporte
|
|
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: "99",
|
|
amount: noventaENoveAmount,
|
|
purchaseDate: dateForPeriodDay(period, 19),
|
|
transactionType: "Despesa",
|
|
condition: "À vista",
|
|
paymentMethod: "Pix",
|
|
accountId: createdAccounts["mercado-pago"],
|
|
categoryId: getCategoryId("Transporte"),
|
|
payerId: adminPayer.id,
|
|
note: "Corridas e idas ao aeroporto.",
|
|
});
|
|
|
|
createRecords({
|
|
name: "Shell",
|
|
amount: shellAmount,
|
|
purchaseDate: dateForPeriodDay(period, 9),
|
|
transactionType: "Despesa",
|
|
condition: "À vista",
|
|
paymentMethod: "Cartão de débito",
|
|
accountId: createdAccounts.itau,
|
|
categoryId: getCategoryId("Transporte"),
|
|
payerId: adminPayer.id,
|
|
note: "Abastecimento principal do mês.",
|
|
});
|
|
|
|
createRecords({
|
|
name: "Ipiranga",
|
|
amount: ipirangaAmount,
|
|
purchaseDate: dateForPeriodDay(period, 23),
|
|
transactionType: "Despesa",
|
|
condition: "À vista",
|
|
paymentMethod: "Cartão de débito",
|
|
accountId: createdAccounts.itau,
|
|
categoryId: getCategoryId("Transporte"),
|
|
payerId: adminPayer.id,
|
|
note: "Reabastecimento de fim de mês.",
|
|
});
|
|
|
|
// Delivery
|
|
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: "Rappi",
|
|
amount: rappiAmount,
|
|
purchaseDate: dateForPeriodDay(period, 27),
|
|
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 fim de semana.",
|
|
});
|
|
|
|
// Restaurantes — estoura o orçamento
|
|
createRecords({
|
|
name: "McDonald's",
|
|
amount: mcdonaldsAmount,
|
|
purchaseDate: dateForPeriodDay(period, 16),
|
|
transactionType: "Despesa",
|
|
condition: "À vista",
|
|
paymentMethod: "Pré-Pago | VR/VA",
|
|
accountId: createdAccounts["mercado-pago"],
|
|
categoryId: getCategoryId("Restaurantes"),
|
|
payerId: adminPayer.id,
|
|
note: "Almoço rápido no intervalo.",
|
|
});
|
|
|
|
createRecords({
|
|
name: "Starbucks",
|
|
amount: starbucksAmount,
|
|
purchaseDate: dateForPeriodDay(period, 11),
|
|
transactionType: "Despesa",
|
|
condition: "À vista",
|
|
paymentMethod: "Cartão de crédito",
|
|
cardId: createdCards.ultravioleta.id,
|
|
categoryId: getCategoryId("Restaurantes"),
|
|
payerId: adminPayer.id,
|
|
cardMeta: createdCards.ultravioleta,
|
|
note: "Café e snack do trabalho.",
|
|
});
|
|
|
|
createRecords({
|
|
name: "Burger King",
|
|
amount: burgerKingAmount,
|
|
purchaseDate: dateForPeriodDay(period, 24),
|
|
transactionType: "Despesa",
|
|
condition: "À vista",
|
|
paymentMethod: "Pré-Pago | VR/VA",
|
|
accountId: createdAccounts["mercado-pago"],
|
|
categoryId: getCategoryId("Restaurantes"),
|
|
payerId: adminPayer.id,
|
|
note: "Jantar rápido.",
|
|
});
|
|
|
|
// Saúde / farmácia
|
|
createRecords({
|
|
name: "Drogasil",
|
|
amount: drogasilAmount,
|
|
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.",
|
|
});
|
|
|
|
createRecords({
|
|
name: "Raia",
|
|
amount: raiaAmount,
|
|
purchaseDate: dateForPeriodDay(period, 26),
|
|
transactionType: "Despesa",
|
|
condition: "À vista",
|
|
paymentMethod: "Cartão de débito",
|
|
accountId: createdAccounts.nubank,
|
|
categoryId: getCategoryId("Saúde"),
|
|
payerId: adminPayer.id,
|
|
note: "Remédios e suplementos.",
|
|
});
|
|
|
|
// Investimentos (rendimento)
|
|
createRecords({
|
|
name: "XP Investimentos",
|
|
amount: investmentYield,
|
|
purchaseDate: dateForPeriodDay(period, 27),
|
|
transactionType: "Receita",
|
|
condition: "À vista",
|
|
paymentMethod: "Transferência bancária",
|
|
accountId: createdAccounts.inter,
|
|
categoryId: getCategoryId("Investimentos"),
|
|
payerId: adminPayer.id,
|
|
note: "Rendimento líquido do CDB.",
|
|
});
|
|
|
|
// Esporádicos por paridade do mês
|
|
if (index % 2 === 0) {
|
|
createRecords({
|
|
name: "Amazon",
|
|
amount: 168 + index * 12.8,
|
|
purchaseDate: dateForPeriodDay(period, 18),
|
|
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 === 0) {
|
|
createRecords({
|
|
name: "Mercado Livre",
|
|
amount: 134 + index * 11.4,
|
|
purchaseDate: dateForPeriodDay(period, 25),
|
|
transactionType: "Despesa",
|
|
condition: "À vista",
|
|
paymentMethod: "Cartão de crédito",
|
|
cardId: createdCards.itaucard.id,
|
|
categoryId: getCategoryId("Compras"),
|
|
payerId: adminPayer.id,
|
|
cardMeta: createdCards.itaucard,
|
|
note: "Compras avulsas no marketplace.",
|
|
});
|
|
}
|
|
|
|
if (index % 2 === 1) {
|
|
createRecords({
|
|
name: "Stone",
|
|
amount: 1850 + index * 140,
|
|
purchaseDate: dateForPeriodDay(period, 24),
|
|
transactionType: "Receita",
|
|
condition: "À vista",
|
|
paymentMethod: "Pix",
|
|
accountId: createdAccounts.itau,
|
|
categoryId: getCategoryId("Freelance"),
|
|
payerId: adminPayer.id,
|
|
note: "Projeto freelance de consultoria.",
|
|
});
|
|
}
|
|
|
|
if (index % 3 === 0) {
|
|
createRecords({
|
|
name: "Outback",
|
|
amount: 248 + index * 11.2,
|
|
purchaseDate: dateForPeriodDay(period, 21),
|
|
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 % 3 === 1) {
|
|
createRecords({
|
|
name: "Madero",
|
|
amount: 198 + index * 9.5,
|
|
purchaseDate: dateForPeriodDay(period, 17),
|
|
transactionType: "Despesa",
|
|
condition: "À vista",
|
|
paymentMethod: "Cartão de crédito",
|
|
cardId: createdCards.itaucard.id,
|
|
categoryId: getCategoryId("Restaurantes"),
|
|
payerId: adminPayer.id,
|
|
cardMeta: createdCards.itaucard,
|
|
note: "Almoço de domingo.",
|
|
});
|
|
}
|
|
|
|
if (index % 2 === 0) {
|
|
createRecords({
|
|
name: "Cinemark",
|
|
amount: 142 + 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: "Sessão IMAX para dois.",
|
|
});
|
|
}
|
|
}
|
|
|
|
// ── Parcelados grandes ──
|
|
createRecords({
|
|
name: "Apple",
|
|
amount: 9999,
|
|
purchaseDate: dateForPeriodDay(firstPeriod, 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: "iPhone 16 Pro 256GB, 12x sem juros.",
|
|
});
|
|
|
|
createRecords({
|
|
name: "Samsung",
|
|
amount: 5499,
|
|
purchaseDate: dateForPeriodDay(firstPeriod, 26),
|
|
transactionType: "Despesa",
|
|
condition: "Parcelado",
|
|
paymentMethod: "Cartão de crédito",
|
|
cardId: createdCards.itaucard.id,
|
|
categoryId: getCategoryId("Compras"),
|
|
payerId: adminPayer.id,
|
|
installmentCount: 10,
|
|
cardMeta: createdCards.itaucard,
|
|
note: "Smart TV QLED 65 polegadas.",
|
|
});
|
|
|
|
createRecords({
|
|
name: "Dell",
|
|
amount: 7999.2,
|
|
purchaseDate: dateForPeriodDay(secondPeriod, 18),
|
|
transactionType: "Despesa",
|
|
condition: "Parcelado",
|
|
paymentMethod: "Cartão de crédito",
|
|
cardId: createdCards.itaucard.id,
|
|
categoryId: getCategoryId("Compras"),
|
|
payerId: adminPayer.id,
|
|
installmentCount: 10,
|
|
cardMeta: createdCards.itaucard,
|
|
note: "Notebook Dell XPS 13 para o home office.",
|
|
});
|
|
|
|
createRecords({
|
|
name: "Magazine Luiza",
|
|
amount: 2299.5,
|
|
purchaseDate: dateForPeriodDay(thirdPeriod, 7),
|
|
transactionType: "Despesa",
|
|
condition: "Parcelado",
|
|
paymentMethod: "Cartão de crédito",
|
|
cardId: createdCards.itaucard.id,
|
|
categoryId: getCategoryId("Moradia"),
|
|
payerId: adminPayer.id,
|
|
installmentCount: 5,
|
|
cardMeta: createdCards.itaucard,
|
|
note: "Geladeira inverter para o apartamento.",
|
|
});
|
|
|
|
createRecords({
|
|
name: "LATAM",
|
|
amount: 4280.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: 2140.25,
|
|
secondarySplitAmount: 2140.25,
|
|
installmentCount: 6,
|
|
cardMeta: createdCards.ultravioleta,
|
|
note: "Passagens para Salvador, divididas com Eduardo.",
|
|
});
|
|
|
|
createRecords({
|
|
name: "Booking",
|
|
amount: 3690,
|
|
purchaseDate: dateForPeriodDay(thirdPeriod, 17),
|
|
transactionType: "Despesa",
|
|
condition: "Parcelado",
|
|
paymentMethod: "Cartão de crédito",
|
|
cardId: createdCards.ultravioleta.id,
|
|
categoryId: getCategoryId("Viagem"),
|
|
payerId: adminPayer.id,
|
|
installmentCount: 6,
|
|
cardMeta: createdCards.ultravioleta,
|
|
note: "Hospedagem da viagem.",
|
|
});
|
|
|
|
// ── Compras avulsas (à vista) ──
|
|
createRecords({
|
|
name: "Apple",
|
|
amount: 4299,
|
|
purchaseDate: dateForPeriodDay(secondPeriod, 5),
|
|
transactionType: "Despesa",
|
|
condition: "À vista",
|
|
paymentMethod: "Cartão de crédito",
|
|
cardId: createdCards.ultravioleta.id,
|
|
categoryId: getCategoryId("Compras"),
|
|
payerId: adminPayer.id,
|
|
cardMeta: createdCards.ultravioleta,
|
|
note: "Apple Watch Series 10.",
|
|
});
|
|
|
|
createRecords({
|
|
name: "Zara",
|
|
amount: 689.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: "Coleção de inverno.",
|
|
});
|
|
|
|
createRecords({
|
|
name: "Renner",
|
|
amount: 412.5,
|
|
purchaseDate: dateForPeriodDay(thirdPeriod, 9),
|
|
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: "Camisas e calças básicas.",
|
|
});
|
|
|
|
createRecords({
|
|
name: "Nike",
|
|
amount: 899.9,
|
|
purchaseDate: dateForPeriodDay(firstPeriod, 23),
|
|
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: "Tênis Air Zoom para academia.",
|
|
});
|
|
|
|
// ── Educação, presentes, lazer ──
|
|
createRecords({
|
|
name: "Alura",
|
|
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 de desenvolvimento.",
|
|
});
|
|
|
|
createRecords({
|
|
name: "Amazon",
|
|
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 do Eduardo.",
|
|
});
|
|
|
|
createRecords({
|
|
name: "Eventim",
|
|
amount: 480,
|
|
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.mario,
|
|
isSplit: true,
|
|
primarySplitAmount: 240,
|
|
secondarySplitAmount: 240,
|
|
installmentCount: 3,
|
|
cardMeta: createdCards.ultravioleta,
|
|
note: "Ingressos para show, divididos com mario.",
|
|
});
|
|
|
|
// ── Saúde / consultas ──
|
|
createRecords({
|
|
name: "Hapvida",
|
|
amount: 320,
|
|
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: "Consulta odontológica particular.",
|
|
});
|
|
|
|
createRecords({
|
|
name: "Amil",
|
|
amount: 218.5,
|
|
purchaseDate: dateForPeriodDay(secondPeriod, 26),
|
|
transactionType: "Receita",
|
|
condition: "À vista",
|
|
paymentMethod: "Pix",
|
|
accountId: createdAccounts.itau,
|
|
categoryId: getCategoryId("Reembolso"),
|
|
payerId: adminPayer.id,
|
|
note: "Reembolso de consulta médica.",
|
|
});
|
|
|
|
// ── Boleto em aberto (testa lembretes) ──
|
|
createRecords({
|
|
name: "Detran",
|
|
amount: 1284.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: "IPVA 2026 em aberto.",
|
|
settlementBehavior: "open",
|
|
});
|
|
|
|
// ── Estouro do Inter Black no último mês (testa barra destructive) ──
|
|
createRecords({
|
|
name: "Magazine Luiza",
|
|
amount: 2199,
|
|
purchaseDate: dateForPeriodDay(lastPeriod, 4),
|
|
transactionType: "Despesa",
|
|
condition: "À vista",
|
|
paymentMethod: "Cartão de crédito",
|
|
cardId: createdCards.interblack.id,
|
|
categoryId: getCategoryId("Moradia"),
|
|
payerId: adminPayer.id,
|
|
cardMeta: createdCards.interblack,
|
|
note: "Air fryer e cafeteira para a cozinha.",
|
|
});
|
|
|
|
// ── Transferências internas entre contas ──
|
|
const transferCategoryId = getCategoryId(TRANSFER_CATEGORY_NAME);
|
|
|
|
const pushTransfer = (params: {
|
|
fromAccountId: string;
|
|
toAccountId: string;
|
|
amount: number;
|
|
date: string;
|
|
fromAccountName: string;
|
|
toAccountName: string;
|
|
}) => {
|
|
const transferId = randomUUID();
|
|
const period = derivePeriodFromDate(params.date);
|
|
const purchaseDate = parseLocalDateString(params.date);
|
|
const note = `de ${params.fromAccountName} -> ${params.toAccountName}`;
|
|
const sharedFields = {
|
|
condition: TRANSFER_CONDITION,
|
|
paymentMethod: TRANSFER_PAYMENT_METHOD,
|
|
note,
|
|
purchaseDate,
|
|
transactionType: "Transferência" as const,
|
|
period,
|
|
isSettled: true,
|
|
userId: targetUser.id,
|
|
categoryId: transferCategoryId,
|
|
payerId: adminPayer.id,
|
|
transferId,
|
|
};
|
|
|
|
seedTransactionRecords.push({
|
|
...sharedFields,
|
|
name: TRANSFER_ESTABLISHMENT_SAIDA,
|
|
amount: `-${params.amount.toFixed(2)}`,
|
|
accountId: params.fromAccountId,
|
|
});
|
|
seedTransactionRecords.push({
|
|
...sharedFields,
|
|
name: TRANSFER_ESTABLISHMENT_ENTRADA,
|
|
amount: params.amount.toFixed(2),
|
|
accountId: params.toAccountId,
|
|
});
|
|
};
|
|
|
|
// Repasse mensal do salário (Itaú → Nubank) para o dia a dia
|
|
for (const period of periods) {
|
|
pushTransfer({
|
|
fromAccountId: createdAccounts.itau,
|
|
toAccountId: createdAccounts.nubank,
|
|
amount: 4000,
|
|
date: dateForPeriodDay(period, 6),
|
|
fromAccountName: "Itaú Personnalité",
|
|
toAccountName: "Nubank",
|
|
});
|
|
}
|
|
|
|
// Reserva de emergência no Inter (eventual)
|
|
pushTransfer({
|
|
fromAccountId: createdAccounts.itau,
|
|
toAccountId: createdAccounts.inter,
|
|
amount: 1500,
|
|
date: dateForPeriodDay(secondPeriod, 12),
|
|
fromAccountName: "Itaú Personnalité",
|
|
toAccountName: "Banco Inter",
|
|
});
|
|
|
|
pushTransfer({
|
|
fromAccountId: createdAccounts.itau,
|
|
toAccountId: createdAccounts.inter,
|
|
amount: 2000,
|
|
date: dateForPeriodDay(middlePeriod, 14),
|
|
fromAccountName: "Itaú Personnalité",
|
|
toAccountName: "Banco Inter",
|
|
});
|
|
|
|
// Recargas pontuais para o Mercado Pago
|
|
pushTransfer({
|
|
fromAccountId: createdAccounts.nubank,
|
|
toAccountId: createdAccounts["mercado-pago"],
|
|
amount: 250,
|
|
date: dateForPeriodDay(firstPeriod, 18),
|
|
fromAccountName: "Nubank",
|
|
toAccountName: "Mercado Pago",
|
|
});
|
|
|
|
pushTransfer({
|
|
fromAccountId: createdAccounts.nubank,
|
|
toAccountId: createdAccounts["mercado-pago"],
|
|
amount: 180,
|
|
date: dateForPeriodDay(thirdPeriod, 14),
|
|
fromAccountName: "Nubank",
|
|
toAccountName: "Mercado Pago",
|
|
});
|
|
|
|
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$ 142,80 aprovada no cartão Ultravioleta em IFOOD*IFOOD",
|
|
notificationTimestamp: parseLocalDateString(
|
|
dateForPeriodDay(lastPeriod, 3),
|
|
),
|
|
parsedName: "iFood",
|
|
parsedAmount: "142.80",
|
|
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$ 320,00 via Pix para mario Oliveira.",
|
|
notificationTimestamp: parseLocalDateString(
|
|
dateForPeriodDay(lastPeriod, 5),
|
|
),
|
|
parsedName: "mario Oliveira",
|
|
parsedAmount: "320.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$ 78,90 realizado. STARBUCKS COFFEE",
|
|
notificationTimestamp: parseLocalDateString(
|
|
dateForPeriodDay(lastPeriod, 7),
|
|
),
|
|
parsedName: "Starbucks",
|
|
parsedAmount: "78.90",
|
|
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$ 487,40 aprovada no cartão Itaucard em AMAZON BR. Limite disponível: R$ 1.512,60",
|
|
notificationTimestamp: parseLocalDateString(
|
|
dateForPeriodDay(lastPeriod, 10),
|
|
),
|
|
parsedName: "Amazon",
|
|
parsedAmount: "487.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 UBER*TRIP",
|
|
notificationTimestamp: parseLocalDateString(
|
|
dateForPeriodDay(lastPeriod, 13),
|
|
),
|
|
parsedName: "Uber",
|
|
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$ 224,90 aprovada no cartão Ultravioleta em SHOPEE*SHOPEE BR",
|
|
notificationTimestamp: parseLocalDateString(
|
|
dateForPeriodDay(lastPeriod, 18),
|
|
),
|
|
parsedName: "Shopee",
|
|
parsedAmount: "224.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$ 89,90 aprovada no cartão Ultravioleta em NETFLIX.COM",
|
|
notificationTimestamp: parseLocalDateString(
|
|
dateForPeriodDay(lastPeriod, 21),
|
|
),
|
|
parsedName: "Netflix",
|
|
parsedAmount: "89.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$ 92,50 via Pix para SHELL POSTO",
|
|
notificationTimestamp: parseLocalDateString(
|
|
dateForPeriodDay(lastPeriod, 22),
|
|
),
|
|
parsedName: "Shell",
|
|
parsedAmount: "92.50",
|
|
status: "pending" as const,
|
|
transactionId: null,
|
|
processedAt: null,
|
|
discardedAt: null,
|
|
},
|
|
{
|
|
userId: targetUser.id,
|
|
sourceApp: "com.mercadolivre.mercadolibre",
|
|
sourceAppName: "Mercado Livre",
|
|
originalTitle: "Pedido confirmado",
|
|
originalText:
|
|
"Seu pedido de R$ 379,00 foi confirmado em MERCADO LIVRE BR",
|
|
notificationTimestamp: parseLocalDateString(
|
|
dateForPeriodDay(lastPeriod, 24),
|
|
),
|
|
parsedName: "Mercado Livre",
|
|
parsedAmount: "379.00",
|
|
status: "pending" as const,
|
|
transactionId: null,
|
|
processedAt: null,
|
|
discardedAt: null,
|
|
},
|
|
{
|
|
userId: targetUser.id,
|
|
sourceApp: "com.apple.android.music",
|
|
sourceAppName: "Apple",
|
|
originalTitle: "Compra recebida",
|
|
originalText: "Sua compra de R$ 19,90 em APPLE.COM/BILL foi processada.",
|
|
notificationTimestamp: parseLocalDateString(
|
|
dateForPeriodDay(lastPeriod, 25),
|
|
),
|
|
parsedName: "Apple",
|
|
parsedAmount: "19.90",
|
|
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);
|
|
});
|