initial: standalone repo from monorepo split

This commit is contained in:
kevin-asprec
2026-04-13 09:37:07 +08:00
commit 5382f3b4e5
87 changed files with 5278 additions and 0 deletions

28
packages/db/package.json Normal file
View File

@@ -0,0 +1,28 @@
{
"name": "@fiberops/db",
"version": "0.1.0",
"private": true,
"main": "./src/index.ts",
"types": "./src/index.ts",
"scripts": {
"build": "tsc",
"generate": "prisma generate",
"migrate:dev": "prisma migrate dev",
"migrate:deploy": "prisma migrate deploy",
"seed": "tsx prisma/seed.ts",
"studio": "prisma studio",
"lint": "tsc --noEmit",
"clean": "rm -rf dist"
},
"prisma": {
"seed": "tsx prisma/seed.ts"
},
"dependencies": {
"@prisma/client": "^6.5.0"
},
"devDependencies": {
"prisma": "^6.5.0",
"tsx": "^4.19.0",
"typescript": "^5.7.0"
}
}

View File

@@ -0,0 +1,361 @@
-- CreateTable
CREATE TABLE "tenants" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"slug" TEXT NOT NULL,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"settings" JSONB NOT NULL DEFAULT '{}',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "tenants_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "users" (
"id" TEXT NOT NULL,
"tenantId" TEXT NOT NULL,
"email" TEXT NOT NULL,
"password" TEXT NOT NULL,
"firstName" TEXT NOT NULL,
"lastName" TEXT NOT NULL,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "user_roles" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"role" TEXT NOT NULL,
CONSTRAINT "user_roles_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "refresh_tokens" (
"id" TEXT NOT NULL,
"token" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"expiresAt" TIMESTAMP(3) NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "refresh_tokens_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "areas" (
"id" TEXT NOT NULL,
"tenantId" TEXT NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "areas_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "plans" (
"id" TEXT NOT NULL,
"tenantId" TEXT NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"speedDown" INTEGER NOT NULL,
"speedUp" INTEGER NOT NULL,
"price" DECIMAL(10,2) NOT NULL,
"billingCycle" INTEGER NOT NULL DEFAULT 30,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "plans_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "clients" (
"id" TEXT NOT NULL,
"tenantId" TEXT NOT NULL,
"accountNumber" TEXT NOT NULL,
"firstName" TEXT NOT NULL,
"lastName" TEXT NOT NULL,
"email" TEXT,
"phone" TEXT,
"address" TEXT NOT NULL,
"areaId" TEXT,
"status" TEXT NOT NULL DEFAULT 'active',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "clients_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "subscriptions" (
"id" TEXT NOT NULL,
"tenantId" TEXT NOT NULL,
"clientId" TEXT NOT NULL,
"planId" TEXT NOT NULL,
"type" TEXT NOT NULL,
"status" TEXT NOT NULL DEFAULT 'pending',
"startDate" TIMESTAMP(3),
"endDate" TIMESTAMP(3),
"installedAt" TIMESTAMP(3),
"activatedAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "subscriptions_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "invoices" (
"id" TEXT NOT NULL,
"tenantId" TEXT NOT NULL,
"clientId" TEXT NOT NULL,
"number" TEXT NOT NULL,
"amount" DECIMAL(10,2) NOT NULL,
"balance" DECIMAL(10,2) NOT NULL,
"status" TEXT NOT NULL DEFAULT 'draft',
"dueDate" TIMESTAMP(3) NOT NULL,
"paidAt" TIMESTAMP(3),
"periodStart" TIMESTAMP(3),
"periodEnd" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "invoices_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "payments" (
"id" TEXT NOT NULL,
"tenantId" TEXT NOT NULL,
"clientId" TEXT NOT NULL,
"invoiceId" TEXT,
"collectedById" TEXT,
"amount" DECIMAL(10,2) NOT NULL,
"method" TEXT NOT NULL,
"referenceNo" TEXT,
"notes" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "payments_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "remittances" (
"id" TEXT NOT NULL,
"tenantId" TEXT NOT NULL,
"collectorId" TEXT NOT NULL,
"confirmedById" TEXT,
"totalAmount" DECIMAL(10,2) NOT NULL,
"status" TEXT NOT NULL DEFAULT 'pending',
"submittedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"confirmedAt" TIMESTAMP(3),
"notes" TEXT,
CONSTRAINT "remittances_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "tickets" (
"id" TEXT NOT NULL,
"tenantId" TEXT NOT NULL,
"clientId" TEXT,
"createdById" TEXT NOT NULL,
"assigneeId" TEXT,
"type" TEXT NOT NULL,
"status" TEXT NOT NULL DEFAULT 'open',
"title" TEXT NOT NULL,
"description" TEXT,
"priority" TEXT NOT NULL DEFAULT 'normal',
"resolvedAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "tickets_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "notifications" (
"id" TEXT NOT NULL,
"tenantId" TEXT NOT NULL,
"userId" TEXT,
"clientId" TEXT,
"type" TEXT NOT NULL,
"channel" TEXT NOT NULL,
"title" TEXT NOT NULL,
"message" TEXT NOT NULL,
"isRead" BOOLEAN NOT NULL DEFAULT false,
"sentAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "notifications_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "tenants_slug_key" ON "tenants"("slug");
-- CreateIndex
CREATE INDEX "users_tenantId_idx" ON "users"("tenantId");
-- CreateIndex
CREATE UNIQUE INDEX "users_tenantId_email_key" ON "users"("tenantId", "email");
-- CreateIndex
CREATE UNIQUE INDEX "user_roles_userId_role_key" ON "user_roles"("userId", "role");
-- CreateIndex
CREATE UNIQUE INDEX "refresh_tokens_token_key" ON "refresh_tokens"("token");
-- CreateIndex
CREATE INDEX "refresh_tokens_userId_idx" ON "refresh_tokens"("userId");
-- CreateIndex
CREATE INDEX "refresh_tokens_expiresAt_idx" ON "refresh_tokens"("expiresAt");
-- CreateIndex
CREATE INDEX "areas_tenantId_idx" ON "areas"("tenantId");
-- CreateIndex
CREATE UNIQUE INDEX "areas_tenantId_name_key" ON "areas"("tenantId", "name");
-- CreateIndex
CREATE INDEX "plans_tenantId_idx" ON "plans"("tenantId");
-- CreateIndex
CREATE UNIQUE INDEX "plans_tenantId_name_key" ON "plans"("tenantId", "name");
-- CreateIndex
CREATE INDEX "clients_tenantId_idx" ON "clients"("tenantId");
-- CreateIndex
CREATE INDEX "clients_tenantId_status_idx" ON "clients"("tenantId", "status");
-- CreateIndex
CREATE UNIQUE INDEX "clients_tenantId_accountNumber_key" ON "clients"("tenantId", "accountNumber");
-- CreateIndex
CREATE INDEX "subscriptions_tenantId_idx" ON "subscriptions"("tenantId");
-- CreateIndex
CREATE INDEX "subscriptions_tenantId_status_idx" ON "subscriptions"("tenantId", "status");
-- CreateIndex
CREATE INDEX "subscriptions_clientId_idx" ON "subscriptions"("clientId");
-- CreateIndex
CREATE INDEX "invoices_tenantId_idx" ON "invoices"("tenantId");
-- CreateIndex
CREATE INDEX "invoices_tenantId_status_idx" ON "invoices"("tenantId", "status");
-- CreateIndex
CREATE INDEX "invoices_clientId_idx" ON "invoices"("clientId");
-- CreateIndex
CREATE UNIQUE INDEX "invoices_tenantId_number_key" ON "invoices"("tenantId", "number");
-- CreateIndex
CREATE INDEX "payments_tenantId_idx" ON "payments"("tenantId");
-- CreateIndex
CREATE INDEX "payments_clientId_idx" ON "payments"("clientId");
-- CreateIndex
CREATE INDEX "payments_invoiceId_idx" ON "payments"("invoiceId");
-- CreateIndex
CREATE INDEX "remittances_tenantId_idx" ON "remittances"("tenantId");
-- CreateIndex
CREATE INDEX "remittances_collectorId_idx" ON "remittances"("collectorId");
-- CreateIndex
CREATE INDEX "tickets_tenantId_idx" ON "tickets"("tenantId");
-- CreateIndex
CREATE INDEX "tickets_tenantId_type_status_idx" ON "tickets"("tenantId", "type", "status");
-- CreateIndex
CREATE INDEX "tickets_clientId_idx" ON "tickets"("clientId");
-- CreateIndex
CREATE INDEX "tickets_assigneeId_idx" ON "tickets"("assigneeId");
-- CreateIndex
CREATE INDEX "notifications_tenantId_idx" ON "notifications"("tenantId");
-- CreateIndex
CREATE INDEX "notifications_userId_isRead_idx" ON "notifications"("userId", "isRead");
-- AddForeignKey
ALTER TABLE "users" ADD CONSTRAINT "users_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "tenants"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "user_roles" ADD CONSTRAINT "user_roles_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "areas" ADD CONSTRAINT "areas_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "tenants"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "plans" ADD CONSTRAINT "plans_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "tenants"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "clients" ADD CONSTRAINT "clients_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "tenants"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "clients" ADD CONSTRAINT "clients_areaId_fkey" FOREIGN KEY ("areaId") REFERENCES "areas"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "subscriptions" ADD CONSTRAINT "subscriptions_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "tenants"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "subscriptions" ADD CONSTRAINT "subscriptions_clientId_fkey" FOREIGN KEY ("clientId") REFERENCES "clients"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "subscriptions" ADD CONSTRAINT "subscriptions_planId_fkey" FOREIGN KEY ("planId") REFERENCES "plans"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "invoices" ADD CONSTRAINT "invoices_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "tenants"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "invoices" ADD CONSTRAINT "invoices_clientId_fkey" FOREIGN KEY ("clientId") REFERENCES "clients"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "payments" ADD CONSTRAINT "payments_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "tenants"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "payments" ADD CONSTRAINT "payments_clientId_fkey" FOREIGN KEY ("clientId") REFERENCES "clients"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "payments" ADD CONSTRAINT "payments_invoiceId_fkey" FOREIGN KEY ("invoiceId") REFERENCES "invoices"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "payments" ADD CONSTRAINT "payments_collectedById_fkey" FOREIGN KEY ("collectedById") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "remittances" ADD CONSTRAINT "remittances_collectorId_fkey" FOREIGN KEY ("collectorId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "remittances" ADD CONSTRAINT "remittances_confirmedById_fkey" FOREIGN KEY ("confirmedById") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "tickets" ADD CONSTRAINT "tickets_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "tenants"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "tickets" ADD CONSTRAINT "tickets_clientId_fkey" FOREIGN KEY ("clientId") REFERENCES "clients"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "tickets" ADD CONSTRAINT "tickets_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "tickets" ADD CONSTRAINT "tickets_assigneeId_fkey" FOREIGN KEY ("assigneeId") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "notifications" ADD CONSTRAINT "notifications_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "tenants"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -0,0 +1,23 @@
-- CreateTable
CREATE TABLE "audit_logs" (
"id" TEXT NOT NULL,
"tenantId" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"action" TEXT NOT NULL,
"entity" TEXT NOT NULL,
"entityId" TEXT NOT NULL,
"details" JSONB NOT NULL DEFAULT '{}',
"ipAddress" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "audit_logs_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "audit_logs_tenantId_idx" ON "audit_logs"("tenantId");
-- CreateIndex
CREATE INDEX "audit_logs_tenantId_entity_idx" ON "audit_logs"("tenantId", "entity");
-- CreateIndex
CREATE INDEX "audit_logs_userId_idx" ON "audit_logs"("userId");

View File

@@ -0,0 +1,135 @@
-- CreateTable
CREATE TABLE "employees" (
"id" TEXT NOT NULL,
"tenantId" TEXT NOT NULL,
"userId" TEXT,
"firstName" TEXT NOT NULL,
"lastName" TEXT NOT NULL,
"email" TEXT,
"phone" TEXT,
"position" TEXT NOT NULL,
"department" TEXT,
"employeeNo" TEXT NOT NULL,
"status" TEXT NOT NULL DEFAULT 'active',
"hireDate" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"terminatedAt" TIMESTAMP(3),
"salary" DECIMAL(10,2),
"notes" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "employees_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "expenses" (
"id" TEXT NOT NULL,
"tenantId" TEXT NOT NULL,
"createdById" TEXT NOT NULL,
"approvedById" TEXT,
"category" TEXT NOT NULL,
"description" TEXT NOT NULL,
"amount" DECIMAL(10,2) NOT NULL,
"receiptUrl" TEXT,
"status" TEXT NOT NULL DEFAULT 'pending',
"expenseDate" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"approvedAt" TIMESTAMP(3),
"notes" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "expenses_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "company_accounts" (
"id" TEXT NOT NULL,
"tenantId" TEXT NOT NULL,
"name" TEXT NOT NULL,
"type" TEXT NOT NULL,
"accountNo" TEXT,
"balance" DECIMAL(12,2) NOT NULL DEFAULT 0,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "company_accounts_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "fund_transfers" (
"id" TEXT NOT NULL,
"tenantId" TEXT NOT NULL,
"fromAccountId" TEXT NOT NULL,
"toAccountId" TEXT NOT NULL,
"amount" DECIMAL(12,2) NOT NULL,
"description" TEXT,
"transferredBy" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "fund_transfers_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "assets" (
"id" TEXT NOT NULL,
"tenantId" TEXT NOT NULL,
"name" TEXT NOT NULL,
"category" TEXT NOT NULL,
"serialNumber" TEXT,
"purchaseDate" TIMESTAMP(3),
"purchasePrice" DECIMAL(10,2),
"assignedToId" TEXT,
"status" TEXT NOT NULL DEFAULT 'available',
"location" TEXT,
"notes" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "assets_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "employees_tenantId_idx" ON "employees"("tenantId");
-- CreateIndex
CREATE INDEX "employees_tenantId_status_idx" ON "employees"("tenantId", "status");
-- CreateIndex
CREATE UNIQUE INDEX "employees_tenantId_employeeNo_key" ON "employees"("tenantId", "employeeNo");
-- CreateIndex
CREATE INDEX "expenses_tenantId_idx" ON "expenses"("tenantId");
-- CreateIndex
CREATE INDEX "expenses_tenantId_status_idx" ON "expenses"("tenantId", "status");
-- CreateIndex
CREATE INDEX "expenses_tenantId_category_idx" ON "expenses"("tenantId", "category");
-- CreateIndex
CREATE INDEX "company_accounts_tenantId_idx" ON "company_accounts"("tenantId");
-- CreateIndex
CREATE UNIQUE INDEX "company_accounts_tenantId_name_key" ON "company_accounts"("tenantId", "name");
-- CreateIndex
CREATE INDEX "fund_transfers_tenantId_idx" ON "fund_transfers"("tenantId");
-- CreateIndex
CREATE INDEX "assets_tenantId_idx" ON "assets"("tenantId");
-- CreateIndex
CREATE INDEX "assets_tenantId_status_idx" ON "assets"("tenantId", "status");
-- CreateIndex
CREATE INDEX "assets_tenantId_category_idx" ON "assets"("tenantId", "category");
-- AddForeignKey
ALTER TABLE "fund_transfers" ADD CONSTRAINT "fund_transfers_fromAccountId_fkey" FOREIGN KEY ("fromAccountId") REFERENCES "company_accounts"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "fund_transfers" ADD CONSTRAINT "fund_transfers_toAccountId_fkey" FOREIGN KEY ("toAccountId") REFERENCES "company_accounts"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "assets" ADD CONSTRAINT "assets_assignedToId_fkey" FOREIGN KEY ("assignedToId") REFERENCES "employees"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@@ -0,0 +1,90 @@
-- CreateTable
CREATE TABLE "billing_settings" (
"id" TEXT NOT NULL,
"tenantId" TEXT NOT NULL,
"autoGenerate" BOOLEAN NOT NULL DEFAULT true,
"gracePeriodDays" INTEGER NOT NULL DEFAULT 7,
"dueDateOffsetDays" INTEGER NOT NULL DEFAULT 15,
"lateFeePercent" DECIMAL(5,2) NOT NULL DEFAULT 0,
"invoicePrefix" TEXT NOT NULL DEFAULT 'INV',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "billing_settings_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "chart_of_accounts" (
"id" TEXT NOT NULL,
"tenantId" TEXT NOT NULL,
"code" TEXT NOT NULL,
"name" TEXT NOT NULL,
"type" TEXT NOT NULL,
"parentId" TEXT,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"isSystem" BOOLEAN NOT NULL DEFAULT false,
"balance" DECIMAL(14,2) NOT NULL DEFAULT 0,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "chart_of_accounts_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "journal_entries" (
"id" TEXT NOT NULL,
"tenantId" TEXT NOT NULL,
"entryDate" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"description" TEXT NOT NULL,
"reference" TEXT,
"sourceType" TEXT,
"sourceId" TEXT,
"createdById" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "journal_entries_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "journal_lines" (
"id" TEXT NOT NULL,
"journalEntryId" TEXT NOT NULL,
"accountId" TEXT NOT NULL,
"debit" DECIMAL(14,2) NOT NULL DEFAULT 0,
"credit" DECIMAL(14,2) NOT NULL DEFAULT 0,
CONSTRAINT "journal_lines_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "billing_settings_tenantId_key" ON "billing_settings"("tenantId");
-- CreateIndex
CREATE INDEX "chart_of_accounts_tenantId_idx" ON "chart_of_accounts"("tenantId");
-- CreateIndex
CREATE INDEX "chart_of_accounts_tenantId_type_idx" ON "chart_of_accounts"("tenantId", "type");
-- CreateIndex
CREATE UNIQUE INDEX "chart_of_accounts_tenantId_code_key" ON "chart_of_accounts"("tenantId", "code");
-- CreateIndex
CREATE INDEX "journal_entries_tenantId_idx" ON "journal_entries"("tenantId");
-- CreateIndex
CREATE INDEX "journal_entries_tenantId_sourceType_sourceId_idx" ON "journal_entries"("tenantId", "sourceType", "sourceId");
-- CreateIndex
CREATE INDEX "journal_lines_journalEntryId_idx" ON "journal_lines"("journalEntryId");
-- CreateIndex
CREATE INDEX "journal_lines_accountId_idx" ON "journal_lines"("accountId");
-- AddForeignKey
ALTER TABLE "chart_of_accounts" ADD CONSTRAINT "chart_of_accounts_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "chart_of_accounts"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "journal_lines" ADD CONSTRAINT "journal_lines_journalEntryId_fkey" FOREIGN KEY ("journalEntryId") REFERENCES "journal_entries"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "journal_lines" ADD CONSTRAINT "journal_lines_accountId_fkey" FOREIGN KEY ("accountId") REFERENCES "chart_of_accounts"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "company_accounts" ADD COLUMN "chartOfAccountId" TEXT,
ADD COLUMN "isSystem" BOOLEAN NOT NULL DEFAULT false;

View File

@@ -0,0 +1,14 @@
-- CreateTable
CREATE TABLE "remittance_payments" (
"id" TEXT NOT NULL,
"remittanceId" TEXT NOT NULL,
"paymentId" TEXT NOT NULL,
CONSTRAINT "remittance_payments_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "remittance_payments_remittanceId_paymentId_key" ON "remittance_payments"("remittanceId", "paymentId");
-- AddForeignKey
ALTER TABLE "remittance_payments" ADD CONSTRAINT "remittance_payments_remittanceId_fkey" FOREIGN KEY ("remittanceId") REFERENCES "remittances"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -0,0 +1,183 @@
-- Sync schema drift: soft-delete columns, mustChangePassword, RBAC tables,
-- recurring expenses, and payroll tables.
-- Uses IF NOT EXISTS / IF NOT EXISTS for idempotency (safe on fresh and db-pushed databases).
-- ── Soft-delete columns ──
ALTER TABLE "tenants" ADD COLUMN IF NOT EXISTS "deletedAt" TIMESTAMP(3);
ALTER TABLE "users" ADD COLUMN IF NOT EXISTS "deletedAt" TIMESTAMP(3);
ALTER TABLE "users" ADD COLUMN IF NOT EXISTS "mustChangePassword" BOOLEAN NOT NULL DEFAULT false;
ALTER TABLE "subscriptions" ADD COLUMN IF NOT EXISTS "deletedAt" TIMESTAMP(3);
ALTER TABLE "invoices" ADD COLUMN IF NOT EXISTS "deletedAt" TIMESTAMP(3);
ALTER TABLE "payments" ADD COLUMN IF NOT EXISTS "deletedAt" TIMESTAMP(3);
ALTER TABLE "remittances" ADD COLUMN IF NOT EXISTS "deletedAt" TIMESTAMP(3);
ALTER TABLE "tickets" ADD COLUMN IF NOT EXISTS "deletedAt" TIMESTAMP(3);
ALTER TABLE "areas" ADD COLUMN IF NOT EXISTS "deletedAt" TIMESTAMP(3);
ALTER TABLE "plans" ADD COLUMN IF NOT EXISTS "deletedAt" TIMESTAMP(3);
ALTER TABLE "clients" ADD COLUMN IF NOT EXISTS "deletedAt" TIMESTAMP(3);
ALTER TABLE "employees" ADD COLUMN IF NOT EXISTS "deletedAt" TIMESTAMP(3);
ALTER TABLE "expenses" ADD COLUMN IF NOT EXISTS "deletedAt" TIMESTAMP(3);
ALTER TABLE "assets" ADD COLUMN IF NOT EXISTS "deletedAt" TIMESTAMP(3);
ALTER TABLE "company_accounts" ADD COLUMN IF NOT EXISTS "deletedAt" TIMESTAMP(3);
ALTER TABLE "chart_of_accounts" ADD COLUMN IF NOT EXISTS "deletedAt" TIMESTAMP(3);
-- Indexes for deletedAt
CREATE INDEX IF NOT EXISTS "tenants_deletedAt_idx" ON "tenants"("deletedAt");
CREATE INDEX IF NOT EXISTS "users_deletedAt_idx" ON "users"("deletedAt");
CREATE INDEX IF NOT EXISTS "subscriptions_deletedAt_idx" ON "subscriptions"("deletedAt");
CREATE INDEX IF NOT EXISTS "invoices_deletedAt_idx" ON "invoices"("deletedAt");
CREATE INDEX IF NOT EXISTS "payments_deletedAt_idx" ON "payments"("deletedAt");
CREATE INDEX IF NOT EXISTS "remittances_deletedAt_idx" ON "remittances"("deletedAt");
CREATE INDEX IF NOT EXISTS "tickets_deletedAt_idx" ON "tickets"("deletedAt");
CREATE INDEX IF NOT EXISTS "areas_deletedAt_idx" ON "areas"("deletedAt");
CREATE INDEX IF NOT EXISTS "plans_deletedAt_idx" ON "plans"("deletedAt");
CREATE INDEX IF NOT EXISTS "clients_deletedAt_idx" ON "clients"("deletedAt");
CREATE INDEX IF NOT EXISTS "employees_deletedAt_idx" ON "employees"("deletedAt");
CREATE INDEX IF NOT EXISTS "expenses_deletedAt_idx" ON "expenses"("deletedAt");
CREATE INDEX IF NOT EXISTS "assets_deletedAt_idx" ON "assets"("deletedAt");
CREATE INDEX IF NOT EXISTS "company_accounts_deletedAt_idx" ON "company_accounts"("deletedAt");
CREATE INDEX IF NOT EXISTS "chart_of_accounts_deletedAt_idx" ON "chart_of_accounts"("deletedAt");
-- ── RBAC: tenant_roles ──
CREATE TABLE IF NOT EXISTS "tenant_roles" (
"id" TEXT NOT NULL,
"tenantId" TEXT NOT NULL,
"name" TEXT NOT NULL,
"slug" TEXT NOT NULL,
"description" TEXT,
"isSystem" BOOLEAN NOT NULL DEFAULT false,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"deletedAt" TIMESTAMP(3),
CONSTRAINT "tenant_roles_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX IF NOT EXISTS "tenant_roles_tenantId_slug_key" ON "tenant_roles"("tenantId", "slug");
CREATE INDEX IF NOT EXISTS "tenant_roles_tenantId_idx" ON "tenant_roles"("tenantId");
CREATE INDEX IF NOT EXISTS "tenant_roles_deletedAt_idx" ON "tenant_roles"("deletedAt");
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'tenant_roles_tenantId_fkey') THEN
ALTER TABLE "tenant_roles" ADD CONSTRAINT "tenant_roles_tenantId_fkey"
FOREIGN KEY ("tenantId") REFERENCES "tenants"("id") ON DELETE CASCADE ON UPDATE CASCADE;
END IF;
END $$;
-- ── RBAC: role_permissions ──
CREATE TABLE IF NOT EXISTS "role_permissions" (
"id" TEXT NOT NULL,
"tenantRoleId" TEXT NOT NULL,
"module" TEXT NOT NULL,
"canView" BOOLEAN NOT NULL DEFAULT false,
"canCreate" BOOLEAN NOT NULL DEFAULT false,
"canUpdate" BOOLEAN NOT NULL DEFAULT false,
"canArchive" BOOLEAN NOT NULL DEFAULT false,
"canApprove" BOOLEAN NOT NULL DEFAULT false,
"canExport" BOOLEAN NOT NULL DEFAULT false,
CONSTRAINT "role_permissions_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX IF NOT EXISTS "role_permissions_tenantRoleId_module_key" ON "role_permissions"("tenantRoleId", "module");
CREATE INDEX IF NOT EXISTS "role_permissions_tenantRoleId_idx" ON "role_permissions"("tenantRoleId");
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'role_permissions_tenantRoleId_fkey') THEN
ALTER TABLE "role_permissions" ADD CONSTRAINT "role_permissions_tenantRoleId_fkey"
FOREIGN KEY ("tenantRoleId") REFERENCES "tenant_roles"("id") ON DELETE CASCADE ON UPDATE CASCADE;
END IF;
END $$;
-- ── RBAC: user_tenant_roles ──
CREATE TABLE IF NOT EXISTS "user_tenant_roles" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"tenantRoleId" TEXT NOT NULL,
CONSTRAINT "user_tenant_roles_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX IF NOT EXISTS "user_tenant_roles_userId_tenantRoleId_key" ON "user_tenant_roles"("userId", "tenantRoleId");
CREATE INDEX IF NOT EXISTS "user_tenant_roles_userId_idx" ON "user_tenant_roles"("userId");
CREATE INDEX IF NOT EXISTS "user_tenant_roles_tenantRoleId_idx" ON "user_tenant_roles"("tenantRoleId");
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'user_tenant_roles_userId_fkey') THEN
ALTER TABLE "user_tenant_roles" ADD CONSTRAINT "user_tenant_roles_userId_fkey"
FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'user_tenant_roles_tenantRoleId_fkey') THEN
ALTER TABLE "user_tenant_roles" ADD CONSTRAINT "user_tenant_roles_tenantRoleId_fkey"
FOREIGN KEY ("tenantRoleId") REFERENCES "tenant_roles"("id") ON DELETE CASCADE ON UPDATE CASCADE;
END IF;
END $$;
-- ── Recurring Expenses ──
CREATE TABLE IF NOT EXISTS "recurring_expenses" (
"id" TEXT NOT NULL,
"tenantId" TEXT NOT NULL,
"category" TEXT NOT NULL,
"description" TEXT NOT NULL,
"amount" DECIMAL(10,2) NOT NULL,
"frequency" TEXT NOT NULL DEFAULT 'monthly',
"isActive" BOOLEAN NOT NULL DEFAULT true,
"nextRunDate" TIMESTAMP(3) NOT NULL,
"lastRunDate" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"deletedAt" TIMESTAMP(3),
CONSTRAINT "recurring_expenses_pkey" PRIMARY KEY ("id")
);
CREATE INDEX IF NOT EXISTS "recurring_expenses_tenantId_idx" ON "recurring_expenses"("tenantId");
CREATE INDEX IF NOT EXISTS "recurring_expenses_deletedAt_idx" ON "recurring_expenses"("deletedAt");
-- ── Payroll Runs ──
CREATE TABLE IF NOT EXISTS "payroll_runs" (
"id" TEXT NOT NULL,
"tenantId" TEXT NOT NULL,
"period" TEXT NOT NULL,
"status" TEXT NOT NULL DEFAULT 'draft',
"totalAmount" DECIMAL(12,2) NOT NULL DEFAULT 0,
"processedBy" TEXT,
"processedAt" TIMESTAMP(3),
"notes" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"deletedAt" TIMESTAMP(3),
CONSTRAINT "payroll_runs_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX IF NOT EXISTS "payroll_runs_tenantId_period_key" ON "payroll_runs"("tenantId", "period");
CREATE INDEX IF NOT EXISTS "payroll_runs_tenantId_idx" ON "payroll_runs"("tenantId");
CREATE INDEX IF NOT EXISTS "payroll_runs_deletedAt_idx" ON "payroll_runs"("deletedAt");
-- ── Payslips ──
CREATE TABLE IF NOT EXISTS "payslips" (
"id" TEXT NOT NULL,
"payrollRunId" TEXT NOT NULL,
"employeeId" TEXT NOT NULL,
"baseSalary" DECIMAL(10,2) NOT NULL,
"deductions" DECIMAL(10,2) NOT NULL DEFAULT 0,
"bonuses" DECIMAL(10,2) NOT NULL DEFAULT 0,
"netPay" DECIMAL(10,2) NOT NULL,
"status" TEXT NOT NULL DEFAULT 'pending',
"notes" TEXT,
CONSTRAINT "payslips_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX IF NOT EXISTS "payslips_payrollRunId_employeeId_key" ON "payslips"("payrollRunId", "employeeId");
CREATE INDEX IF NOT EXISTS "payslips_payrollRunId_idx" ON "payslips"("payrollRunId");
CREATE INDEX IF NOT EXISTS "payslips_employeeId_idx" ON "payslips"("employeeId");
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'payslips_payrollRunId_fkey') THEN
ALTER TABLE "payslips" ADD CONSTRAINT "payslips_payrollRunId_fkey"
FOREIGN KEY ("payrollRunId") REFERENCES "payroll_runs"("id") ON DELETE CASCADE ON UPDATE CASCADE;
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'payslips_employeeId_fkey') THEN
ALTER TABLE "payslips" ADD CONSTRAINT "payslips_employeeId_fkey"
FOREIGN KEY ("employeeId") REFERENCES "employees"("id") ON UPDATE CASCADE;
END IF;
END $$;

View File

@@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "clients" ADD COLUMN "latitude" DOUBLE PRECISION;
ALTER TABLE "clients" ADD COLUMN "longitude" DOUBLE PRECISION;

View File

@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "postgresql"

View File

@@ -0,0 +1,682 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
// ─── Multi-Tenant Foundation ────────────────────────────────────────
model Tenant {
id String @id @default(uuid())
name String
slug String @unique
isActive Boolean @default(true)
settings Json @default("{}")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
users User[]
tenantRoles TenantRole[]
areas Area[]
plans Plan[]
clients Client[]
subscriptions Subscription[]
invoices Invoice[]
payments Payment[]
tickets Ticket[]
notifications Notification[]
@@index([deletedAt])
@@map("tenants")
}
// ─── Auth & Users ───────────────────────────────────────────────────
model User {
id String @id @default(uuid())
tenantId String? // null for super_admin (platform-level user)
email String
password String
firstName String
lastName String
isActive Boolean @default(true)
mustChangePassword Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
tenant Tenant? @relation(fields: [tenantId], references: [id], onDelete: Cascade)
roles UserRole[]
tenantRoles UserTenantRole[]
assignedTickets Ticket[] @relation("TicketAssignee")
createdTickets Ticket[] @relation("TicketCreator")
payments Payment[]
remittances Remittance[] @relation("RemittanceCollector")
confirmedRemittances Remittance[] @relation("RemittanceConfirmer")
@@unique([tenantId, email])
@@index([tenantId])
@@index([deletedAt])
@@map("users")
}
model UserRole {
id String @id @default(uuid())
userId String
role String // super_admin (platform-level only, legacy compat)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([userId, role])
@@map("user_roles")
}
// ─── Tenant-Scoped Role Management ─────────────────────────────────
model TenantRole {
id String @id @default(uuid())
tenantId String
name String
slug String
description String?
isSystem Boolean @default(false) // system roles can't be deleted
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
permissions RolePermission[]
users UserTenantRole[]
@@unique([tenantId, slug])
@@index([tenantId])
@@index([deletedAt])
@@map("tenant_roles")
}
model RolePermission {
id String @id @default(uuid())
tenantRoleId String
module String // clients, subscriptions, invoices, payments, tickets, employees, expenses, assets, payroll, accounts, accounting, reports, settings, users, areas, plans, dashboard, fund_transfers
canView Boolean @default(false)
canCreate Boolean @default(false)
canUpdate Boolean @default(false)
canArchive Boolean @default(false)
canApprove Boolean @default(false)
canExport Boolean @default(false)
tenantRole TenantRole @relation(fields: [tenantRoleId], references: [id], onDelete: Cascade)
@@unique([tenantRoleId, module])
@@index([tenantRoleId])
@@map("role_permissions")
}
model UserTenantRole {
id String @id @default(uuid())
userId String
tenantRoleId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
tenantRole TenantRole @relation(fields: [tenantRoleId], references: [id], onDelete: Cascade)
@@unique([userId, tenantRoleId])
@@index([userId])
@@index([tenantRoleId])
@@map("user_tenant_roles")
}
model RefreshToken {
id String @id @default(uuid())
token String @unique
userId String
expiresAt DateTime
createdAt DateTime @default(now())
@@index([userId])
@@index([expiresAt])
@@map("refresh_tokens")
}
// ─── Area & Zone Management ─────────────────────────────────────────
model Area {
id String @id @default(uuid())
tenantId String
name String
description String?
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
clients Client[]
@@unique([tenantId, name])
@@index([tenantId])
@@index([deletedAt])
@@map("areas")
}
// ─── Plan / Package Management ──────────────────────────────────────
model Plan {
id String @id @default(uuid())
tenantId String
name String
description String?
speedDown Int // Mbps download
speedUp Int // Mbps upload
price Decimal @db.Decimal(10, 2)
billingCycle Int @default(30) // days
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
subscriptions Subscription[]
@@unique([tenantId, name])
@@index([tenantId])
@@index([deletedAt])
@@map("plans")
}
// ─── Client Profiling ───────────────────────────────────────────────
model Client {
id String @id @default(uuid())
tenantId String
accountNumber String
firstName String
lastName String
email String?
phone String?
address String
areaId String?
latitude Float?
longitude Float?
status String @default("active") // active, inactive, suspended
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
area Area? @relation(fields: [areaId], references: [id])
subscriptions Subscription[]
invoices Invoice[]
payments Payment[]
tickets Ticket[]
@@unique([tenantId, accountNumber])
@@index([tenantId])
@@index([tenantId, status])
@@index([deletedAt])
@@map("clients")
}
// ─── Subscription Management ────────────────────────────────────────
model Subscription {
id String @id @default(uuid())
tenantId String
clientId String
planId String
type String // prepaid, postpaid
status String @default("pending") // pending, active, suspended, cancelled, expired
startDate DateTime?
endDate DateTime?
installedAt DateTime?
activatedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
client Client @relation(fields: [clientId], references: [id], onDelete: Cascade)
plan Plan @relation(fields: [planId], references: [id])
@@index([tenantId])
@@index([tenantId, status])
@@index([clientId])
@@index([deletedAt])
@@map("subscriptions")
}
// ─── Billing & Invoicing ────────────────────────────────────────────
model Invoice {
id String @id @default(uuid())
tenantId String
clientId String
number String
amount Decimal @db.Decimal(10, 2)
balance Decimal @db.Decimal(10, 2) // remaining unpaid
status String @default("draft") // draft, sent, partial, paid, overdue, void
dueDate DateTime
paidAt DateTime?
periodStart DateTime?
periodEnd DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
client Client @relation(fields: [clientId], references: [id], onDelete: Cascade)
payments Payment[]
@@unique([tenantId, number])
@@index([tenantId])
@@index([tenantId, status])
@@index([clientId])
@@index([deletedAt])
@@map("invoices")
}
// ─── Payment & Collection ───────────────────────────────────────────
model Payment {
id String @id @default(uuid())
tenantId String
clientId String
invoiceId String?
collectedById String?
amount Decimal @db.Decimal(10, 2)
method String // gcash, maya, cash, bank_transfer
referenceNo String?
notes String?
createdAt DateTime @default(now())
deletedAt DateTime?
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
client Client @relation(fields: [clientId], references: [id], onDelete: Cascade)
invoice Invoice? @relation(fields: [invoiceId], references: [id])
collectedBy User? @relation(fields: [collectedById], references: [id])
@@index([tenantId])
@@index([clientId])
@@index([invoiceId])
@@index([deletedAt])
@@map("payments")
}
model Remittance {
id String @id @default(uuid())
tenantId String
collectorId String
confirmedById String?
totalAmount Decimal @db.Decimal(10, 2)
status String @default("pending") // pending, confirmed, rejected
submittedAt DateTime @default(now())
confirmedAt DateTime?
notes String?
deletedAt DateTime?
collector User @relation("RemittanceCollector", fields: [collectorId], references: [id])
confirmedBy User? @relation("RemittanceConfirmer", fields: [confirmedById], references: [id])
payments RemittancePayment[]
@@index([tenantId])
@@index([collectorId])
@@index([deletedAt])
@@map("remittances")
}
model RemittancePayment {
id String @id @default(uuid())
remittanceId String
paymentId String
remittance Remittance @relation(fields: [remittanceId], references: [id], onDelete: Cascade)
@@unique([remittanceId, paymentId])
@@map("remittance_payments")
}
// ─── Tickets (Work Orders) ──────────────────────────────────────────
model Ticket {
id String @id @default(uuid())
tenantId String
clientId String?
createdById String
assigneeId String?
type String // installation, activation, support, maintenance
status String @default("open") // open, in_progress, resolved, cancelled
title String
description String?
priority String @default("normal") // low, normal, high, urgent
resolvedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
client Client? @relation(fields: [clientId], references: [id])
createdBy User @relation("TicketCreator", fields: [createdById], references: [id])
assignee User? @relation("TicketAssignee", fields: [assigneeId], references: [id])
@@index([tenantId])
@@index([tenantId, type, status])
@@index([clientId])
@@index([assigneeId])
@@index([deletedAt])
@@map("tickets")
}
// ─── Notifications ──────────────────────────────────────────────────
model Notification {
id String @id @default(uuid())
tenantId String
userId String?
clientId String?
type String // sms, in_app
channel String // billing_reminder, payment_confirmation, ticket_update
title String
message String
isRead Boolean @default(false)
sentAt DateTime?
createdAt DateTime @default(now())
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
@@index([tenantId])
@@index([userId, isRead])
@@map("notifications")
}
// ─── Audit Log (append-only, no soft delete) ────────────────────────
model AuditLog {
id String @id @default(uuid())
tenantId String
userId String
action String
entity String
entityId String
details Json @default("{}")
ipAddress String?
createdAt DateTime @default(now())
@@index([tenantId])
@@index([tenantId, entity])
@@index([userId])
@@map("audit_logs")
}
// ─── Employee Management ────────────────────────────────────────────
model Employee {
id String @id @default(uuid())
tenantId String
userId String? @unique // Linked user account (optional, 1:1)
firstName String
lastName String
email String?
phone String?
position String
department String?
employeeNo String
status String @default("active") // active, on_leave, terminated
hireDate DateTime @default(now())
terminatedAt DateTime?
salary Decimal? @db.Decimal(10, 2)
notes String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
assets Asset[] @relation("AssetAssignee")
payslips Payslip[]
@@unique([tenantId, employeeNo])
@@index([tenantId])
@@index([tenantId, status])
@@index([deletedAt])
@@map("employees")
}
// ─── Payroll ────────────────────────────────────────────────────────
model PayrollRun {
id String @id @default(uuid())
tenantId String
period String
status String @default("draft") // draft, processing, completed
totalAmount Decimal @default(0) @db.Decimal(12, 2)
processedBy String?
processedAt DateTime?
notes String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
payslips Payslip[]
@@unique([tenantId, period])
@@index([tenantId])
@@index([deletedAt])
@@map("payroll_runs")
}
model Payslip {
id String @id @default(uuid())
payrollRunId String
employeeId String
baseSalary Decimal @db.Decimal(10, 2)
deductions Decimal @default(0) @db.Decimal(10, 2)
bonuses Decimal @default(0) @db.Decimal(10, 2)
netPay Decimal @db.Decimal(10, 2)
status String @default("pending") // pending, paid
notes String?
payrollRun PayrollRun @relation(fields: [payrollRunId], references: [id], onDelete: Cascade)
employee Employee @relation(fields: [employeeId], references: [id])
@@unique([payrollRunId, employeeId])
@@index([payrollRunId])
@@index([employeeId])
@@map("payslips")
}
// ─── Recurring Expenses ─────────────────────────────────────────────
model RecurringExpense {
id String @id @default(uuid())
tenantId String
category String
description String
amount Decimal @db.Decimal(10, 2)
frequency String @default("monthly") // monthly, quarterly, yearly
isActive Boolean @default(true)
nextRunDate DateTime
lastRunDate DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
@@index([tenantId])
@@index([deletedAt])
@@map("recurring_expenses")
}
// ─── Expense Management ─────────────────────────────────────────────
model Expense {
id String @id @default(uuid())
tenantId String
createdById String
approvedById String?
category String
description String
amount Decimal @db.Decimal(10, 2)
receiptUrl String?
status String @default("pending") // pending, approved, rejected
expenseDate DateTime @default(now())
approvedAt DateTime?
notes String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
@@index([tenantId])
@@index([tenantId, status])
@@index([tenantId, category])
@@index([deletedAt])
@@map("expenses")
}
// ─── Company Accounts & Fund Transfers ──────────────────────────────
model CompanyAccount {
id String @id @default(uuid())
tenantId String
name String
type String // bank, e_wallet, cash
accountNo String?
balance Decimal @default(0) @db.Decimal(12, 2)
isActive Boolean @default(true)
isSystem Boolean @default(false)
chartOfAccountId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
outgoing FundTransfer[] @relation("TransferFrom")
incoming FundTransfer[] @relation("TransferTo")
@@unique([tenantId, name])
@@index([tenantId])
@@index([deletedAt])
@@map("company_accounts")
}
model FundTransfer {
id String @id @default(uuid())
tenantId String
fromAccountId String
toAccountId String
amount Decimal @db.Decimal(12, 2)
description String?
transferredBy String
createdAt DateTime @default(now())
fromAccount CompanyAccount @relation("TransferFrom", fields: [fromAccountId], references: [id])
toAccount CompanyAccount @relation("TransferTo", fields: [toAccountId], references: [id])
@@index([tenantId])
@@map("fund_transfers")
}
// ─── Asset Management ───────────────────────────────────────────────
model Asset {
id String @id @default(uuid())
tenantId String
name String
category String // router, olt, cable, tool, vehicle, computer, other
serialNumber String?
purchaseDate DateTime?
purchasePrice Decimal? @db.Decimal(10, 2)
assignedToId String?
status String @default("available") // available, in_use, maintenance, retired
location String?
notes String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
assignedTo Employee? @relation("AssetAssignee", fields: [assignedToId], references: [id])
@@index([tenantId])
@@index([tenantId, status])
@@index([tenantId, category])
@@index([deletedAt])
@@map("assets")
}
// ─── Billing Settings ───────────────────────────────────────────────
model BillingSetting {
id String @id @default(uuid())
tenantId String @unique
autoGenerate Boolean @default(true)
gracePeriodDays Int @default(7)
dueDateOffsetDays Int @default(15)
lateFeePercent Decimal @default(0) @db.Decimal(5, 2)
invoicePrefix String @default("INV")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("billing_settings")
}
// ─── Chart of Accounts ──────────────────────────────────────────────
model ChartOfAccount {
id String @id @default(uuid())
tenantId String
code String
name String
type String // asset, liability, equity, revenue, expense
parentId String?
isActive Boolean @default(true)
isSystem Boolean @default(false)
balance Decimal @default(0) @db.Decimal(14, 2)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime?
parent ChartOfAccount? @relation("AccountTree", fields: [parentId], references: [id])
children ChartOfAccount[] @relation("AccountTree")
journalLines JournalLine[]
@@unique([tenantId, code])
@@index([tenantId])
@@index([tenantId, type])
@@index([deletedAt])
@@map("chart_of_accounts")
}
// ─── Journal Entries (append-only, no soft delete) ──────────────────
model JournalEntry {
id String @id @default(uuid())
tenantId String
entryDate DateTime @default(now())
description String
reference String?
sourceType String?
sourceId String?
createdById String?
createdAt DateTime @default(now())
lines JournalLine[]
@@index([tenantId])
@@index([tenantId, sourceType, sourceId])
@@map("journal_entries")
}
model JournalLine {
id String @id @default(uuid())
journalEntryId String
accountId String
debit Decimal @default(0) @db.Decimal(14, 2)
credit Decimal @default(0) @db.Decimal(14, 2)
journalEntry JournalEntry @relation(fields: [journalEntryId], references: [id], onDelete: Cascade)
account ChartOfAccount @relation(fields: [accountId], references: [id])
@@index([journalEntryId])
@@index([accountId])
@@map("journal_lines")
}

603
packages/db/prisma/seed.ts Normal file
View File

@@ -0,0 +1,603 @@
import { PrismaClient } from '@prisma/client';
import { randomUUID } from 'crypto';
import * as bcrypt from 'bcrypt';
// Default permission matrices for system roles
const MODULES = [
'dashboard', 'clients', 'subscriptions', 'invoices', 'payments', 'tickets',
'employees', 'payroll', 'expenses', 'assets', 'accounts', 'fund_transfers',
'accounting', 'reports', 'areas', 'plans', 'settings', 'users',
] as const;
type Module = (typeof MODULES)[number];
interface PermRow { module: Module; canView: boolean; canCreate: boolean; canUpdate: boolean; canArchive: boolean; canApprove: boolean; canExport: boolean; }
const DEFAULT_ROLES: { name: string; slug: string; description: string; perms: PermRow[] }[] = [
{
name: 'Tenant Admin', slug: 'tenant_admin', description: 'Full access to all modules',
perms: MODULES.map((m) => ({ module: m, canView: true, canCreate: true, canUpdate: true, canArchive: true, canApprove: true, canExport: true })),
},
{
name: 'Manager', slug: 'manager', description: 'Operational management with approval rights',
perms: MODULES.map((m) => {
const noAccess: Module[] = ['users'];
const viewOnly: Module[] = ['dashboard', 'accounting', 'settings'];
if (noAccess.includes(m)) return { module: m, canView: false, canCreate: false, canUpdate: false, canArchive: false, canApprove: false, canExport: false };
if (viewOnly.includes(m)) return { module: m, canView: true, canCreate: false, canUpdate: false, canArchive: false, canApprove: false, canExport: m === 'accounting' };
return { module: m, canView: true, canCreate: true, canUpdate: true, canArchive: true, canApprove: ['invoices', 'payments', 'expenses', 'payroll', 'fund_transfers'].includes(m), canExport: true };
}),
},
{
name: 'Technician', slug: 'technician', description: 'Field operations: tickets, payments, client/invoice viewing',
perms: MODULES.map((m) => {
const viewOnly: Module[] = ['clients', 'subscriptions', 'invoices', 'dashboard'];
const fullAccess: Module[] = ['tickets', 'payments'];
if (viewOnly.includes(m)) return { module: m, canView: true, canCreate: false, canUpdate: false, canArchive: false, canApprove: false, canExport: false };
if (fullAccess.includes(m)) return { module: m, canView: true, canCreate: true, canUpdate: true, canArchive: false, canApprove: false, canExport: false };
return { module: m, canView: false, canCreate: false, canUpdate: false, canArchive: false, canApprove: false, canExport: false };
}),
},
{
name: 'Collector', slug: 'collector', description: 'Payment collection and client viewing',
perms: MODULES.map((m) => {
const canWrite: Module[] = ['payments'];
const canViewMods: Module[] = ['dashboard', 'clients', 'invoices', 'payments'];
if (!canViewMods.includes(m)) return { module: m, canView: false, canCreate: false, canUpdate: false, canArchive: false, canApprove: false, canExport: false };
return { module: m, canView: true, canCreate: canWrite.includes(m), canUpdate: false, canArchive: false, canApprove: false, canExport: false };
}),
},
];
const prisma = new PrismaClient();
async function hashPassword(password: string): Promise<string> {
return bcrypt.hash(password, 12);
}
async function main() {
console.log('Seeding database...');
// Clean existing data (order matters for FK constraints)
await prisma.journalLine.deleteMany();
await prisma.journalEntry.deleteMany();
await prisma.chartOfAccount.deleteMany();
await prisma.billingSetting.deleteMany();
await prisma.fundTransfer.deleteMany();
await prisma.companyAccount.deleteMany();
await prisma.asset.deleteMany();
await prisma.expense.deleteMany();
await prisma.payslip.deleteMany();
await prisma.payrollRun.deleteMany();
await prisma.recurringExpense.deleteMany();
await prisma.employee.deleteMany();
await prisma.notification.deleteMany();
await prisma.remittancePayment.deleteMany();
await prisma.remittance.deleteMany();
await prisma.auditLog.deleteMany();
await prisma.payment.deleteMany();
await prisma.invoice.deleteMany();
await prisma.ticket.deleteMany();
await prisma.subscription.deleteMany();
await prisma.client.deleteMany();
await prisma.plan.deleteMany();
await prisma.area.deleteMany();
await prisma.refreshToken.deleteMany();
await prisma.userTenantRole.deleteMany();
await prisma.rolePermission.deleteMany();
await prisma.tenantRole.deleteMany();
await prisma.userRole.deleteMany();
await prisma.user.deleteMany();
await prisma.tenant.deleteMany();
// ─── Tenant ────────────────────────────────────────────
const tenant = await prisma.tenant.create({
data: {
id: randomUUID(),
name: 'FiberNet Philippines',
slug: 'fibernet-ph',
settings: { companyName: 'FiberNet Philippines Inc.', currency: 'PHP', timezone: 'Asia/Manila' },
},
});
console.log(`Tenant: ${tenant.name}`);
// ─── Super Admin (platform-level, no tenant) ───────────
const superAdmin = await prisma.user.create({
data: { tenantId: null, email: 'superadmin@fiberops.dev', password: await hashPassword('admin123!'), firstName: 'Super', lastName: 'Admin' },
});
await prisma.userRole.create({ data: { userId: superAdmin.id, role: 'super_admin' } });
console.log(`Super Admin: superadmin@fiberops.dev (super_admin)`);
// ─── Tenant Users ─────────────────────────────────────
const users: Record<string, any> = {};
const usersByEmail: Record<string, any> = {};
const userDefs = [
{ email: 'admin@demo-isp.com', first: 'Admin', last: 'User', role: 'tenant_admin' },
{ email: 'manager@demo-isp.com', first: 'Maria', last: 'Reyes', role: 'manager' },
{ email: 'collector@demo-isp.com', first: 'Juan', last: 'Santos', role: 'technician' },
{ email: 'tech@demo-isp.com', first: 'Pedro', last: 'Cruz', role: 'technician' },
{ email: 'tech2@demo-isp.com', first: 'Jose', last: 'Garcia', role: 'technician' },
{ email: 'viewer@demo-isp.com', first: 'Ana', last: 'Lopez', role: 'viewer' },
];
for (const u of userDefs) {
const user = await prisma.user.create({
data: { tenantId: tenant.id, email: u.email, password: await hashPassword('admin123!'), firstName: u.first, lastName: u.last },
});
await prisma.userRole.create({ data: { userId: user.id, role: u.role } });
users[u.role] = user;
usersByEmail[u.email] = user;
console.log(`User: ${u.email} (${u.role})`);
}
// ─── Default Tenant Roles ──────────────────────────────
const tenantRoles: Record<string, any> = {};
for (const def of DEFAULT_ROLES) {
const role = await prisma.tenantRole.create({
data: {
tenantId: tenant.id,
name: def.name,
slug: def.slug,
description: def.description,
isSystem: true,
permissions: {
create: def.perms.map((p) => ({
module: p.module,
canView: p.canView,
canCreate: p.canCreate,
canUpdate: p.canUpdate,
canArchive: p.canArchive,
canApprove: p.canApprove,
canExport: p.canExport,
})),
},
},
});
tenantRoles[def.slug] = role;
}
console.log(`Tenant Roles: ${DEFAULT_ROLES.length} (${DEFAULT_ROLES.map((r) => r.slug).join(', ')})`);
// ─── Assign Tenant Roles to Users ─────────────────────
// Map of old role names to new tenant role slugs
const userRoleMap: Record<string, string> = {
tenant_admin: 'tenant_admin',
manager: 'manager',
technician: 'technician',
viewer: 'collector', // viewer user gets collector role for demo
};
// Assign roles from the map
for (const [oldRole, newRoleSlug] of Object.entries(userRoleMap)) {
if (users[oldRole] && tenantRoles[newRoleSlug]) {
await prisma.userTenantRole.create({
data: { userId: users[oldRole].id, tenantRoleId: tenantRoles[newRoleSlug].id },
});
}
}
// Additional assignments:
// - Assign technician users (collector, tech, tech2) to technician role
// - Assign viewer user to collector role
const technicianUsers = ['collector', 'tech', 'tech2'];
for (const email of technicianUsers) {
const userEmail = `${email}@demo-isp.com`;
const user = usersByEmail[userEmail];
if (user) {
const existing = await prisma.userTenantRole.findFirst({ where: { userId: user.id } });
if (!existing) {
await prisma.userTenantRole.create({
data: { userId: user.id, tenantRoleId: tenantRoles['technician'].id },
});
}
}
}
const viewerUserEmail = 'viewer@demo-isp.com';
const viewerUser = usersByEmail[viewerUserEmail];
if (viewerUser) {
const existing = await prisma.userTenantRole.findFirst({ where: { userId: viewerUser.id } });
if (!existing) {
await prisma.userTenantRole.create({
data: { userId: viewerUser.id, tenantRoleId: tenantRoles['collector'].id },
});
}
}
console.log('User-TenantRole assignments complete');
// ─── Areas ─────────────────────────────────────────────
const areas = await Promise.all([
prisma.area.create({ data: { tenantId: tenant.id, name: 'Barangay 1 - Centro', description: 'Town center, commercial area' } }),
prisma.area.create({ data: { tenantId: tenant.id, name: 'Barangay 2 - Poblacion', description: 'Residential zone near market' } }),
prisma.area.create({ data: { tenantId: tenant.id, name: 'Barangay 3 - San Isidro', description: 'Agricultural and residential' } }),
prisma.area.create({ data: { tenantId: tenant.id, name: 'Barangay 4 - Riverside', description: 'River-side residential' } }),
prisma.area.create({ data: { tenantId: tenant.id, name: 'Barangay 5 - Hilltop', description: 'Elevated residential subdivision' } }),
]);
console.log(`Areas: ${areas.length}`);
// ─── Plans ─────────────────────────────────────────────
const plans = await Promise.all([
prisma.plan.create({ data: { tenantId: tenant.id, name: 'Lite 15', description: 'Entry-level 15 Mbps', speedDown: 15, speedUp: 15, price: 699, billingCycle: 30 } }),
prisma.plan.create({ data: { tenantId: tenant.id, name: 'Basic 25', description: '25 Mbps residential', speedDown: 25, speedUp: 25, price: 999, billingCycle: 30 } }),
prisma.plan.create({ data: { tenantId: tenant.id, name: 'Standard 50', description: '50 Mbps residential', speedDown: 50, speedUp: 50, price: 1499, billingCycle: 30 } }),
prisma.plan.create({ data: { tenantId: tenant.id, name: 'Premium 100', description: '100 Mbps business', speedDown: 100, speedUp: 100, price: 2499, billingCycle: 30 } }),
prisma.plan.create({ data: { tenantId: tenant.id, name: 'Enterprise 200', description: '200 Mbps dedicated', speedDown: 200, speedUp: 200, price: 4999, billingCycle: 30 } }),
]);
console.log(`Plans: ${plans.length}`);
// ─── Clients (20 clients across various areas/plans) ──
// Area center coordinates (Lipa City, Batangas area)
const areaCoords: [number, number][] = [
[14.0785, 121.1760], // Barangay 1 - Centro
[14.0820, 121.1800], // Barangay 2 - Poblacion
[14.0850, 121.1700], // Barangay 3 - San Isidro
[14.0750, 121.1720], // Barangay 4 - Riverside
[14.0900, 121.1780], // Barangay 5 - Hilltop
];
const clientDefs = [
{ first: 'Juan', last: 'Dela Cruz', phone: '09171234567', email: 'juan@email.com', address: '123 Rizal St, Centro', area: 0, plan: 1, type: 'postpaid', latOff: 0.001, lngOff: 0.002 },
{ first: 'Maria', last: 'Santos', phone: '09181234567', email: 'maria@email.com', address: '456 Mabini St, Centro', area: 0, plan: 2, type: 'postpaid', latOff: -0.002, lngOff: 0.001 },
{ first: 'Jose', last: 'Garcia', phone: '09191234567', email: 'jose@email.com', address: '789 Bonifacio St, Poblacion', area: 1, plan: 1, type: 'prepaid', latOff: 0.003, lngOff: -0.001 },
{ first: 'Ana', last: 'Reyes', phone: '09201234567', email: 'ana@email.com', address: '12 Luna St, Poblacion', area: 1, plan: 2, type: 'postpaid', latOff: -0.001, lngOff: 0.003 },
{ first: 'Pedro', last: 'Aquino', phone: '09211234567', email: null, address: '34 Del Pilar St, San Isidro', area: 2, plan: 0, type: 'prepaid', latOff: 0.002, lngOff: -0.002 },
{ first: 'Rosa', last: 'Mendoza', phone: '09221234567', email: 'rosa@email.com', address: '56 Quezon Ave, San Isidro', area: 2, plan: 3, type: 'postpaid', latOff: -0.003, lngOff: 0.001 },
{ first: 'Carlos', last: 'Bautista', phone: '09231234567', email: null, address: '78 Magsaysay Blvd, Riverside', area: 3, plan: 1, type: 'postpaid', latOff: 0.001, lngOff: 0.002 },
{ first: 'Elena', last: 'Villanueva', phone: '09241234567', email: 'elena@email.com', address: '90 Roxas St, Riverside', area: 3, plan: 2, type: 'postpaid', latOff: -0.002, lngOff: -0.003 },
{ first: 'Roberto', last: 'Tan', phone: '09251234567', email: 'roberto@email.com', address: '11 Laurel St, Hilltop', area: 4, plan: 4, type: 'postpaid', latOff: 0.002, lngOff: 0.001 },
{ first: 'Carmen', last: 'Lim', phone: '09261234567', email: 'carmen@email.com', address: '22 Osmena Ave, Hilltop', area: 4, plan: 3, type: 'postpaid', latOff: -0.001, lngOff: 0.002 },
{ first: 'Miguel', last: 'Ramos', phone: '09271234567', email: null, address: '33 Aguinaldo St, Centro', area: 0, plan: 1, type: 'prepaid', latOff: 0.003, lngOff: -0.001 },
{ first: 'Isabel', last: 'Torres', phone: '09281234567', email: 'isabel@email.com', address: '44 Andres Blvd, Poblacion', area: 1, plan: 0, type: 'postpaid', latOff: -0.002, lngOff: 0.003 },
{ first: 'Ricardo', last: 'Flores', phone: '09291234567', email: null, address: '55 Katipunan Rd, San Isidro', area: 2, plan: 2, type: 'postpaid', latOff: 0.001, lngOff: -0.002 },
{ first: 'Teresa', last: 'Navarro', phone: '09301234567', email: 'teresa@email.com', address: '66 Makabayan St, Riverside', area: 3, plan: 1, type: 'prepaid', latOff: -0.003, lngOff: 0.001 },
{ first: 'Fernando', last: 'Castillo', phone: '09311234567', email: null, address: '77 Silang Blvd, Hilltop', area: 4, plan: 2, type: 'postpaid', latOff: 0.002, lngOff: -0.001 },
];
const clients: any[] = [];
let invoiceCount = 0;
const now = new Date();
for (let i = 0; i < clientDefs.length; i++) {
const c = clientDefs[i];
const accountNumber = `C-${String(i + 1).padStart(6, '0')}`;
const plan = plans[c.plan];
const client = await prisma.client.create({
data: {
tenantId: tenant.id,
accountNumber,
firstName: c.first,
lastName: c.last,
phone: c.phone,
email: c.email,
address: c.address,
areaId: areas[c.area].id,
latitude: areaCoords[c.area][0] + c.latOff,
longitude: areaCoords[c.area][1] + c.lngOff,
},
});
clients.push(client);
// Create subscription
const installedAt = new Date(now);
installedAt.setDate(installedAt.getDate() - (30 + Math.floor(Math.random() * 60))); // 30-90 days ago
const activatedAt = new Date(installedAt);
activatedAt.setDate(activatedAt.getDate() + 2);
await prisma.subscription.create({
data: {
tenantId: tenant.id,
clientId: client.id,
planId: plan.id,
type: c.type,
status: 'active',
installedAt,
activatedAt,
startDate: activatedAt,
},
});
// Create resolved installation + activation tickets
await prisma.ticket.create({
data: {
tenantId: tenant.id,
clientId: client.id,
createdById: users.tenant_admin.id,
assigneeId: users.technician.id,
type: 'installation',
title: `Installation for ${c.first} ${c.last}`,
description: `Installation at ${c.address}`,
status: 'resolved',
priority: 'high',
resolvedAt: new Date(installedAt.getTime() + 86400000),
},
});
await prisma.ticket.create({
data: {
tenantId: tenant.id,
clientId: client.id,
createdById: users.tenant_admin.id,
assigneeId: users.technician.id,
type: 'activation',
title: `Activation for ${c.first} ${c.last}`,
status: 'resolved',
priority: 'high',
resolvedAt: activatedAt,
},
});
// Create 2 invoices per client
for (let m = 0; m < 2; m++) {
invoiceCount++;
const periodStart = new Date(activatedAt);
periodStart.setMonth(periodStart.getMonth() + m);
const periodEnd = new Date(periodStart);
periodEnd.setDate(periodEnd.getDate() + 30);
const dueDate = new Date(periodStart);
dueDate.setDate(dueDate.getDate() + 15);
const isPaid = m === 0 || Math.random() > 0.4; // First invoice always paid, second 60% chance
const balance = isPaid ? 0 : Number(plan.price);
const invoice = await prisma.invoice.create({
data: {
tenantId: tenant.id,
clientId: client.id,
number: `INV-${String(invoiceCount).padStart(6, '0')}`,
amount: plan.price,
balance,
status: isPaid ? 'paid' : (dueDate < now ? 'overdue' : 'sent'),
dueDate,
paidAt: isPaid ? new Date(dueDate.getTime() - 86400000 * 3) : null,
periodStart,
periodEnd,
},
});
// Create payment for paid invoices
if (isPaid) {
const methods = ['gcash', 'maya', 'cash', 'bank_transfer'];
const method = methods[Math.floor(Math.random() * methods.length)];
const paidDate = new Date(dueDate.getTime() - 86400000 * Math.floor(Math.random() * 5));
await prisma.payment.create({
data: {
tenantId: tenant.id,
clientId: client.id,
invoiceId: invoice.id,
collectedById: users.technician.id,
amount: plan.price,
method,
referenceNo: method !== 'cash' ? `REF-${String(Math.floor(Math.random() * 99999)).padStart(5, '0')}` : null,
createdAt: paidDate,
},
});
}
}
}
console.log(`Clients: ${clients.length} (with subscriptions, tickets, invoices, payments)`);
// ─── Open support tickets ──────────────────────────────
const supportTickets = [
{ clientIdx: 2, title: 'Intermittent connection drops', desc: 'Internet keeps disconnecting every 30 minutes', priority: 'high' },
{ clientIdx: 5, title: 'Slow speed during peak hours', desc: 'Speed drops to 5 Mbps from 8-10 PM', priority: 'normal' },
{ clientIdx: 8, title: 'No internet connection', desc: 'Complete outage since this morning', priority: 'urgent' },
{ clientIdx: 11, title: 'Request for plan upgrade', desc: 'Would like to upgrade from Basic to Standard', priority: 'low' },
{ clientIdx: 1, title: 'WiFi router not working', desc: 'Power light blinking, no WiFi signal', priority: 'high' },
];
for (const t of supportTickets) {
await prisma.ticket.create({
data: {
tenantId: tenant.id,
clientId: clients[t.clientIdx].id,
createdById: users.tenant_admin.id,
type: 'support',
title: t.title,
description: t.desc,
priority: t.priority,
status: 'open',
},
});
}
console.log(`Support tickets: ${supportTickets.length}`);
// ─── Employees ─────────────────────────────────────────
const empDefs = [
{ first: 'Pedro', last: 'Cruz', position: 'Senior Technician', dept: 'Operations', salary: 18000 },
{ first: 'Jose', last: 'Garcia', position: 'Field Technician', dept: 'Operations', salary: 15000 },
{ first: 'Maria', last: 'Reyes', position: 'Operations Manager', dept: 'Management', salary: 30000 },
{ first: 'Juan', last: 'Santos', position: 'Collection Officer', dept: 'Finance', salary: 16000 },
{ first: 'Ana', last: 'De Leon', position: 'Billing Clerk', dept: 'Finance', salary: 14000 },
{ first: 'Luis', last: 'Mercado', position: 'Network Engineer', dept: 'Technical', salary: 25000 },
{ first: 'Sofia', last: 'Pascual', position: 'Customer Service', dept: 'Support', salary: 14000 },
];
const employees: any[] = [];
for (let i = 0; i < empDefs.length; i++) {
const e = empDefs[i];
const emp = await prisma.employee.create({
data: { tenantId: tenant.id, employeeNo: `E-${String(i + 1).padStart(4, '0')}`, firstName: e.first, lastName: e.last, position: e.position, department: e.dept, salary: e.salary },
});
employees.push(emp);
}
console.log(`Employees: ${employees.length}`);
// ─── Expenses ──────────────────────────────────────────
const expDefs = [
{ cat: 'utilities', desc: 'Electricity bill - March 2026', amount: 12500, status: 'approved', days: -15 },
{ cat: 'utilities', desc: 'Internet backbone ISP bill', amount: 35000, status: 'approved', days: -10 },
{ cat: 'supplies', desc: 'Fiber optic cables (500m)', amount: 8500, status: 'approved', days: -8 },
{ cat: 'maintenance', desc: 'OLT maintenance and cleaning', amount: 3500, status: 'approved', days: -5 },
{ cat: 'transport', desc: 'Fuel for service vehicles', amount: 4200, status: 'approved', days: -3 },
{ cat: 'equipment', desc: '10x Mikrotik hEX S routers', amount: 28000, status: 'approved', days: -2 },
{ cat: 'supplies', desc: 'Office supplies and printer ink', amount: 2100, status: 'pending', days: -1 },
{ cat: 'maintenance', desc: 'Generator repair', amount: 7800, status: 'pending', days: 0 },
{ cat: 'transport', desc: 'Technician transport allowance - April', amount: 6000, status: 'pending', days: 0 },
];
for (const e of expDefs) {
const expDate = new Date();
expDate.setDate(expDate.getDate() + e.days);
await prisma.expense.create({
data: {
tenantId: tenant.id,
createdById: users.manager.id,
approvedById: e.status === 'approved' ? users.tenant_admin.id : null,
category: e.cat,
description: e.desc,
amount: e.amount,
status: e.status,
expenseDate: expDate,
approvedAt: e.status === 'approved' ? expDate : null,
},
});
}
console.log(`Expenses: ${expDefs.length}`);
// ─── Company Accounts ──────────────────────────────────
// Company accounts will be created after CoA so we can link them
console.log('Company accounts: deferred to after CoA');
// ─── Fund Transfers ────────────────────────────────────
// Fund transfers deferred to after company accounts
console.log('Fund transfers: deferred');
// ─── Assets ────────────────────────────────────────────
const assetDefs = [
{ name: 'Huawei MA5608T OLT', cat: 'olt', serial: 'HW-OLT-001', price: 85000, status: 'in_use', loc: 'Main Office' },
{ name: 'Mikrotik CCR1009', cat: 'router', serial: 'MK-CCR-001', price: 32000, status: 'in_use', loc: 'Main Office' },
{ name: 'Mikrotik hEX S #1', cat: 'router', serial: 'MK-HEX-001', price: 2800, status: 'in_use', empIdx: 0 },
{ name: 'Mikrotik hEX S #2', cat: 'router', serial: 'MK-HEX-002', price: 2800, status: 'in_use', empIdx: 1 },
{ name: 'Mikrotik hEX S #3', cat: 'router', serial: 'MK-HEX-003', price: 2800, status: 'available', loc: 'Warehouse' },
{ name: 'OTDR Tester', cat: 'tool', serial: 'OTDR-001', price: 45000, status: 'in_use', empIdx: 0 },
{ name: 'Fiber Splicer', cat: 'tool', serial: 'FS-001', price: 65000, status: 'in_use', empIdx: 5 },
{ name: 'Honda XRM 125 (Field)', cat: 'vehicle', serial: 'MV-2024-001', price: 68000, status: 'in_use', empIdx: 0 },
{ name: 'Honda Wave 110 (Field)', cat: 'vehicle', serial: 'MV-2024-002', price: 55000, status: 'in_use', empIdx: 1 },
{ name: 'Dell Latitude 5540', cat: 'computer', serial: 'DELL-LAP-001', price: 48000, status: 'in_use', empIdx: 2 },
{ name: 'Fiber Cable Spool 1km', cat: 'cable', price: 12000, status: 'available', loc: 'Warehouse' },
{ name: 'Fiber Cable Spool 500m', cat: 'cable', price: 6500, status: 'available', loc: 'Warehouse' },
{ name: 'UPS 1500VA', cat: 'other', serial: 'UPS-001', price: 8500, status: 'in_use', loc: 'Main Office' },
{ name: 'Old Mikrotik RB750', cat: 'router', serial: 'MK-OLD-001', price: 1500, status: 'retired' },
];
for (const a of assetDefs) {
await prisma.asset.create({
data: {
tenantId: tenant.id,
name: a.name,
category: a.cat,
serialNumber: a.serial || null,
purchasePrice: a.price,
status: a.status,
location: a.loc || null,
assignedToId: a.empIdx !== undefined ? employees[a.empIdx].id : null,
},
});
}
console.log(`Assets: ${assetDefs.length}`);
// ─── Billing Settings ──────────────────────────────────
await prisma.billingSetting.create({
data: { tenantId: tenant.id, autoGenerate: true, gracePeriodDays: 7, dueDateOffsetDays: 15, invoicePrefix: 'INV' },
});
console.log('Billing settings created');
// ─── Chart of Accounts (auto-seeded by API, but seed defaults) ──
const coaDefs = [
{ code: '1000', name: 'Assets', type: 'asset', sys: true },
{ code: '1010', name: 'Cash on Hand', type: 'asset', sys: true },
{ code: '1020', name: 'GCash Business', type: 'asset', sys: true },
{ code: '1030', name: 'Maya Business', type: 'asset', sys: true },
{ code: '1040', name: 'Bank Account', type: 'asset', sys: true },
{ code: '1100', name: 'Accounts Receivable', type: 'asset', sys: true },
{ code: '1200', name: 'Equipment', type: 'asset', sys: true },
{ code: '2000', name: 'Liabilities', type: 'liability', sys: true },
{ code: '2010', name: 'Accounts Payable', type: 'liability', sys: true },
{ code: '3000', name: 'Equity', type: 'equity', sys: true },
{ code: '3010', name: "Owner's Equity", type: 'equity', sys: true },
{ code: '3020', name: 'Retained Earnings', type: 'equity', sys: true },
{ code: '4000', name: 'Revenue', type: 'revenue', sys: true },
{ code: '4010', name: 'Internet Service Revenue', type: 'revenue', sys: true },
{ code: '4020', name: 'Installation Fees', type: 'revenue', sys: true },
{ code: '5000', name: 'Expenses', type: 'expense', sys: true },
{ code: '5010', name: 'Utilities Expense', type: 'expense', sys: true },
{ code: '5020', name: 'Salaries Expense', type: 'expense', sys: true },
{ code: '5030', name: 'Maintenance Expense', type: 'expense', sys: true },
{ code: '5040', name: 'Transport Expense', type: 'expense', sys: true },
{ code: '5050', name: 'Supplies Expense', type: 'expense', sys: true },
{ code: '5060', name: 'Equipment Expense', type: 'expense', sys: true },
];
for (const a of coaDefs) {
await prisma.chartOfAccount.create({
data: { tenantId: tenant.id, code: a.code, name: a.name, type: a.type, isSystem: a.sys },
});
}
console.log(`Chart of Accounts: ${coaDefs.length}`);
// ─── Custodial CoA per user ────────────────────────────
const methods = ['Cash', 'GCash', 'Maya', 'Bank'];
let custodialCode = 1500;
for (const u of userDefs) {
const user = users[u.role];
for (const m of methods) {
await prisma.chartOfAccount.create({
data: { tenantId: tenant.id, code: String(custodialCode++), name: `${u.first} ${u.last} - ${m}`, type: 'asset', isSystem: false },
});
}
}
console.log(`Custodial CoA accounts: ${userDefs.length * 4}`);
// ─── Company Accounts (linked to CoA) ──────────────────
const coa1010 = await prisma.chartOfAccount.findFirst({ where: { tenantId: tenant.id, code: '1010' } });
const coa1020 = await prisma.chartOfAccount.findFirst({ where: { tenantId: tenant.id, code: '1020' } });
const coa1030 = await prisma.chartOfAccount.findFirst({ where: { tenantId: tenant.id, code: '1030' } });
const coa1040 = await prisma.chartOfAccount.findFirst({ where: { tenantId: tenant.id, code: '1040' } });
const accts = await Promise.all([
prisma.companyAccount.create({ data: { tenantId: tenant.id, name: 'Cash on Hand', type: 'cash', balance: 15000, isSystem: true, chartOfAccountId: coa1010?.id } }),
prisma.companyAccount.create({ data: { tenantId: tenant.id, name: 'GCash Business', type: 'e_wallet', accountNo: '09171234567', balance: 42500, chartOfAccountId: coa1020?.id } }),
prisma.companyAccount.create({ data: { tenantId: tenant.id, name: 'Maya Business', type: 'e_wallet', accountNo: '09181234567', balance: 18200, chartOfAccountId: coa1030?.id } }),
prisma.companyAccount.create({ data: { tenantId: tenant.id, name: 'BDO Savings', type: 'bank', accountNo: '0012-3456-7890', balance: 285000, chartOfAccountId: coa1040?.id } }),
]);
console.log(`Company accounts: ${accts.length} (linked to CoA)`);
// ─── Fund Transfers ────────────────────────────────────
await prisma.fundTransfer.create({
data: { tenantId: tenant.id, fromAccountId: accts[1].id, toAccountId: accts[3].id, amount: 20000, description: 'GCash to BDO weekly transfer', transferredBy: users.tenant_admin.id },
});
await prisma.fundTransfer.create({
data: { tenantId: tenant.id, fromAccountId: accts[3].id, toAccountId: accts[0].id, amount: 5000, description: 'Petty cash replenishment', transferredBy: users.tenant_admin.id },
});
console.log('Fund transfers: 2');
// ─── Remittances ───────────────────────────────────────
await prisma.remittance.create({
data: { tenantId: tenant.id, collectorId: users.technician.id, confirmedById: users.tenant_admin.id, totalAmount: 8995, status: 'confirmed', confirmedAt: new Date() },
});
await prisma.remittance.create({
data: { tenantId: tenant.id, collectorId: users.technician.id, totalAmount: 5497, status: 'pending' },
});
console.log('Remittances: 2');
console.log('\n✅ Seed completed successfully!');
console.log(`\n📊 Summary:`);
console.log(` Tenant: ${tenant.name}`);
console.log(` Users: ${userDefs.length}`);
console.log(` Areas: ${areas.length}`);
console.log(` Plans: ${plans.length}`);
console.log(` Clients: ${clients.length} (with active subscriptions)`);
console.log(` Invoices: ${invoiceCount * 2} (paid + unpaid)`);
console.log(` Employees: ${empDefs.length}`);
console.log(` Expenses: ${expDefs.length} (approved + pending)`);
console.log(` Assets: ${assetDefs.length}`);
console.log(` Company Accounts: ${accts.length}`);
console.log(`\n🔑 Login: admin@demo-isp.com / admin123!`);
console.log(`🌐 Portal: C-000001 / 09171234567`);
}
main()
.catch((e) => { console.error('Seed failed:', e); process.exit(1); })
.finally(async () => { await prisma.$disconnect(); });

19
packages/db/src/index.ts Normal file
View File

@@ -0,0 +1,19 @@
import { PrismaClient } from '@prisma/client';
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient };
export const prisma =
globalForPrisma.prisma ||
new PrismaClient({
log:
process.env.NODE_ENV === 'development'
? ['query', 'error', 'warn']
: ['error'],
});
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = prisma;
}
export { PrismaClient };
export * from '@prisma/client';

View File

@@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src", "prisma"]
}