Implementuj moduły finansowe OwnHome (budget, subscriptions, obligations/recurring). Użyj tego skilla zawsze gdy tworzysz lub edytujesz pliki w modules/budget, modules/subscriptions, modules/obligations, app/api/budget, app/api/subscriptions, app/api/recurring. Skill zawiera kompletną specyfikację techniczną i kolejność implementacji.
Przed każdą implementacją przeczytaj plik specyfikacji:
cat SPEC_FINANCE_MODULES.md (W folderze /docs)
Specyfikacja zawiera: schematy Prisma, pełne API, logikę biznesową, typy TypeScript, schematy Zod i kolejność implementacji. Nie implementuj nic czego tam nie ma.
BudgetCategory to model Prisma (tabela budget_categories), nie enum.
category we wszystkich modelach to String @db.VarChar(50) przechowujące slugGET /api/budget/categories — nie z hardcoded tablicyBudgetCategoryView { id, slug, label, icon?, sortOrder }z.string().regex(/^[a-z0-9_]+$/)budget_categories: auth.uid() IS NOT NULL (globalna tabela, brak userId)RecurringPayment nie wymaga periodId. Zamiast tego używa year: Int i month: Int.
periodId jest opcjonalne (String?) — wypełniane tylko gdy BudgetPeriod istnieje@@unique([templateId, year, month])# 1. Zaktualizuj prisma/schema.prisma — dodaj wszystkie modele z SPEC sekcja 3.1
# WAŻNE: BudgetCategory jako model (nie enum), category pola jako String
# 2. Utwórz migrację SQL z RLS z SPEC sekcja 3.2
npx prisma migrate dev --name finance_modules_init
npx prisma generate
# 3. Seed kategorii (14 rekordów)
# 4. Sprawdź: npx tsc --noEmit
budget.types.ts → budget.schema.ts → budget.repository.ts (tylko template) →
budget.service.ts (tylko template) → app/api/budget/template/route.ts →
app/api/budget/template/incomes/ → app/api/budget/template/expenses/ →
app/api/budget/categories/route.ts + [id]/route.ts
budget.repository.ts (periods, transactions) → budget.service.ts (createPeriod z pełną logiką) →
wszystkie routy periods i transactions →
app/api/budget/periods/[id]/replace-template/route.ts →
app/api/budget/periods/[id]/reset/route.ts
subscriptions.types.ts → subscriptions.schema.ts → subscriptions.repository.ts →
subscriptions.service.ts → app/api/subscriptions/ →
app/api/subscriptions/process-due/route.ts
obligations.types.ts → obligations.schema.ts → obligations.repository.ts →
obligations.service.ts → app/api/recurring/
budget.ui.tsx → subscriptions.ui.tsx → obligations.ui.tsx
To najważniejsza metoda w całym module. Musi w jednej transakcji:
Użyj prisma.$transaction() dla kroków 3-7 — atomowość obowiązkowa.
Okresy można tworzyć dla dowolnego miesiąca (przeszłego lub przyszłego) — brak ograniczenia.
Sygnatura: POST /api/recurring/[id]/confirm?year=Y&month=M
[id] to id RecurringPayment (nie templateId)year i month przekazywane jako query params (nie w body)periodId NIE jest wymagany w bodyMusi w jednej transakcji (prisma.$transaction):
obligationService.getMonthView(userId, year, month) to główny entry point dla zobowiązań.
Wykonuje auto-sync: dla każdego aktywnego RecurringTemplate tworzy RecurringPayment dla podanego roku/miesiąca jeśli jeszcze nie istnieje (idempotent upsert). Działa niezależnie od tego czy BudgetPeriod dla tego miesiąca istnieje.
Używany przez GET /api/recurring/pending?year=Y&month=M.
subscriptionService.processDue(userId) — zaksięguj przeterminowane subskrypcje.
Dla każdej aktywnej subskrypcji gdzie nextBillingDate <= today:
sourceId=sub.id dla danej daty już istniejenextBillingDate do następnego cyklu{ processed, booked }Repository pobiera period z include:
prisma.budgetPeriod.findFirst({
where: { id, userId },
include: {
incomes: true,
categoryPlans: true,
transactions: { where: { deletedAt: null } },
}
})
Service przelicza sumy w pamięci — zero dodatkowych zapytań.
Kategorie do byCategory pobrane wcześniej z categoryRepository.getAll() — nie hardcoded.
□ npx tsc --noEmit — zero błędów TypeScript
□ Brak any w żadnym pliku
□ Każdy endpoint: requireAuth → validate → service → apiSuccess/apiError
□ Każde repository query: where: { ..., userId }
□ Każde listing query: where: { deletedAt: null }
□ Kwoty jako string w response
□ Daty jako ISO string w response
□ Rate limit na mutujących endpointach
□ RLS policy na każdej nowej tabeli
□ category pola jako String (slug), nie enum Prisma
□ RecurringPayment: year+month w unique constraint, periodId opcjonalne
□ confirmPayment: year+month z query params, nie body
lib/prisma.ts — singleton clientlib/auth.ts — requireAuth()lib/api-response.ts — apiSuccess(), apiError()lib/event-emitter.ts — emit()lib/rate-limit.ts — withRateLimit()types/common.types.ts — AppError, ErrorCode