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:
kevin-asprec
2026-03-04 22:58:53 +08:00
parent e49db94f38
commit 9cc6af14e9
5 changed files with 926 additions and 1 deletions

View File

@@ -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.