feat(02-03): Subscriber and ServicePlan Prisma models + service layer
- Add SubscriberStatus (ACTIVE/SUSPENDED/CANCELLED) and BillingType (PREPAID/POSTPAID) enums - Add ServicePlan model with name, speed, monthlyPrice, billingType, soft-delete - Add Subscriber model with accountNumber, billingDay, status lifecycle, creditBalance - Add TenantSettings model with autoSuspendDays and prepaidLeadDays - Migrate: 20260304145633_add_subscriber_models - Extend prisma-tenant.ts with subscriber, servicePlan, tenantSettings query scoping - Create service-plan-service.ts: createServicePlan, updateServicePlan, listServicePlans, deactivateServicePlan - Create subscriber-service.ts: createSubscriber, updateSubscriber, changeSubscriberStatus, searchSubscribers, getSubscriber, generateAccountNumber
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "SubscriberStatus" AS ENUM ('ACTIVE', 'SUSPENDED', 'CANCELLED');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "BillingType" AS ENUM ('PREPAID', 'POSTPAID');
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "TenantSettings" (
|
||||
"id" TEXT NOT NULL,
|
||||
"tenantId" TEXT NOT NULL,
|
||||
"autoSuspendDays" INTEGER NOT NULL DEFAULT 30,
|
||||
"prepaidLeadDays" INTEGER NOT NULL DEFAULT 7,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "TenantSettings_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "ServicePlan" (
|
||||
"id" TEXT NOT NULL,
|
||||
"tenantId" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"speed" TEXT NOT NULL,
|
||||
"monthlyPrice" DECIMAL(10,2) NOT NULL,
|
||||
"billingType" "BillingType" NOT NULL,
|
||||
"description" TEXT,
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "ServicePlan_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Subscriber" (
|
||||
"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,
|
||||
"zone" TEXT,
|
||||
"servicePlanId" TEXT NOT NULL,
|
||||
"status" "SubscriberStatus" NOT NULL DEFAULT 'ACTIVE',
|
||||
"billingDay" INTEGER NOT NULL,
|
||||
"activatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"suspendedAt" TIMESTAMP(3),
|
||||
"cancelledAt" TIMESTAMP(3),
|
||||
"autoSuspendDays" INTEGER,
|
||||
"notes" TEXT,
|
||||
"creditBalance" DECIMAL(10,2) NOT NULL DEFAULT 0,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Subscriber_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "TenantSettings_tenantId_key" ON "TenantSettings"("tenantId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "TenantSettings_tenantId_idx" ON "TenantSettings"("tenantId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "ServicePlan_tenantId_idx" ON "ServicePlan"("tenantId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "ServicePlan_tenantId_name_key" ON "ServicePlan"("tenantId", "name");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Subscriber_tenantId_idx" ON "Subscriber"("tenantId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Subscriber_tenantId_status_idx" ON "Subscriber"("tenantId", "status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Subscriber_tenantId_servicePlanId_idx" ON "Subscriber"("tenantId", "servicePlanId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Subscriber_tenantId_accountNumber_key" ON "Subscriber"("tenantId", "accountNumber");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Subscriber" ADD CONSTRAINT "Subscriber_servicePlanId_fkey" FOREIGN KEY ("servicePlanId") REFERENCES "ServicePlan"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
@@ -57,6 +57,17 @@ enum PeriodStatus {
|
||||
CLOSED
|
||||
}
|
||||
|
||||
enum SubscriberStatus {
|
||||
ACTIVE
|
||||
SUSPENDED
|
||||
CANCELLED
|
||||
}
|
||||
|
||||
enum BillingType {
|
||||
PREPAID
|
||||
POSTPAID
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// MODELS
|
||||
// =============================================================================
|
||||
@@ -137,6 +148,87 @@ model AccountingPeriod {
|
||||
@@index([tenantId])
|
||||
}
|
||||
|
||||
/// TenantSettings holds per-tenant configuration for billing and auto-suspension.
|
||||
/// One record per tenant, created on first access or at tenant provisioning.
|
||||
model TenantSettings {
|
||||
id String @id @default(uuid())
|
||||
tenantId String @unique
|
||||
/// Days overdue before automatic subscriber suspension (default 30)
|
||||
autoSuspendDays Int @default(30)
|
||||
/// Days before billing date to generate prepaid invoices (default 7)
|
||||
prepaidLeadDays Int @default(7)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
/// RLS-ready index — always present on tenant-scoped models
|
||||
@@index([tenantId])
|
||||
}
|
||||
|
||||
/// A ServicePlan defines the internet service offering (speed, price, billing type).
|
||||
/// Plans are soft-deleted (isActive=false) to preserve subscriber history.
|
||||
model ServicePlan {
|
||||
id String @id @default(uuid())
|
||||
tenantId String
|
||||
name String
|
||||
/// Human-readable speed description (e.g., "50 Mbps", "100/20 Mbps")
|
||||
speed String
|
||||
/// Monthly charge for this plan (2 decimal places)
|
||||
monthlyPrice Decimal @db.Decimal(10, 2)
|
||||
billingType BillingType
|
||||
description String?
|
||||
/// Soft-delete: inactive plans cannot be assigned to new subscribers
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
subscribers Subscriber[]
|
||||
|
||||
/// Plan names must be unique within a tenant
|
||||
@@unique([tenantId, name])
|
||||
/// RLS-ready index — always present on tenant-scoped models
|
||||
@@index([tenantId])
|
||||
}
|
||||
|
||||
/// A Subscriber is a customer of the ISP — the core billing entity.
|
||||
/// All invoices, payments, and collections target subscribers.
|
||||
model Subscriber {
|
||||
id String @id @default(uuid())
|
||||
tenantId String
|
||||
/// Auto-generated sequential identifier per tenant (e.g., "SUB-0001")
|
||||
accountNumber String
|
||||
firstName String
|
||||
lastName String
|
||||
email String?
|
||||
phone String?
|
||||
address String
|
||||
/// Zone for collector routing (Phase 3)
|
||||
zone String?
|
||||
servicePlanId String
|
||||
servicePlan ServicePlan @relation(fields: [servicePlanId], references: [id])
|
||||
status SubscriberStatus @default(ACTIVE)
|
||||
/// Day of month for invoice generation — derived from signup date, capped at 28
|
||||
billingDay Int
|
||||
activatedAt DateTime @default(now())
|
||||
suspendedAt DateTime?
|
||||
cancelledAt DateTime?
|
||||
/// Per-subscriber auto-suspend override (null = use TenantSettings.autoSuspendDays)
|
||||
autoSuspendDays Int?
|
||||
notes String?
|
||||
/// Overpayment credit balance — always updated atomically with journal entries.
|
||||
/// This is NOT a stored ledger balance; it tracks credits for the FIFO allocation
|
||||
/// system (02-05) and is always updated within the same transaction as the journal entry.
|
||||
creditBalance Decimal @default(0) @db.Decimal(10, 2)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
/// Account numbers must be unique within a tenant
|
||||
@@unique([tenantId, accountNumber])
|
||||
/// RLS-ready index — always present on tenant-scoped models
|
||||
@@index([tenantId])
|
||||
@@index([tenantId, status])
|
||||
@@index([tenantId, servicePlanId])
|
||||
}
|
||||
|
||||
/// A User belongs to a Tenant (or is a super-admin with no tenant).
|
||||
/// Email uniqueness is enforced per-tenant, not globally.
|
||||
/// Super-admins have isSuperAdmin=true and tenantId=null.
|
||||
|
||||
Reference in New Issue
Block a user