From a2bfe16b5baf1e7e2226f11c5731220308d4afd4 Mon Sep 17 00:00:00 2001 From: ankitsejwal Date: Thu, 16 Jul 2026 16:40:39 +0530 Subject: [PATCH] feat(finance): penalties & interest on arrears (#95) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Simple (non-compound), opt-in, waivable interest on overdue dues. - computeInterest / sumApartmentInterest (shared): simple interest from due date + grace to as-of, at a configured annual rate. Rate always from config, never hardcoded (Bye-law 71 cap). - bill_config gains interest_enabled / interest_rate_pct / grace_period_days (migration 0022) + Zod + PUT wiring. - API: GET /bills/interest-preview (per-flat accrued, not charged), POST /bills/apply-interest (charges one-time interest bills per flat, mapped to Interest on Arrears Income, idempotent per period, posts to GL), and POST /bills/:id/cancel (waive with reason → status CANCELLED + §6.9 reversing ledger entry). Interest is excluded from its own base so it never compounds. - Ledger: JournalDraft carries reversal metadata; safeReverseBill posts the swapped-legs ADJUSTMENT for a cancelled bill. - Tests: computeInterest/grace/pro-rata units + interest route tests (auth, disabled-config, empty preview). Suite 333 green; web typechecks. Editable interest controls in the bill-config UI come with the consolidated M3.5 admin-UI pass; values are preserved on save meanwhile. --- apps/api/src/bills-interest.route.test.ts | 58 + apps/api/src/interest.test.ts | 49 + apps/api/src/lib/ledger-posting.ts | 49 + apps/api/src/routes/bill-config.ts | 32 +- apps/api/src/routes/bills.ts | 165 +- apps/web/src/routes/admin/billing.tsx | 5 + packages/db/drizzle/0022_lyrical_corsair.sql | 3 + packages/db/drizzle/meta/0022_snapshot.json | 3691 ++++++++++++++++++ packages/db/drizzle/meta/_journal.json | 7 + packages/db/src/schema/bill-config.ts | 8 +- packages/shared/src/finance.ts | 39 + packages/shared/src/ledger.ts | 9 + 12 files changed, 4107 insertions(+), 8 deletions(-) create mode 100644 apps/api/src/bills-interest.route.test.ts create mode 100644 apps/api/src/interest.test.ts create mode 100644 packages/db/drizzle/0022_lyrical_corsair.sql create mode 100644 packages/db/drizzle/meta/0022_snapshot.json diff --git a/apps/api/src/bills-interest.route.test.ts b/apps/api/src/bills-interest.route.test.ts new file mode 100644 index 0000000..330844c --- /dev/null +++ b/apps/api/src/bills-interest.route.test.ts @@ -0,0 +1,58 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest' + +const { fakeDb, setQueue } = vi.hoisted(() => { + let queue: unknown[][] = [] + const makeChain = (): unknown => + new Proxy(function () {}, { + get(_t, prop) { + if (prop === 'then') { + const r = queue.length ? (queue.shift() as unknown[]) : [] + return (resolve: (v: unknown) => void) => resolve(r) + } + return () => makeChain() + }, + apply: () => makeChain(), + }) + return { fakeDb: () => makeChain(), setQueue: (q: unknown[][]) => (queue = q) } +}) + +vi.mock('@opensociety/db', async (orig) => ({ + ...(await orig()), + createDb: () => fakeDb(), +})) + +const { billRoutes } = await import('./routes/bills') +import type { AppEnv } from './types' + +const env = { DATABASE_URL: 'test' } as AppEnv['Bindings'] +const ADMIN = { id: 'a1', role: 'ADMIN', status: 'APPROVED' } +const RESIDENT = { id: 'r1', role: 'RESIDENT', status: 'APPROVED' } +const req = (path: string, id: string | undefined, method = 'GET') => + billRoutes.request(path, { method, headers: id ? { 'x-user-id': id } : {} }, env) + +beforeEach(() => setQueue([])) + +describe('interest routes — admin only', () => { + it('403s a resident on POST /apply-interest', async () => { + setQueue([[RESIDENT]]) + expect((await req('/apply-interest', 'r1', 'POST')).status).toBe(403) + }) + + it('400s apply-interest when interest is disabled in config', async () => { + setQueue([[ADMIN], [{ id: 'c1', interestEnabled: false, interestRatePct: 18, gracePeriodDays: 15 }]]) + const res = await req('/apply-interest', 'a1', 'POST') + expect(res.status).toBe(400) + }) + + it('returns an empty preview when no config exists', async () => { + setQueue([[ADMIN], []]) + const res = await req('/interest-preview', 'a1') + expect(res.status).toBe(200) + expect(await res.json()).toMatchObject({ enabled: false, rows: [] }) + }) + + it('403s a resident on POST /:id/cancel', async () => { + setQueue([[RESIDENT]]) + expect((await req('/some-bill-id/cancel', 'r1', 'POST')).status).toBe(403) + }) +}) diff --git a/apps/api/src/interest.test.ts b/apps/api/src/interest.test.ts new file mode 100644 index 0000000..92fba67 --- /dev/null +++ b/apps/api/src/interest.test.ts @@ -0,0 +1,49 @@ +import { describe, it, expect } from 'vitest' +import { computeInterest, sumApartmentInterest } from '@opensociety/shared' + +const day = (y: number, m: number, d: number) => Date.UTC(y, m - 1, d) + +describe('computeInterest — simple interest on arrears', () => { + it('is zero on the due date (no elapsed days)', () => { + expect(computeInterest(100000, day(2026, 1, 1), day(2026, 1, 1), 12, 0)).toBe(0) + }) + + it('accrues a full year at the annual rate', () => { + // ₹1000 @ 12% for 365 days = ₹120 = 12000 paise + expect(computeInterest(100000, day(2026, 1, 1), day(2027, 1, 1), 12, 0)).toBe(12000) + }) + + it('accrues pro-rata for part of a year', () => { + // 30 days: round(100000*12*30/36500) = 986 + expect(computeInterest(100000, day(2026, 1, 1), day(2026, 1, 31), 12, 0)).toBe(986) + }) + + it('respects the grace period', () => { + // 9 days elapsed, 15-day grace -> still 0 + expect(computeInterest(100000, day(2026, 1, 1), day(2026, 1, 10), 12, 15)).toBe(0) + // 20 days elapsed, 15-day grace -> 5 chargeable days + expect(computeInterest(100000, day(2026, 1, 1), day(2026, 1, 21), 12, 15)).toBe( + Math.round((100000 * 12 * 5) / 36500), + ) + }) + + it('is zero for a cleared balance or a disabled rate', () => { + expect(computeInterest(0, day(2026, 1, 1), day(2027, 1, 1), 12, 0)).toBe(0) + expect(computeInterest(100000, day(2026, 1, 1), day(2027, 1, 1), 0, 0)).toBe(0) + expect(computeInterest(-5000, day(2026, 1, 1), day(2027, 1, 1), 12, 0)).toBe(0) + }) +}) + +describe('sumApartmentInterest', () => { + it('sums interest across a flat’s overdue bills', () => { + const asOf = day(2027, 1, 1) + const bills = [ + { outstanding: 100000, dueDateMs: day(2026, 1, 1) }, // full year -> 12000 + { outstanding: 50000, dueDateMs: day(2026, 7, 1) }, // ~184 days + ] + const expected = + computeInterest(100000, day(2026, 1, 1), asOf, 12, 0) + computeInterest(50000, day(2026, 7, 1), asOf, 12, 0) + expect(sumApartmentInterest(bills, asOf, 12, 0)).toBe(expected) + expect(sumApartmentInterest(bills, asOf, 12, 0)).toBeGreaterThan(12000) + }) +}) diff --git a/apps/api/src/lib/ledger-posting.ts b/apps/api/src/lib/ledger-posting.ts index c653098..f6bfbfb 100644 --- a/apps/api/src/lib/ledger-posting.ts +++ b/apps/api/src/lib/ledger-posting.ts @@ -7,9 +7,11 @@ import { postPaymentReceived as buildPaymentEntry, postExpense as buildExpenseEntry, postExpenseSettlement as buildSettlementEntry, + reverseLines, splitReceivableAdvance, validateEntryLines, type JournalDraft, + type JournalLineInput, type BillPostingLine, type PaymentMethod, type ExpenseStatus, @@ -55,6 +57,8 @@ export async function insertJournalEntry(db: Database, draft: JournalDraft, crea sourceType: draft.sourceType, sourceId: draft.sourceId ?? null, period: draft.period, + isReversal: draft.isReversal ?? false, + reversesId: draft.reversesId ?? null, createdBy: createdBy ?? null, }) .returning({ id: journalEntries.id }) @@ -266,6 +270,51 @@ export async function safeSettleExpense( } } +// §6.9 — reverse the "bill issued" entry when a bill is cancelled. Posts an +// ADJUSTMENT with swapped debit/credit that references the original entry. No-op +// if the bill was never posted or is already reversed (best-effort). +export async function safeReverseBill(db: Database, billId: string, createdBy?: string): Promise { + try { + const [orig] = await db + .select({ id: journalEntries.id, entryDate: journalEntries.entryDate, period: journalEntries.period }) + .from(journalEntries) + .where(and(eq(journalEntries.sourceType, 'BILL'), eq(journalEntries.sourceId, billId))) + .limit(1) + if (!orig) return + const [{ already }] = await db + .select({ already: sql`count(*)::int` }) + .from(journalEntries) + .where(eq(journalEntries.reversesId, orig.id)) + if (Number(already) > 0) return // already reversed + + const lines = await db + .select({ accountId: journalLines.accountId, debit: journalLines.debit, credit: journalLines.credit, apartmentId: journalLines.apartmentId }) + .from(journalLines) + .where(eq(journalLines.entryId, orig.id)) + if (lines.length === 0) return + + const reversed: JournalLineInput[] = reverseLines( + lines.map((l) => ({ accountId: l.accountId, debit: l.debit, credit: l.credit, apartmentId: l.apartmentId })), + ) + await insertJournalEntry( + db, + { + entryDate: orig.entryDate, + narration: 'Bill cancelled — reversal', + sourceType: 'ADJUSTMENT', + sourceId: billId, + period: orig.period, + isReversal: true, + reversesId: orig.id, + lines: reversed, + }, + createdBy, + ) + } catch (e) { + console.error('ledger: failed to reverse bill', billId, e) + } +} + // Best-effort wrappers: log and swallow so a ledger hiccup never fails the // underlying bill/payment write. export async function safePostBill(db: Database, billId: string, createdBy?: string): Promise { diff --git a/apps/api/src/routes/bill-config.ts b/apps/api/src/routes/bill-config.ts index b103111..a51a3b4 100644 --- a/apps/api/src/routes/bill-config.ts +++ b/apps/api/src/routes/bill-config.ts @@ -15,7 +15,18 @@ billConfigRoutes.use('*', requireRole('ADMIN')) billConfigRoutes.get('/', async (c) => { const [cfg] = await c.get('db').select().from(billConfig).limit(1) - return c.json(cfg ?? { id: null, dueDayOfMonth: 10, lineItems: [], updatedBy: null, updatedAt: null }) + return c.json( + cfg ?? { + id: null, + dueDayOfMonth: 10, + lineItems: [], + interestEnabled: false, + interestRatePct: 18, + gracePeriodDays: 15, + updatedBy: null, + updatedAt: null, + }, + ) }) billConfigRoutes.put('/', zValidator('json', updateBillConfigSchema), async (c) => { @@ -25,14 +36,29 @@ billConfigRoutes.put('/', zValidator('json', updateBillConfigSchema), async (c) if (existing) { const [updated] = await db .update(billConfig) - .set({ dueDayOfMonth: input.dueDayOfMonth, lineItems: input.lineItems, updatedBy: actingUserId(c), updatedAt: new Date() }) + .set({ + dueDayOfMonth: input.dueDayOfMonth, + lineItems: input.lineItems, + interestEnabled: input.interestEnabled, + interestRatePct: input.interestRatePct, + gracePeriodDays: input.gracePeriodDays, + updatedBy: actingUserId(c), + updatedAt: new Date(), + }) .where(eq(billConfig.id, existing.id)) .returning() return c.json(updated) } const [created] = await db .insert(billConfig) - .values({ dueDayOfMonth: input.dueDayOfMonth, lineItems: input.lineItems, updatedBy: actingUserId(c) }) + .values({ + dueDayOfMonth: input.dueDayOfMonth, + lineItems: input.lineItems, + interestEnabled: input.interestEnabled, + interestRatePct: input.interestRatePct, + gracePeriodDays: input.gracePeriodDays, + updatedBy: actingUserId(c), + }) .returning() return c.json(created, 201) }) diff --git a/apps/api/src/routes/bills.ts b/apps/api/src/routes/bills.ts index 0ca11fc..ee47a8a 100644 --- a/apps/api/src/routes/bills.ts +++ b/apps/api/src/routes/bills.ts @@ -1,14 +1,24 @@ import { Hono } from 'hono' import type { Context } from 'hono' import { zValidator } from '@hono/zod-validator' -import { and, desc, eq, inArray, isNull, sql } from 'drizzle-orm' -import { maintenanceBills, billLineItems, payments, apartments, residencies, societyConfig } from '@opensociety/db' +import { and, desc, eq, inArray, isNotNull, isNull, ne, sql } from 'drizzle-orm' +import { maintenanceBills, billLineItems, payments, apartments, residencies, societyConfig, billConfig } from '@opensociety/db' import type { BillStatus } from '@opensociety/shared' -import { generateBillsSchema, createBillSchema, computeBill, billStatusSchema } from '@opensociety/shared' +import { + generateBillsSchema, + createBillSchema, + computeBill, + billStatusSchema, + sumApartmentInterest, + periodMonthOf, + ACCOUNT_CODES, +} from '@opensociety/shared' import { withDb, withAuth, requireAuth, requireRole, actingUserId } from '../middleware' import { generateMonthlyBills } from '../lib/generate-bills' -import { safePostBill } from '../lib/ledger-posting' +import { safePostBill, safeReverseBill, resolveAccounts } from '../lib/ledger-posting' import { renderInvoicePdf } from '../lib/invoice-pdf' + +const INTEREST_TITLE_PREFIX = 'Interest on arrears' import type { AppEnv } from '../types' // Apartment ids the acting user currently lives in (open residencies). @@ -127,6 +137,153 @@ billRoutes.get('/dues', requireRole('ADMIN'), async (c) => { return c.json(rows) }) +// Per-apartment accrued simple interest on overdue, unpaid, non-interest bills +// as of `asOfMs`, using the society's configured rate + grace. Interest bills +// are excluded from the base so interest never compounds on itself. +async function interestByApartment( + c: Context, + asOfMs: number, + ratePct: number, + graceDays: number, +): Promise> { + const db = c.get('db') + const rows = await db + .select({ + apartmentId: maintenanceBills.apartmentId, + tower: apartments.tower, + apartmentNo: apartments.apartmentNo, + total: maintenanceBills.totalAmount, + dueDate: maintenanceBills.dueDate, + paid: sql`coalesce(sum(${payments.amount}), 0)`, + }) + .from(maintenanceBills) + .innerJoin(apartments, eq(apartments.id, maintenanceBills.apartmentId)) + .leftJoin(payments, eq(payments.billId, maintenanceBills.id)) + .where( + and( + isNotNull(maintenanceBills.dueDate), + ne(maintenanceBills.status, 'CANCELLED'), + sql`${maintenanceBills.title} not like ${INTEREST_TITLE_PREFIX + '%'}`, + ), + ) + .groupBy( + maintenanceBills.id, + maintenanceBills.apartmentId, + apartments.tower, + apartments.apartmentNo, + maintenanceBills.totalAmount, + maintenanceBills.dueDate, + ) + + const byApt = new Map() + for (const r of rows) { + const outstanding = Number(r.total) - Number(r.paid) + if (outstanding <= 0 || !r.dueDate) continue + const entry = byApt.get(r.apartmentId) ?? { label: `${r.tower}-${r.apartmentNo}`, bills: [] } + entry.bills.push({ outstanding, dueDateMs: new Date(r.dueDate as unknown as string).getTime() }) + byApt.set(r.apartmentId, entry) + } + + const out = new Map() + for (const [aptId, { label, bills }] of byApt) { + const interest = sumApartmentInterest(bills, asOfMs, ratePct, graceDays) + if (interest > 0) out.set(aptId, { label, interest }) + } + return out +} + +// Preview accrued interest per flat without charging it (ADMIN). ?asOf=YYYY-MM-DD. +billRoutes.get('/interest-preview', requireRole('ADMIN'), async (c) => { + const [cfg] = await c.get('db').select().from(billConfig).limit(1) + if (!cfg) return c.json({ enabled: false, asOf: null, rows: [] }) + const asOf = c.req.query('asOf') ? new Date(`${c.req.query('asOf')}T00:00:00.000Z`) : new Date() + const map = await interestByApartment(c, asOf.getTime(), cfg.interestRatePct, cfg.gracePeriodDays) + const rows = [...map.entries()].map(([apartmentId, v]) => ({ apartmentId, apartment: v.label, interest: v.interest })) + rows.sort((a, b) => b.interest - a.interest) + return c.json({ + enabled: cfg.interestEnabled, + ratePct: cfg.interestRatePct, + graceDays: cfg.gracePeriodDays, + asOf: asOf.toISOString().slice(0, 10), + rows, + }) +}) + +// Charge accrued interest as one-time bills, one per flat (ADMIN). Idempotent per +// interest period. Requires interest to be enabled in bill config. +billRoutes.post('/apply-interest', requireRole('ADMIN'), async (c) => { + const db = c.get('db') + const [cfg] = await db.select().from(billConfig).limit(1) + if (!cfg || !cfg.interestEnabled) return c.json({ error: 'interest is not enabled' }, 400) + const asOf = c.req.query('asOf') ? new Date(`${c.req.query('asOf')}T00:00:00.000Z`) : new Date() + const period = periodMonthOf(asOf) + + const map = await interestByApartment(c, asOf.getTime(), cfg.interestRatePct, cfg.gracePeriodDays) + if (map.size === 0) return c.json({ created: 0, skipped: 0 }) + + // Skip flats already charged interest for this period (idempotency). + const existing = await db + .select({ apartmentId: maintenanceBills.apartmentId }) + .from(maintenanceBills) + .where(and(eq(maintenanceBills.periodMonth, period), sql`${maintenanceBills.title} like ${INTEREST_TITLE_PREFIX + '%'}`)) + const already = new Set(existing.map((r) => r.apartmentId)) + + const interestAccount = (await resolveAccounts(db, [ACCOUNT_CODES.INTEREST_ON_ARREARS_INCOME])).get( + ACCOUNT_CODES.INTEREST_ON_ARREARS_INCOME, + ) + const title = `${INTEREST_TITLE_PREFIX} — ${period}` + const description = `Interest on overdue dues (${cfg.interestRatePct}% p.a.)` + + let created = 0 + for (const [apartmentId, { interest }] of map) { + if (already.has(apartmentId)) continue + const [bill] = await db + .insert(maintenanceBills) + .values({ + apartmentId, + type: 'ONE_TIME', + title, + periodMonth: period, + subtotal: interest, + taxAmount: 0, + totalAmount: interest, + dueDate: asOf, + createdBy: actingUserId(c), + }) + .returning({ id: maintenanceBills.id }) + await db.insert(billLineItems).values({ + billId: bill.id, + description, + amount: interest, + taxRatePct: 0, + taxAmount: 0, + accountId: interestAccount ?? null, + }) + await safePostBill(db, bill.id, actingUserId(c)) + created++ + } + return c.json({ created, skipped: already.size, period }, created > 0 ? 201 : 200) +}) + +// Cancel a bill (e.g. waive an interest charge) with an optional reason. Sets +// status CANCELLED and posts a reversing ledger entry (§6.9). Idempotent. +billRoutes.post('/:id/cancel', requireRole('ADMIN'), async (c) => { + const db = c.get('db') + const id = c.req.param('id') + const body = await c.req.json().catch(() => ({}) as { reason?: string }) + const [bill] = await db.select().from(maintenanceBills).where(eq(maintenanceBills.id, id)).limit(1) + if (!bill) return c.json({ error: 'not found' }, 404) + if (bill.status === 'CANCELLED') return c.json(bill) + + const [updated] = await db + .update(maintenanceBills) + .set({ status: 'CANCELLED', updatedAt: new Date() }) + .where(eq(maintenanceBills.id, id)) + .returning() + await safeReverseBill(db, id, actingUserId(c)) + return c.json({ ...updated, cancelReason: (body as { reason?: string }).reason ?? null }) +}) + // List bills. Admins see all (filter ?apartmentId, ?status, ?period); residents // only their apartments'. Enriched with paidAmount + apartment label. billRoutes.get('/', async (c) => { diff --git a/apps/web/src/routes/admin/billing.tsx b/apps/web/src/routes/admin/billing.tsx index 94225f8..41f7f85 100644 --- a/apps/web/src/routes/admin/billing.tsx +++ b/apps/web/src/routes/admin/billing.tsx @@ -296,6 +296,11 @@ function BillConfigForm({ initial }: { initial: BillConfig }) { lineItems: lines .filter((l) => l.description.trim() && l.amount) .map((l) => ({ description: l.description.trim(), amount: rupeesToPaise(l.amount), taxRatePct: parseInt(l.taxRatePct) || 0 })), + // Interest-on-arrears settings (#95) are preserved here; the editable + // controls land with the M3.5 admin-UI pass. + interestEnabled: initial.interestEnabled, + interestRatePct: initial.interestRatePct, + gracePeriodDays: initial.gracePeriodDays, }), onSuccess: () => qc.invalidateQueries({ queryKey: ['bill-config'] }), }) diff --git a/packages/db/drizzle/0022_lyrical_corsair.sql b/packages/db/drizzle/0022_lyrical_corsair.sql new file mode 100644 index 0000000..db96a69 --- /dev/null +++ b/packages/db/drizzle/0022_lyrical_corsair.sql @@ -0,0 +1,3 @@ +ALTER TABLE "bill_config" ADD COLUMN "interest_enabled" boolean DEFAULT false NOT NULL;--> statement-breakpoint +ALTER TABLE "bill_config" ADD COLUMN "interest_rate_pct" integer DEFAULT 18 NOT NULL;--> statement-breakpoint +ALTER TABLE "bill_config" ADD COLUMN "grace_period_days" integer DEFAULT 15 NOT NULL; \ No newline at end of file diff --git a/packages/db/drizzle/meta/0022_snapshot.json b/packages/db/drizzle/meta/0022_snapshot.json new file mode 100644 index 0000000..ba28945 --- /dev/null +++ b/packages/db/drizzle/meta/0022_snapshot.json @@ -0,0 +1,3691 @@ +{ + "id": "4d2b4d1f-ec9b-4010-bf36-658de95590f9", + "prevId": "239d2ae5-b084-4214-a53d-9b1e88a77da0", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "clerk_id": { + "name": "clerk_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phone": { + "name": "phone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "user_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'RESIDENT'" + }, + "status": { + "name": "status", + "type": "user_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'PENDING'" + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_clerk_id_unique": { + "name": "users_clerk_id_unique", + "nullsNotDistinct": false, + "columns": [ + "clerk_id" + ] + }, + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + }, + "users_phone_unique": { + "name": "users_phone_unique", + "nullsNotDistinct": false, + "columns": [ + "phone" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.society_config": { + "name": "society_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "city": { + "name": "city", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pincode": { + "name": "pincode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "gstin": { + "name": "gstin", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.apartments": { + "name": "apartments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "tower": { + "name": "tower", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "apartment_no": { + "name": "apartment_no", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "floor": { + "name": "floor", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "bhk_type": { + "name": "bhk_type", + "type": "bhk_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "apartments_tower_apartment_no_unique": { + "name": "apartments_tower_apartment_no_unique", + "nullsNotDistinct": false, + "columns": [ + "tower", + "apartment_no" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.residencies": { + "name": "residencies", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "apartment_id": { + "name": "apartment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "relation": { + "name": "relation", + "type": "residency_relation", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'OWNER'" + }, + "is_primary": { + "name": "is_primary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "start_date": { + "name": "start_date", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "end_date": { + "name": "end_date", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "residencies_user_id_idx": { + "name": "residencies_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "residencies_apartment_id_idx": { + "name": "residencies_apartment_id_idx", + "columns": [ + { + "expression": "apartment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "residencies_user_id_users_id_fk": { + "name": "residencies_user_id_users_id_fk", + "tableFrom": "residencies", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "residencies_apartment_id_apartments_id_fk": { + "name": "residencies_apartment_id_apartments_id_fk", + "tableFrom": "residencies", + "tableTo": "apartments", + "columnsFrom": [ + "apartment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.guards": { + "name": "guards", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "phone": { + "name": "phone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "employee_code": { + "name": "employee_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "guards_user_id_users_id_fk": { + "name": "guards_user_id_users_id_fk", + "tableFrom": "guards", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.visitor_pre_approvals": { + "name": "visitor_pre_approvals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "apartment_id": { + "name": "apartment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "visitor_name": { + "name": "visitor_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "visitor_phone": { + "name": "visitor_phone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "approval_type": { + "name": "approval_type", + "type": "pre_approval_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'ONE_TIME'" + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "valid_from": { + "name": "valid_from", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "valid_until": { + "name": "valid_until", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "max_uses": { + "name": "max_uses", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "use_count": { + "name": "use_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "visitor_pre_approvals_apartment_id_apartments_id_fk": { + "name": "visitor_pre_approvals_apartment_id_apartments_id_fk", + "tableFrom": "visitor_pre_approvals", + "tableTo": "apartments", + "columnsFrom": [ + "apartment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "visitor_pre_approvals_created_by_users_id_fk": { + "name": "visitor_pre_approvals_created_by_users_id_fk", + "tableFrom": "visitor_pre_approvals", + "tableTo": "users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "visitor_pre_approvals_code_unique": { + "name": "visitor_pre_approvals_code_unique", + "nullsNotDistinct": false, + "columns": [ + "code" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.visitor_entries": { + "name": "visitor_entries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "client_id": { + "name": "client_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "apartment_id": { + "name": "apartment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "pre_approval_id": { + "name": "pre_approval_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "visitor_name": { + "name": "visitor_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "visitor_phone": { + "name": "visitor_phone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "visitor_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'GUEST'" + }, + "status": { + "name": "status", + "type": "visitor_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'PENDING'" + }, + "purpose": { + "name": "purpose", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vehicle_number": { + "name": "vehicle_number", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "photo_url": { + "name": "photo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "approved_by": { + "name": "approved_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "denied_reason": { + "name": "denied_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "check_in_by": { + "name": "check_in_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "check_out_by": { + "name": "check_out_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "check_in_at": { + "name": "check_in_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "check_out_at": { + "name": "check_out_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "visitor_entries_status_idx": { + "name": "visitor_entries_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "visitor_entries_apartment_id_idx": { + "name": "visitor_entries_apartment_id_idx", + "columns": [ + { + "expression": "apartment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "visitor_entries_created_at_idx": { + "name": "visitor_entries_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "visitor_entries_client_id_idx": { + "name": "visitor_entries_client_id_idx", + "columns": [ + { + "expression": "client_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "visitor_entries_apartment_id_apartments_id_fk": { + "name": "visitor_entries_apartment_id_apartments_id_fk", + "tableFrom": "visitor_entries", + "tableTo": "apartments", + "columnsFrom": [ + "apartment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "visitor_entries_pre_approval_id_visitor_pre_approvals_id_fk": { + "name": "visitor_entries_pre_approval_id_visitor_pre_approvals_id_fk", + "tableFrom": "visitor_entries", + "tableTo": "visitor_pre_approvals", + "columnsFrom": [ + "pre_approval_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "visitor_entries_approved_by_users_id_fk": { + "name": "visitor_entries_approved_by_users_id_fk", + "tableFrom": "visitor_entries", + "tableTo": "users", + "columnsFrom": [ + "approved_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "visitor_entries_check_in_by_guards_id_fk": { + "name": "visitor_entries_check_in_by_guards_id_fk", + "tableFrom": "visitor_entries", + "tableTo": "guards", + "columnsFrom": [ + "check_in_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "visitor_entries_check_out_by_guards_id_fk": { + "name": "visitor_entries_check_out_by_guards_id_fk", + "tableFrom": "visitor_entries", + "tableTo": "guards", + "columnsFrom": [ + "check_out_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notices": { + "name": "notices", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "priority": { + "name": "priority", + "type": "notice_priority", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'NORMAL'" + }, + "category": { + "name": "category", + "type": "notice_category", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'GENERAL'" + }, + "attachment_url": { + "name": "attachment_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attachment_name": { + "name": "attachment_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "published_by": { + "name": "published_by", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "published_at": { + "name": "published_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "notices_published_by_users_id_fk": { + "name": "notices_published_by_users_id_fk", + "tableFrom": "notices", + "tableTo": "users", + "columnsFrom": [ + "published_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.notice_reads": { + "name": "notice_reads", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "notice_id": { + "name": "notice_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "read_at": { + "name": "read_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "notice_reads_notice_id_notices_id_fk": { + "name": "notice_reads_notice_id_notices_id_fk", + "tableFrom": "notice_reads", + "tableTo": "notices", + "columnsFrom": [ + "notice_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "notice_reads_user_id_users_id_fk": { + "name": "notice_reads_user_id_users_id_fk", + "tableFrom": "notice_reads", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "notice_reads_notice_user_unq": { + "name": "notice_reads_notice_user_unq", + "nullsNotDistinct": false, + "columns": [ + "notice_id", + "user_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.maintenance_tickets": { + "name": "maintenance_tickets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "apartment_id": { + "name": "apartment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "raised_by": { + "name": "raised_by", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "ticket_category", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'OTHER'" + }, + "priority": { + "name": "priority", + "type": "ticket_priority", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'NORMAL'" + }, + "status": { + "name": "status", + "type": "ticket_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'OPEN'" + }, + "assigned_to": { + "name": "assigned_to", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "resolution_note": { + "name": "resolution_note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "maintenance_tickets_status_idx": { + "name": "maintenance_tickets_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "maintenance_tickets_apartment_id_idx": { + "name": "maintenance_tickets_apartment_id_idx", + "columns": [ + { + "expression": "apartment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "maintenance_tickets_category_idx": { + "name": "maintenance_tickets_category_idx", + "columns": [ + { + "expression": "category", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "maintenance_tickets_apartment_id_apartments_id_fk": { + "name": "maintenance_tickets_apartment_id_apartments_id_fk", + "tableFrom": "maintenance_tickets", + "tableTo": "apartments", + "columnsFrom": [ + "apartment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "maintenance_tickets_raised_by_users_id_fk": { + "name": "maintenance_tickets_raised_by_users_id_fk", + "tableFrom": "maintenance_tickets", + "tableTo": "users", + "columnsFrom": [ + "raised_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "maintenance_tickets_assigned_to_users_id_fk": { + "name": "maintenance_tickets_assigned_to_users_id_fk", + "tableFrom": "maintenance_tickets", + "tableTo": "users", + "columnsFrom": [ + "assigned_to" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.house_help": { + "name": "house_help", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "phone": { + "name": "phone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "house_help_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'OTHER'" + }, + "photo_url": { + "name": "photo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_proof_type": { + "name": "id_proof_type", + "type": "id_proof_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "id_proof_number": { + "name": "id_proof_number", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_proof_url": { + "name": "id_proof_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_verified": { + "name": "id_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "background_check": { + "name": "background_check", + "type": "background_check_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'PENDING'" + }, + "incident_count": { + "name": "incident_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "registered_by": { + "name": "registered_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "house_help_registered_by_users_id_fk": { + "name": "house_help_registered_by_users_id_fk", + "tableFrom": "house_help", + "tableTo": "users", + "columnsFrom": [ + "registered_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.house_help_entries": { + "name": "house_help_entries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "house_help_id": { + "name": "house_help_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "apartment_id": { + "name": "apartment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "check_in_at": { + "name": "check_in_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "check_in_by": { + "name": "check_in_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "check_out_at": { + "name": "check_out_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "check_out_by": { + "name": "check_out_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "house_help_entries_house_help_id_idx": { + "name": "house_help_entries_house_help_id_idx", + "columns": [ + { + "expression": "house_help_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "house_help_entries_house_help_id_house_help_id_fk": { + "name": "house_help_entries_house_help_id_house_help_id_fk", + "tableFrom": "house_help_entries", + "tableTo": "house_help", + "columnsFrom": [ + "house_help_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "house_help_entries_apartment_id_apartments_id_fk": { + "name": "house_help_entries_apartment_id_apartments_id_fk", + "tableFrom": "house_help_entries", + "tableTo": "apartments", + "columnsFrom": [ + "apartment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "house_help_entries_check_in_by_users_id_fk": { + "name": "house_help_entries_check_in_by_users_id_fk", + "tableFrom": "house_help_entries", + "tableTo": "users", + "columnsFrom": [ + "check_in_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "house_help_entries_check_out_by_users_id_fk": { + "name": "house_help_entries_check_out_by_users_id_fk", + "tableFrom": "house_help_entries", + "tableTo": "users", + "columnsFrom": [ + "check_out_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.house_help_assignments": { + "name": "house_help_assignments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "house_help_id": { + "name": "house_help_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "apartment_id": { + "name": "apartment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "assigned_by": { + "name": "assigned_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "house_help_assignments_house_help_id_house_help_id_fk": { + "name": "house_help_assignments_house_help_id_house_help_id_fk", + "tableFrom": "house_help_assignments", + "tableTo": "house_help", + "columnsFrom": [ + "house_help_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "house_help_assignments_apartment_id_apartments_id_fk": { + "name": "house_help_assignments_apartment_id_apartments_id_fk", + "tableFrom": "house_help_assignments", + "tableTo": "apartments", + "columnsFrom": [ + "apartment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "house_help_assignments_assigned_by_users_id_fk": { + "name": "house_help_assignments_assigned_by_users_id_fk", + "tableFrom": "house_help_assignments", + "tableTo": "users", + "columnsFrom": [ + "assigned_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "house_help_assignments_help_apartment_unq": { + "name": "house_help_assignments_help_apartment_unq", + "nullsNotDistinct": false, + "columns": [ + "house_help_id", + "apartment_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.house_help_reviews": { + "name": "house_help_reviews", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "house_help_id": { + "name": "house_help_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "reviewer_id": { + "name": "reviewer_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "rating": { + "name": "rating", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "comment": { + "name": "comment", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "house_help_reviews_house_help_id_house_help_id_fk": { + "name": "house_help_reviews_house_help_id_house_help_id_fk", + "tableFrom": "house_help_reviews", + "tableTo": "house_help", + "columnsFrom": [ + "house_help_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "house_help_reviews_reviewer_id_users_id_fk": { + "name": "house_help_reviews_reviewer_id_users_id_fk", + "tableFrom": "house_help_reviews", + "tableTo": "users", + "columnsFrom": [ + "reviewer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "house_help_reviews_help_reviewer_unq": { + "name": "house_help_reviews_help_reviewer_unq", + "nullsNotDistinct": false, + "columns": [ + "house_help_id", + "reviewer_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vehicles": { + "name": "vehicles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "apartment_id": { + "name": "apartment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "registered_by": { + "name": "registered_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "registration_number": { + "name": "registration_number", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "vehicle_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'CAR'" + }, + "make": { + "name": "make", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "vehicles_apartment_id_apartments_id_fk": { + "name": "vehicles_apartment_id_apartments_id_fk", + "tableFrom": "vehicles", + "tableTo": "apartments", + "columnsFrom": [ + "apartment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "vehicles_registered_by_users_id_fk": { + "name": "vehicles_registered_by_users_id_fk", + "tableFrom": "vehicles", + "tableTo": "users", + "columnsFrom": [ + "registered_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "vehicles_registration_number_unq": { + "name": "vehicles_registration_number_unq", + "nullsNotDistinct": false, + "columns": [ + "registration_number" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.parking_slots": { + "name": "parking_slots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "slot_number": { + "name": "slot_number", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "parking_slot_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'OPEN'" + }, + "apartment_id": { + "name": "apartment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "is_temporary": { + "name": "is_temporary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "assigned_until": { + "name": "assigned_until", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "assigned_by": { + "name": "assigned_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "assigned_at": { + "name": "assigned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "is_visitor": { + "name": "is_visitor", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "occupied_by_entry_id": { + "name": "occupied_by_entry_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "occupied_at": { + "name": "occupied_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "parking_slots_apartment_id_idx": { + "name": "parking_slots_apartment_id_idx", + "columns": [ + { + "expression": "apartment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "parking_slots_is_visitor_idx": { + "name": "parking_slots_is_visitor_idx", + "columns": [ + { + "expression": "is_visitor", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "parking_slots_apartment_id_apartments_id_fk": { + "name": "parking_slots_apartment_id_apartments_id_fk", + "tableFrom": "parking_slots", + "tableTo": "apartments", + "columnsFrom": [ + "apartment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "parking_slots_assigned_by_users_id_fk": { + "name": "parking_slots_assigned_by_users_id_fk", + "tableFrom": "parking_slots", + "tableTo": "users", + "columnsFrom": [ + "assigned_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "parking_slots_occupied_by_entry_id_visitor_entries_id_fk": { + "name": "parking_slots_occupied_by_entry_id_visitor_entries_id_fk", + "tableFrom": "parking_slots", + "tableTo": "visitor_entries", + "columnsFrom": [ + "occupied_by_entry_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "parking_slots_slot_number_unq": { + "name": "parking_slots_slot_number_unq", + "nullsNotDistinct": false, + "columns": [ + "slot_number" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.guard_duty_sessions": { + "name": "guard_duty_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "guard_id": { + "name": "guard_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "clock_in_at": { + "name": "clock_in_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "clock_in_lat": { + "name": "clock_in_lat", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "clock_in_lng": { + "name": "clock_in_lng", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "clock_out_at": { + "name": "clock_out_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "clock_out_lat": { + "name": "clock_out_lat", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "clock_out_lng": { + "name": "clock_out_lng", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "guard_duty_sessions_guard_id_guards_id_fk": { + "name": "guard_duty_sessions_guard_id_guards_id_fk", + "tableFrom": "guard_duty_sessions", + "tableTo": "guards", + "columnsFrom": [ + "guard_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.guard_devices": { + "name": "guard_devices", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "guard_id": { + "name": "guard_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "device_id": { + "name": "device_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bound_at": { + "name": "bound_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_active_at": { + "name": "last_active_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "guard_devices_guard_id_guards_id_fk": { + "name": "guard_devices_guard_id_guards_id_fk", + "tableFrom": "guard_devices", + "tableTo": "guards", + "columnsFrom": [ + "guard_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "guard_devices_guard_device_unq": { + "name": "guard_devices_guard_device_unq", + "nullsNotDistinct": false, + "columns": [ + "guard_id", + "device_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bill_line_items": { + "name": "bill_line_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "bill_id": { + "name": "bill_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "tax_rate_pct": { + "name": "tax_rate_pct", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "tax_amount": { + "name": "tax_amount", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "bill_line_items_bill_id_idx": { + "name": "bill_line_items_bill_id_idx", + "columns": [ + { + "expression": "bill_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "bill_line_items_bill_id_maintenance_bills_id_fk": { + "name": "bill_line_items_bill_id_maintenance_bills_id_fk", + "tableFrom": "bill_line_items", + "tableTo": "maintenance_bills", + "columnsFrom": [ + "bill_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "bill_line_items_account_id_accounts_id_fk": { + "name": "bill_line_items_account_id_accounts_id_fk", + "tableFrom": "bill_line_items", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.maintenance_bills": { + "name": "maintenance_bills", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "apartment_id": { + "name": "apartment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "bill_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'MONTHLY'" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "period_month": { + "name": "period_month", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subtotal": { + "name": "subtotal", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "tax_amount": { + "name": "tax_amount", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_amount": { + "name": "total_amount", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "bill_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'ISSUED'" + }, + "due_date": { + "name": "due_date", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "issued_at": { + "name": "issued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "maintenance_bills_apartment_id_idx": { + "name": "maintenance_bills_apartment_id_idx", + "columns": [ + { + "expression": "apartment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "maintenance_bills_period_month_idx": { + "name": "maintenance_bills_period_month_idx", + "columns": [ + { + "expression": "period_month", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "maintenance_bills_apartment_id_apartments_id_fk": { + "name": "maintenance_bills_apartment_id_apartments_id_fk", + "tableFrom": "maintenance_bills", + "tableTo": "apartments", + "columnsFrom": [ + "apartment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "maintenance_bills_created_by_users_id_fk": { + "name": "maintenance_bills_created_by_users_id_fk", + "tableFrom": "maintenance_bills", + "tableTo": "users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.payments": { + "name": "payments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "bill_id": { + "name": "bill_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "apartment_id": { + "name": "apartment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "method": { + "name": "method", + "type": "payment_method", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "reference": { + "name": "reference", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notes": { + "name": "notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "paid_at": { + "name": "paid_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "recorded_by": { + "name": "recorded_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "payments_bill_id_idx": { + "name": "payments_bill_id_idx", + "columns": [ + { + "expression": "bill_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "payments_apartment_id_idx": { + "name": "payments_apartment_id_idx", + "columns": [ + { + "expression": "apartment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "payments_bill_id_maintenance_bills_id_fk": { + "name": "payments_bill_id_maintenance_bills_id_fk", + "tableFrom": "payments", + "tableTo": "maintenance_bills", + "columnsFrom": [ + "bill_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "payments_apartment_id_apartments_id_fk": { + "name": "payments_apartment_id_apartments_id_fk", + "tableFrom": "payments", + "tableTo": "apartments", + "columnsFrom": [ + "apartment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "payments_recorded_by_users_id_fk": { + "name": "payments_recorded_by_users_id_fk", + "tableFrom": "payments", + "tableTo": "users", + "columnsFrom": [ + "recorded_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.bill_config": { + "name": "bill_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "due_day_of_month": { + "name": "due_day_of_month", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10 + }, + "line_items": { + "name": "line_items", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "interest_enabled": { + "name": "interest_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "interest_rate_pct": { + "name": "interest_rate_pct", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 18 + }, + "grace_period_days": { + "name": "grace_period_days", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 15 + }, + "updated_by": { + "name": "updated_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "bill_config_updated_by_users_id_fk": { + "name": "bill_config_updated_by_users_id_fk", + "tableFrom": "bill_config", + "tableTo": "users", + "columnsFrom": [ + "updated_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.accounts": { + "name": "accounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "account_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "is_group": { + "name": "is_group", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_mutual": { + "name": "is_mutual", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "accounts_type_idx": { + "name": "accounts_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "accounts_parent_id_idx": { + "name": "accounts_parent_id_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "accounts_parent_id_accounts_id_fk": { + "name": "accounts_parent_id_accounts_id_fk", + "tableFrom": "accounts", + "tableTo": "accounts", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "accounts_code_unique": { + "name": "accounts_code_unique", + "nullsNotDistinct": false, + "columns": [ + "code" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.journal_entries": { + "name": "journal_entries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "entry_date": { + "name": "entry_date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "narration": { + "name": "narration", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_type": { + "name": "source_type", + "type": "journal_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "period": { + "name": "period", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_reversal": { + "name": "is_reversal", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "reverses_id": { + "name": "reverses_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "journal_entries_period_idx": { + "name": "journal_entries_period_idx", + "columns": [ + { + "expression": "period", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "journal_entries_source_type_idx": { + "name": "journal_entries_source_type_idx", + "columns": [ + { + "expression": "source_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "journal_entries_source_uniq": { + "name": "journal_entries_source_uniq", + "columns": [ + { + "expression": "source_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"journal_entries\".\"source_type\" in ('BILL', 'PAYMENT', 'EXPENSE', 'INTEREST')", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "journal_entries_reverses_id_journal_entries_id_fk": { + "name": "journal_entries_reverses_id_journal_entries_id_fk", + "tableFrom": "journal_entries", + "tableTo": "journal_entries", + "columnsFrom": [ + "reverses_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "journal_entries_created_by_users_id_fk": { + "name": "journal_entries_created_by_users_id_fk", + "tableFrom": "journal_entries", + "tableTo": "users", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.journal_lines": { + "name": "journal_lines", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "entry_id": { + "name": "entry_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "debit": { + "name": "debit", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "credit": { + "name": "credit", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "apartment_id": { + "name": "apartment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "vendor_id": { + "name": "vendor_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "memo": { + "name": "memo", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "journal_lines_entry_id_idx": { + "name": "journal_lines_entry_id_idx", + "columns": [ + { + "expression": "entry_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "journal_lines_account_id_idx": { + "name": "journal_lines_account_id_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "journal_lines_apartment_id_idx": { + "name": "journal_lines_apartment_id_idx", + "columns": [ + { + "expression": "apartment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "journal_lines_entry_id_journal_entries_id_fk": { + "name": "journal_lines_entry_id_journal_entries_id_fk", + "tableFrom": "journal_lines", + "tableTo": "journal_entries", + "columnsFrom": [ + "entry_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "journal_lines_account_id_accounts_id_fk": { + "name": "journal_lines_account_id_accounts_id_fk", + "tableFrom": "journal_lines", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "journal_lines_apartment_id_apartments_id_fk": { + "name": "journal_lines_apartment_id_apartments_id_fk", + "tableFrom": "journal_lines", + "tableTo": "apartments", + "columnsFrom": [ + "apartment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.expenses": { + "name": "expenses", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "vendor_id": { + "name": "vendor_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "tax_amount": { + "name": "tax_amount", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "expense_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'PAID'" + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "bill_ref": { + "name": "bill_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "method": { + "name": "method", + "type": "payment_method", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "paid_at": { + "name": "paid_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "attachment_url": { + "name": "attachment_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "recorded_by": { + "name": "recorded_by", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "expenses_account_id_idx": { + "name": "expenses_account_id_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "expenses_vendor_id_idx": { + "name": "expenses_vendor_id_idx", + "columns": [ + { + "expression": "vendor_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "expenses_created_at_idx": { + "name": "expenses_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "expenses_vendor_id_vendors_id_fk": { + "name": "expenses_vendor_id_vendors_id_fk", + "tableFrom": "expenses", + "tableTo": "vendors", + "columnsFrom": [ + "vendor_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "expenses_account_id_accounts_id_fk": { + "name": "expenses_account_id_accounts_id_fk", + "tableFrom": "expenses", + "tableTo": "accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "expenses_recorded_by_users_id_fk": { + "name": "expenses_recorded_by_users_id_fk", + "tableFrom": "expenses", + "tableTo": "users", + "columnsFrom": [ + "recorded_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tds_entries": { + "name": "tds_entries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "expense_id": { + "name": "expense_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "section": { + "name": "section", + "type": "tds_section", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "rate_pct": { + "name": "rate_pct", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "deducted_at": { + "name": "deducted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "tds_entries_expense_id_idx": { + "name": "tds_entries_expense_id_idx", + "columns": [ + { + "expression": "expense_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "tds_entries_section_idx": { + "name": "tds_entries_section_idx", + "columns": [ + { + "expression": "section", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "tds_entries_expense_id_expenses_id_fk": { + "name": "tds_entries_expense_id_expenses_id_fk", + "tableFrom": "tds_entries", + "tableTo": "expenses", + "columnsFrom": [ + "expense_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.vendors": { + "name": "vendors", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pan": { + "name": "pan", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "gstin": { + "name": "gstin", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contact": { + "name": "contact", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "bank_account": { + "name": "bank_account", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.account_type": { + "name": "account_type", + "schema": "public", + "values": [ + "ASSET", + "LIABILITY", + "INCOME", + "EXPENSE", + "FUND" + ] + }, + "public.background_check_status": { + "name": "background_check_status", + "schema": "public", + "values": [ + "PENDING", + "CLEARED", + "FLAGGED" + ] + }, + "public.bhk_type": { + "name": "bhk_type", + "schema": "public", + "values": [ + "STUDIO", + "1BHK", + "2BHK", + "3BHK", + "4BHK", + "5BHK", + "PENTHOUSE" + ] + }, + "public.bill_status": { + "name": "bill_status", + "schema": "public", + "values": [ + "ISSUED", + "PARTIALLY_PAID", + "PAID", + "CANCELLED" + ] + }, + "public.bill_type": { + "name": "bill_type", + "schema": "public", + "values": [ + "MONTHLY", + "ONE_TIME" + ] + }, + "public.expense_status": { + "name": "expense_status", + "schema": "public", + "values": [ + "PAID", + "PAYABLE" + ] + }, + "public.house_help_type": { + "name": "house_help_type", + "schema": "public", + "values": [ + "MAID", + "COOK", + "DRIVER", + "NANNY", + "GARDENER", + "CARETAKER", + "OTHER" + ] + }, + "public.id_proof_type": { + "name": "id_proof_type", + "schema": "public", + "values": [ + "AADHAAR", + "PAN", + "VOTER_ID", + "DRIVING_LICENSE", + "PASSPORT", + "OTHER" + ] + }, + "public.journal_source": { + "name": "journal_source", + "schema": "public", + "values": [ + "BILL", + "PAYMENT", + "EXPENSE", + "INTEREST", + "WAIVER", + "MANUAL", + "OPENING", + "ADJUSTMENT" + ] + }, + "public.notice_category": { + "name": "notice_category", + "schema": "public", + "values": [ + "GENERAL", + "MAINTENANCE", + "EVENT", + "SECURITY", + "BILLING", + "EMERGENCY" + ] + }, + "public.notice_priority": { + "name": "notice_priority", + "schema": "public", + "values": [ + "LOW", + "NORMAL", + "HIGH", + "URGENT" + ] + }, + "public.parking_slot_type": { + "name": "parking_slot_type", + "schema": "public", + "values": [ + "COVERED", + "OPEN" + ] + }, + "public.payment_method": { + "name": "payment_method", + "schema": "public", + "values": [ + "CASH", + "CHEQUE", + "UPI", + "BANK_TRANSFER", + "ONLINE" + ] + }, + "public.pre_approval_type": { + "name": "pre_approval_type", + "schema": "public", + "values": [ + "ALWAYS", + "SCHEDULED", + "ONE_TIME" + ] + }, + "public.residency_relation": { + "name": "residency_relation", + "schema": "public", + "values": [ + "OWNER", + "TENANT", + "FAMILY" + ] + }, + "public.tds_section": { + "name": "tds_section", + "schema": "public", + "values": [ + "SEC_194C", + "SEC_194J", + "SEC_194I" + ] + }, + "public.ticket_category": { + "name": "ticket_category", + "schema": "public", + "values": [ + "PLUMBING", + "ELECTRICAL", + "CARPENTRY", + "HOUSEKEEPING", + "SECURITY", + "OTHER" + ] + }, + "public.ticket_priority": { + "name": "ticket_priority", + "schema": "public", + "values": [ + "LOW", + "NORMAL", + "HIGH", + "URGENT" + ] + }, + "public.ticket_status": { + "name": "ticket_status", + "schema": "public", + "values": [ + "OPEN", + "IN_PROGRESS", + "RESOLVED", + "CLOSED", + "CANCELLED" + ] + }, + "public.user_role": { + "name": "user_role", + "schema": "public", + "values": [ + "RESIDENT", + "GUARD", + "ADMIN", + "STAFF" + ] + }, + "public.user_status": { + "name": "user_status", + "schema": "public", + "values": [ + "PENDING", + "APPROVED", + "SUSPENDED" + ] + }, + "public.vehicle_type": { + "name": "vehicle_type", + "schema": "public", + "values": [ + "CAR", + "BIKE", + "SCOOTER", + "BICYCLE", + "OTHER" + ] + }, + "public.visitor_status": { + "name": "visitor_status", + "schema": "public", + "values": [ + "PENDING", + "APPROVED", + "DENIED", + "ENTERED", + "EXITED", + "CANCELLED", + "EXPIRED" + ] + }, + "public.visitor_type": { + "name": "visitor_type", + "schema": "public", + "values": [ + "GUEST", + "DELIVERY", + "SERVICE", + "CAB", + "OTHER" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/db/drizzle/meta/_journal.json b/packages/db/drizzle/meta/_journal.json index b5cf43a..95f3dad 100644 --- a/packages/db/drizzle/meta/_journal.json +++ b/packages/db/drizzle/meta/_journal.json @@ -155,6 +155,13 @@ "when": 1784199389317, "tag": "0021_neat_giant_man", "breakpoints": true + }, + { + "idx": 22, + "version": "7", + "when": 1784200007747, + "tag": "0022_lyrical_corsair", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/db/src/schema/bill-config.ts b/packages/db/src/schema/bill-config.ts index 20c04cd..1885eb8 100644 --- a/packages/db/src/schema/bill-config.ts +++ b/packages/db/src/schema/bill-config.ts @@ -1,9 +1,10 @@ -import { pgTable, uuid, integer, jsonb, timestamp } from 'drizzle-orm/pg-core' +import { pgTable, uuid, integer, jsonb, timestamp, boolean } from 'drizzle-orm/pg-core' import { users } from './users' // Singleton recurring-bill template used by the monthly auto-generation cron. // lineItems is the set of charges applied to every flat (amounts in paise, GST%); // dueDayOfMonth sets each bill's due date within the billing month. + export const billConfig = pgTable('bill_config', { id: uuid('id').primaryKey().defaultRandom(), dueDayOfMonth: integer('due_day_of_month').notNull().default(10), @@ -11,6 +12,11 @@ export const billConfig = pgTable('bill_config', { .$type<{ description: string; amount: number; taxRatePct: number; accountId?: string }[]>() .notNull() .default([]), + // Interest on arrears (#95). Opt-in, simple (not compound), waivable. Rate is + // an annual percent capped by the society's registered bye-laws (Bye-law 71). + interestEnabled: boolean('interest_enabled').notNull().default(false), + interestRatePct: integer('interest_rate_pct').notNull().default(18), + gracePeriodDays: integer('grace_period_days').notNull().default(15), updatedBy: uuid('updated_by').references(() => users.id), updatedAt: timestamp('updated_at').defaultNow().notNull(), }) diff --git a/packages/shared/src/finance.ts b/packages/shared/src/finance.ts index f244e31..8b65234 100644 --- a/packages/shared/src/finance.ts +++ b/packages/shared/src/finance.ts @@ -111,6 +111,9 @@ export const billConfigSchema = z.object({ id: z.string().uuid(), dueDayOfMonth: z.number(), lineItems: z.array(billLineInputSchema), + interestEnabled: z.boolean(), + interestRatePct: z.number(), + gracePeriodDays: z.number(), updatedBy: z.string().uuid().nullable(), updatedAt: z.string(), }) @@ -118,6 +121,9 @@ export const billConfigSchema = z.object({ export const updateBillConfigSchema = z.object({ dueDayOfMonth: z.number().int().min(1).max(28).default(10), lineItems: z.array(billLineInputSchema), + interestEnabled: z.boolean().default(false), + interestRatePct: z.number().int().min(0).max(50).default(18), + gracePeriodDays: z.number().int().min(0).max(90).default(15), }) // 'YYYY-MM' for a Date (UTC). @@ -133,6 +139,39 @@ export function dueDateForPeriod(periodMonth: string, dayOfMonth: number): strin return new Date(Date.UTC(y, m - 1, day)).toISOString() } +// ----- Interest on arrears (#95) ----- + +// Simple interest (never compound) on an overdue balance, in paise. Accrues from +// the due date + grace period up to `asOf`, at `ratePct` per annum. Returns 0 +// when nothing is overdue, the rate is off, or we're still inside the grace +// window. Bye-law 71 caps this (≤21% legacy / ~12% revised MH) — the rate is +// always supplied by config, never assumed here. +export function computeInterest( + outstanding: number, + dueDateMs: number, + asOfMs: number, + ratePct: number, + graceDays: number, +): number { + if (outstanding <= 0 || ratePct <= 0) return 0 + const start = dueDateMs + Math.max(0, graceDays) * 86_400_000 + if (asOfMs <= start) return 0 + const days = Math.floor((asOfMs - start) / 86_400_000) + if (days <= 0) return 0 + return Math.round((outstanding * ratePct * days) / (100 * 365)) +} + +// Total simple interest across an apartment's overdue bills (each with its own +// due date + remaining balance), as of `asOf`. +export function sumApartmentInterest( + bills: { outstanding: number; dueDateMs: number }[], + asOfMs: number, + ratePct: number, + graceDays: number, +): number { + return bills.reduce((s, b) => s + computeInterest(b.outstanding, b.dueDateMs, asOfMs, ratePct, graceDays), 0) +} + export type CollectionRow = { period: string; billed: number; collected: number } export type MethodRow = { method: string; amount: number } export type FinanceReport = { diff --git a/packages/shared/src/ledger.ts b/packages/shared/src/ledger.ts index e64d3af..79e553f 100644 --- a/packages/shared/src/ledger.ts +++ b/packages/shared/src/ledger.ts @@ -86,9 +86,18 @@ export type JournalDraft = { sourceType: z.infer sourceId?: string | null period: string // 'YYYY-MM' + isReversal?: boolean + reversesId?: string | null lines: JournalLineInput[] } +// Build the reversing entry for an existing set of lines (swap debit/credit), +// posted as an ADJUSTMENT so it never collides with the original's idempotency +// slot. Used for bill cancellation (§6.9) and other corrections. +export function reverseLines(lines: JournalLineInput[]): JournalLineInput[] { + return lines.map((l) => ({ ...l, debit: l.credit, credit: l.debit })) +} + // ----- Balanced-entry invariant ----- export function sumDebits(lines: JournalLineInput[]): number {