initial: standalone repo from monorepo split
This commit is contained in:
361
packages/db/prisma/migrations/20260403015002_init/migration.sql
Normal file
361
packages/db/prisma/migrations/20260403015002_init/migration.sql
Normal 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;
|
||||
@@ -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");
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -0,0 +1,3 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "company_accounts" ADD COLUMN "chartOfAccountId" TEXT,
|
||||
ADD COLUMN "isSystem" BOOLEAN NOT NULL DEFAULT false;
|
||||
@@ -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;
|
||||
@@ -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 $$;
|
||||
@@ -0,0 +1,3 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "clients" ADD COLUMN "latitude" DOUBLE PRECISION;
|
||||
ALTER TABLE "clients" ADD COLUMN "longitude" DOUBLE PRECISION;
|
||||
3
packages/db/prisma/migrations/migration_lock.toml
Normal file
3
packages/db/prisma/migrations/migration_lock.toml
Normal 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"
|
||||
682
packages/db/prisma/schema.prisma
Normal file
682
packages/db/prisma/schema.prisma
Normal 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
603
packages/db/prisma/seed.ts
Normal 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(); });
|
||||
Reference in New Issue
Block a user