import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose' import { HydratedDocument, Types } from 'mongoose' export type TenantDocument = HydratedDocument export type TenantStatus = 'pending' | 'active' | 'suspended' | 'deleted' export type TenantPlan = 'mvp' | 'pro' | 'enterprise' // One field per external integration. 'pending' = not yet tried; 'ok' = synced; // 'error' = last attempt failed (see provisioningErrors for detail). export type IntegrationState = 'pending' | 'ok' | 'error' | 'skipped' @Schema({ collection: 'tenants', timestamps: true }) export class Tenant { // URL-safe identifier, also used as Authentik group name. Lowercase, hyphenated. @Prop({ required: true, unique: true, index: true, lowercase: true, trim: true }) slug!: string @Prop({ required: true, trim: true }) name!: string @Prop({ enum: ['pending', 'active', 'suspended', 'deleted'], default: 'pending', index: true }) status!: TenantStatus @Prop({ enum: ['mvp', 'pro', 'enterprise'], default: 'mvp' }) plan!: TenantPlan // Initial seat count from provisioning. Used for portfolio displays and // (later) MRR calculations. The "used" count comes from User.tenantIds — // not stored here to avoid a denormalized field that drifts on every // user-add/remove. Default 0 so older docs without this field render // as "0 / N" without throwing. @Prop({ type: Number, min: 0, default: 0 }) seats!: number // Custom domains attached to this tenant. First entry is the primary host. @Prop({ type: [String], default: [] }) domains!: string[] // Partner-editable customer metadata. Display-only — not used for // provisioning. `industry` is free text; `brandColor` is a #rrggbb hex // (validated at the DTO layer) rendered as the customer's swatch in the // partner portal. @Prop({ trim: true }) industry?: string @Prop({ trim: true }) brandColor?: string // Optional MSP/reseller this tenant belongs to. Sparse — direct tenants have none. @Prop({ type: Types.ObjectId, ref: 'Partner', index: true, sparse: true }) partnerId?: Types.ObjectId // External system handles — filled in by the provisioning worker (Phase 4) @Prop({ index: true, sparse: true }) authentikGroupId?: string @Prop({ sparse: true }) ocisSpaceId?: string @Prop({ sparse: true }) stalwartDomain?: string // Free-form billing context. Stripe IDs live on Subscription, not here. @Prop({ type: { companyName: String, vatId: String, country: String, contactEmail: String, }, default: {}, }) billingInfo!: { companyName?: string vatId?: string country?: string contactEmail?: string } // Customer-managed security policy. Stored intent; enforcement is wired // incrementally (MFA enrollment is read live; MFA/geo/session enforcement // via Authentik lands in a later stage). mfaMode: 'all' | 'admins' | // 'optional'. Timeouts in minutes/hours. allowedCountries = ISO alpha-2; // ipAllowlist = CIDR strings. @Prop({ type: { mfaMode: { type: String, enum: ['all', 'admins', 'optional'], default: 'optional' }, sessionIdleMinutes: { type: Number, min: 0 }, sessionAbsoluteHours: { type: Number, min: 0 }, allowedCountries: { type: [String], default: undefined }, ipAllowlist: { type: [String], default: undefined }, }, default: () => ({ mfaMode: 'optional' }), }) securityPolicy!: { mfaMode: 'all' | 'admins' | 'optional' sessionIdleMinutes?: number sessionAbsoluteHours?: number allowedCountries?: string[] ipAllowlist?: string[] } // Per-integration provisioning state. Each one is updated independently when its // upstream API call succeeds or fails — orchestration is best-effort, not atomic. @Prop({ type: { authentik: { type: String, enum: ['pending', 'ok', 'error', 'skipped'], default: 'pending' }, stalwart: { type: String, enum: ['pending', 'ok', 'error', 'skipped'], default: 'pending' }, ocis: { type: String, enum: ['pending', 'ok', 'error', 'skipped'], default: 'pending' }, }, default: () => ({ authentik: 'pending', stalwart: 'pending', ocis: 'pending' }), }) provisioningStatus!: { authentik: IntegrationState stalwart: IntegrationState ocis: IntegrationState } // Last error message per integration. Cleared when a subsequent attempt succeeds. @Prop({ type: Object, default: {} }) provisioningErrors!: { authentik?: string stalwart?: string ocis?: string } } export const TenantSchema = SchemaFactory.createForClass(Tenant)