From 2ffe4864dd05fafa0905e0d7a43270cd98025d2f Mon Sep 17 00:00:00 2001 From: rckrdmrd Date: Fri, 16 Jan 2026 12:32:17 -0600 Subject: [PATCH] [template-saas/backend] feat: Add 7 TypeORM entities from HU-REFACT-005 Entities created: - RefreshToken (auth.refresh_tokens) - SubscriptionItem (billing.subscription_items) - InvoiceItem (billing.invoice_items) - Payment (billing.payments) - FlagEvaluationRecord (feature_flags.evaluations) - PlanFeature (plans.plan_features) - TenantSetting (tenants.tenant_settings) Updated modules to register new entities. DDL-Entity coherence: 82.5% -> 100% Co-Authored-By: Claude Opus 4.5 --- src/modules/auth/auth.module.ts | 4 +- src/modules/auth/entities/index.ts | 1 + .../auth/entities/refresh-token.entity.ts | 56 +++++++++++++ src/modules/billing/billing.module.ts | 22 +++++- src/modules/billing/entities/index.ts | 4 + .../billing/entities/invoice-item.entity.ts | 52 ++++++++++++ .../billing/entities/payment.entity.ts | 79 +++++++++++++++++++ .../billing/entities/plan-feature.entity.ts | 46 +++++++++++ .../entities/subscription-item.entity.ts | 43 ++++++++++ .../entities/flag-evaluation.entity.ts | 44 +++++++++++ src/modules/feature-flags/entities/index.ts | 1 + .../feature-flags/feature-flags.module.ts | 4 +- src/modules/tenants/entities/index.ts | 2 + .../tenants/entities/tenant-setting.entity.ts | 56 +++++++++++++ src/modules/tenants/tenants.module.ts | 4 +- 15 files changed, 410 insertions(+), 8 deletions(-) create mode 100644 src/modules/auth/entities/refresh-token.entity.ts create mode 100644 src/modules/billing/entities/invoice-item.entity.ts create mode 100644 src/modules/billing/entities/payment.entity.ts create mode 100644 src/modules/billing/entities/plan-feature.entity.ts create mode 100644 src/modules/billing/entities/subscription-item.entity.ts create mode 100644 src/modules/feature-flags/entities/flag-evaluation.entity.ts create mode 100644 src/modules/tenants/entities/index.ts create mode 100644 src/modules/tenants/entities/tenant-setting.entity.ts diff --git a/src/modules/auth/auth.module.ts b/src/modules/auth/auth.module.ts index 688cd79..f4fa83b 100644 --- a/src/modules/auth/auth.module.ts +++ b/src/modules/auth/auth.module.ts @@ -5,7 +5,7 @@ import { PassportModule } from '@nestjs/passport'; import { ConfigModule, ConfigService } from '@nestjs/config'; // Entities -import { User, Session, Token, OAuthConnection } from './entities'; +import { User, Session, Token, RefreshToken, OAuthConnection } from './entities'; // Services import { AuthService } from './services/auth.service'; @@ -37,7 +37,7 @@ import { JwtStrategy } from './strategies/jwt.strategy'; }), // TypeORM entities - TypeOrmModule.forFeature([User, Session, Token, OAuthConnection]), + TypeOrmModule.forFeature([User, Session, Token, RefreshToken, OAuthConnection]), ], controllers: [AuthController, OAuthController], providers: [AuthService, OAuthService, MfaService, JwtStrategy], diff --git a/src/modules/auth/entities/index.ts b/src/modules/auth/entities/index.ts index 31d77a2..30435b6 100644 --- a/src/modules/auth/entities/index.ts +++ b/src/modules/auth/entities/index.ts @@ -1,5 +1,6 @@ export * from './user.entity'; export * from './session.entity'; export * from './token.entity'; +export * from './refresh-token.entity'; export * from './oauth-provider.enum'; export * from './oauth-connection.entity'; diff --git a/src/modules/auth/entities/refresh-token.entity.ts b/src/modules/auth/entities/refresh-token.entity.ts new file mode 100644 index 0000000..a6b70a2 --- /dev/null +++ b/src/modules/auth/entities/refresh-token.entity.ts @@ -0,0 +1,56 @@ +import { + Entity, + Column, + PrimaryGeneratedColumn, + CreateDateColumn, + Index, +} from 'typeorm'; + +/** + * RefreshToken Entity + * Maps to auth.refresh_tokens DDL table + * Supports JWT refresh token rotation with family tracking + */ +@Entity({ schema: 'auth', name: 'refresh_tokens' }) +export class RefreshToken { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'uuid' }) + @Index() + tenant_id: string; + + @Column({ type: 'uuid' }) + @Index() + user_id: string; + + @Column({ type: 'uuid', nullable: true }) + @Index() + session_id: string | null; + + @Column({ type: 'varchar', length: 255 }) + @Index({ unique: true }) + token_hash: string; + + @Column({ type: 'uuid' }) + @Index() + family_id: string; + + @Column({ type: 'int', default: 1 }) + generation: number; + + @Column({ type: 'boolean', default: false }) + is_revoked: boolean; + + @Column({ type: 'timestamp with time zone', nullable: true }) + revoked_at: Date | null; + + @Column({ type: 'timestamp with time zone' }) + expires_at: Date; + + @CreateDateColumn({ type: 'timestamp with time zone' }) + created_at: Date; + + @Column({ type: 'timestamp with time zone', nullable: true }) + used_at: Date | null; +} diff --git a/src/modules/billing/billing.module.ts b/src/modules/billing/billing.module.ts index 174a8c6..2ceae1a 100644 --- a/src/modules/billing/billing.module.ts +++ b/src/modules/billing/billing.module.ts @@ -4,12 +4,30 @@ import { ConfigModule } from '@nestjs/config'; import { BillingController } from './billing.controller'; import { BillingService, StripeService, PlansService } from './services'; import { StripeController, StripeWebhookController, PlansController } from './controllers'; -import { Subscription, Invoice, PaymentMethod, Plan } from './entities'; +import { + Subscription, + SubscriptionItem, + Invoice, + InvoiceItem, + Payment, + PaymentMethod, + Plan, + PlanFeature, +} from './entities'; import { RbacModule } from '../rbac/rbac.module'; @Module({ imports: [ - TypeOrmModule.forFeature([Subscription, Invoice, PaymentMethod, Plan]), + TypeOrmModule.forFeature([ + Subscription, + SubscriptionItem, + Invoice, + InvoiceItem, + Payment, + PaymentMethod, + Plan, + PlanFeature, + ]), ConfigModule, RbacModule, ], diff --git a/src/modules/billing/entities/index.ts b/src/modules/billing/entities/index.ts index 015e61d..9bd57ce 100644 --- a/src/modules/billing/entities/index.ts +++ b/src/modules/billing/entities/index.ts @@ -1,4 +1,8 @@ export * from './subscription.entity'; +export * from './subscription-item.entity'; export * from './invoice.entity'; +export * from './invoice-item.entity'; +export * from './payment.entity'; export * from './payment-method.entity'; export * from './plan.entity'; +export * from './plan-feature.entity'; diff --git a/src/modules/billing/entities/invoice-item.entity.ts b/src/modules/billing/entities/invoice-item.entity.ts new file mode 100644 index 0000000..c340e71 --- /dev/null +++ b/src/modules/billing/entities/invoice-item.entity.ts @@ -0,0 +1,52 @@ +import { + Entity, + Column, + PrimaryGeneratedColumn, + CreateDateColumn, + Index, +} from 'typeorm'; + +/** + * InvoiceItem Entity + * Maps to billing.invoice_items DDL table + * Line items within an invoice + */ +@Entity({ schema: 'billing', name: 'invoice_items' }) +export class InvoiceItem { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'uuid' }) + @Index() + invoice_id: string; + + @Column({ type: 'varchar', length: 500 }) + description: string; + + @Column({ type: 'int', default: 1 }) + quantity: number; + + @Column({ type: 'decimal', precision: 10, scale: 2 }) + unit_amount: number; + + @Column({ type: 'decimal', precision: 10, scale: 2 }) + amount: number; + + @Column({ type: 'varchar', length: 3, default: 'USD' }) + currency: string; + + @Column({ type: 'date', nullable: true }) + period_start: Date | null; + + @Column({ type: 'date', nullable: true }) + period_end: Date | null; + + @Column({ type: 'varchar', length: 255, nullable: true }) + stripe_invoice_item_id: string | null; + + @Column({ type: 'jsonb', default: {} }) + metadata: Record; + + @CreateDateColumn({ type: 'timestamp with time zone' }) + created_at: Date; +} diff --git a/src/modules/billing/entities/payment.entity.ts b/src/modules/billing/entities/payment.entity.ts new file mode 100644 index 0000000..1cf681f --- /dev/null +++ b/src/modules/billing/entities/payment.entity.ts @@ -0,0 +1,79 @@ +import { + Entity, + Column, + PrimaryGeneratedColumn, + CreateDateColumn, + UpdateDateColumn, + Index, +} from 'typeorm'; + +/** + * Payment Entity + * Maps to billing.payments DDL table + * Payment records and attempts + */ +@Entity({ schema: 'billing', name: 'payments' }) +export class Payment { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'uuid' }) + @Index() + tenant_id: string; + + @Column({ type: 'uuid', nullable: true }) + @Index() + invoice_id: string | null; + + @Column({ type: 'varchar', length: 255, nullable: true }) + @Index({ unique: true }) + stripe_payment_intent_id: string | null; + + @Column({ type: 'varchar', length: 255, nullable: true }) + stripe_charge_id: string | null; + + @Column({ type: 'decimal', precision: 10, scale: 2 }) + amount: number; + + @Column({ type: 'varchar', length: 3, default: 'USD' }) + currency: string; + + @Column({ type: 'varchar', length: 50, default: 'pending' }) + status: string; + + @Column({ type: 'varchar', length: 50, nullable: true }) + payment_method_type: string | null; + + @Column({ type: 'varchar', length: 4, nullable: true }) + payment_method_last4: string | null; + + @Column({ type: 'varchar', length: 50, nullable: true }) + payment_method_brand: string | null; + + @Column({ type: 'varchar', length: 100, nullable: true }) + failure_code: string | null; + + @Column({ type: 'text', nullable: true }) + failure_message: string | null; + + @CreateDateColumn({ type: 'timestamp with time zone' }) + created_at: Date; + + @UpdateDateColumn({ type: 'timestamp with time zone' }) + updated_at: Date; + + @Column({ type: 'timestamp with time zone', nullable: true }) + succeeded_at: Date | null; + + @Column({ type: 'timestamp with time zone', nullable: true }) + failed_at: Date | null; + + @Column({ type: 'timestamp with time zone', nullable: true }) + refunded_at: Date | null; + + @Column({ type: 'decimal', precision: 10, scale: 2, nullable: true }) + refund_amount: number | null; + + @Column({ type: 'varchar', length: 500, nullable: true }) + refund_reason: string | null; +} diff --git a/src/modules/billing/entities/plan-feature.entity.ts b/src/modules/billing/entities/plan-feature.entity.ts new file mode 100644 index 0000000..6a3bacd --- /dev/null +++ b/src/modules/billing/entities/plan-feature.entity.ts @@ -0,0 +1,46 @@ +import { Entity, Column, PrimaryGeneratedColumn, Index } from 'typeorm'; + +/** + * PlanFeature Entity + * Maps to plans.plan_features DDL table + * Detailed feature matrix per plan + */ +@Entity({ schema: 'plans', name: 'plan_features' }) +export class PlanFeature { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'uuid' }) + @Index() + plan_id: string; + + @Column({ type: 'varchar', length: 100 }) + feature_code: string; + + @Column({ type: 'varchar', length: 200 }) + feature_name: string; + + @Column({ type: 'varchar', length: 100, nullable: true }) + category: string | null; + + @Column({ type: 'varchar', length: 20 }) + value_type: string; + + @Column({ type: 'boolean', nullable: true }) + value_boolean: boolean | null; + + @Column({ type: 'int', nullable: true }) + value_number: number | null; + + @Column({ type: 'varchar', length: 200, nullable: true }) + value_text: string | null; + + @Column({ type: 'varchar', length: 100, nullable: true }) + display_value: string | null; + + @Column({ type: 'boolean', default: false }) + is_highlighted: boolean; + + @Column({ type: 'int', default: 0 }) + sort_order: number; +} diff --git a/src/modules/billing/entities/subscription-item.entity.ts b/src/modules/billing/entities/subscription-item.entity.ts new file mode 100644 index 0000000..d7881f2 --- /dev/null +++ b/src/modules/billing/entities/subscription-item.entity.ts @@ -0,0 +1,43 @@ +import { + Entity, + Column, + PrimaryGeneratedColumn, + CreateDateColumn, + Index, +} from 'typeorm'; + +/** + * SubscriptionItem Entity + * Maps to billing.subscription_items DDL table + * Line items within a subscription (metered/usage-based billing) + */ +@Entity({ schema: 'billing', name: 'subscription_items' }) +export class SubscriptionItem { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'uuid' }) + @Index() + subscription_id: string; + + @Column({ type: 'varchar', length: 255, nullable: true }) + stripe_subscription_item_id: string | null; + + @Column({ type: 'varchar', length: 255, nullable: true }) + stripe_price_id: string | null; + + @Column({ type: 'varchar', length: 200, nullable: true }) + product_name: string | null; + + @Column({ type: 'int', default: 1 }) + quantity: number; + + @Column({ type: 'decimal', precision: 10, scale: 2, nullable: true }) + unit_amount: number | null; + + @Column({ type: 'boolean', default: false }) + is_metered: boolean; + + @CreateDateColumn({ type: 'timestamp with time zone' }) + created_at: Date; +} diff --git a/src/modules/feature-flags/entities/flag-evaluation.entity.ts b/src/modules/feature-flags/entities/flag-evaluation.entity.ts new file mode 100644 index 0000000..c3ac235 --- /dev/null +++ b/src/modules/feature-flags/entities/flag-evaluation.entity.ts @@ -0,0 +1,44 @@ +import { + Entity, + Column, + PrimaryGeneratedColumn, + CreateDateColumn, + Index, +} from 'typeorm'; + +/** + * FlagEvaluationRecord Entity + * Maps to feature_flags.evaluations DDL table + * Flag evaluation history for analytics + */ +@Entity({ schema: 'feature_flags', name: 'evaluations' }) +export class FlagEvaluationRecord { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'uuid' }) + @Index() + tenant_id: string; + + @Column({ type: 'uuid' }) + @Index() + flag_id: string; + + @Column({ type: 'uuid', nullable: true }) + user_id: string | null; + + @Column({ type: 'varchar', length: 100 }) + flag_code: string; + + @Column({ type: 'boolean' }) + result: boolean; + + @Column({ type: 'varchar', length: 100, nullable: true }) + evaluation_reason: string | null; + + @Column({ type: 'jsonb', default: {} }) + context: Record; + + @CreateDateColumn({ type: 'timestamp with time zone' }) + created_at: Date; +} diff --git a/src/modules/feature-flags/entities/index.ts b/src/modules/feature-flags/entities/index.ts index fe807b7..5e6a2d9 100644 --- a/src/modules/feature-flags/entities/index.ts +++ b/src/modules/feature-flags/entities/index.ts @@ -1,3 +1,4 @@ export * from './feature-flag.entity'; export * from './tenant-flag.entity'; export * from './user-flag.entity'; +export * from './flag-evaluation.entity'; diff --git a/src/modules/feature-flags/feature-flags.module.ts b/src/modules/feature-flags/feature-flags.module.ts index be94b80..800dbf1 100644 --- a/src/modules/feature-flags/feature-flags.module.ts +++ b/src/modules/feature-flags/feature-flags.module.ts @@ -2,11 +2,11 @@ import { Module, Global } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { FeatureFlagsController } from './feature-flags.controller'; import { FeatureFlagsService } from './services/feature-flags.service'; -import { FeatureFlag, TenantFlag, UserFlag } from './entities'; +import { FeatureFlag, TenantFlag, UserFlag, FlagEvaluationRecord } from './entities'; @Global() @Module({ - imports: [TypeOrmModule.forFeature([FeatureFlag, TenantFlag, UserFlag])], + imports: [TypeOrmModule.forFeature([FeatureFlag, TenantFlag, UserFlag, FlagEvaluationRecord])], controllers: [FeatureFlagsController], providers: [FeatureFlagsService], exports: [FeatureFlagsService], diff --git a/src/modules/tenants/entities/index.ts b/src/modules/tenants/entities/index.ts new file mode 100644 index 0000000..2c5f924 --- /dev/null +++ b/src/modules/tenants/entities/index.ts @@ -0,0 +1,2 @@ +export * from './tenant.entity'; +export * from './tenant-setting.entity'; diff --git a/src/modules/tenants/entities/tenant-setting.entity.ts b/src/modules/tenants/entities/tenant-setting.entity.ts new file mode 100644 index 0000000..57ab925 --- /dev/null +++ b/src/modules/tenants/entities/tenant-setting.entity.ts @@ -0,0 +1,56 @@ +import { + Entity, + Column, + PrimaryGeneratedColumn, + CreateDateColumn, + UpdateDateColumn, + Index, +} from 'typeorm'; + +/** + * TenantSetting Entity + * Maps to tenants.tenant_settings DDL table + * Structured tenant settings for queryable configuration + */ +@Entity({ schema: 'tenants', name: 'tenant_settings' }) +export class TenantSetting { + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column({ type: 'uuid' }) + @Index() + tenant_id: string; + + @Column({ type: 'varchar', length: 100 }) + category: string; + + @Column({ type: 'varchar', length: 100 }) + key: string; + + @Column({ type: 'varchar', length: 1000, nullable: true }) + value_string: string | null; + + @Column({ type: 'decimal', precision: 20, scale: 4, nullable: true }) + value_number: number | null; + + @Column({ type: 'boolean', nullable: true }) + value_boolean: boolean | null; + + @Column({ type: 'jsonb', nullable: true }) + value_json: Record | null; + + @Column({ type: 'text', nullable: true }) + description: string | null; + + @Column({ type: 'boolean', default: false }) + is_sensitive: boolean; + + @CreateDateColumn({ type: 'timestamp with time zone' }) + created_at: Date; + + @UpdateDateColumn({ type: 'timestamp with time zone' }) + updated_at: Date; + + @Column({ type: 'uuid', nullable: true }) + updated_by: string | null; +} diff --git a/src/modules/tenants/tenants.module.ts b/src/modules/tenants/tenants.module.ts index f1f65c6..3a1c861 100644 --- a/src/modules/tenants/tenants.module.ts +++ b/src/modules/tenants/tenants.module.ts @@ -2,11 +2,11 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { TenantsController } from './tenants.controller'; import { TenantsService } from './tenants.service'; -import { Tenant } from './entities/tenant.entity'; +import { Tenant, TenantSetting } from './entities'; import { RbacModule } from '../rbac/rbac.module'; @Module({ - imports: [TypeOrmModule.forFeature([Tenant]), RbacModule], + imports: [TypeOrmModule.forFeature([Tenant, TenantSetting]), RbacModule], controllers: [TenantsController], providers: [TenantsService], exports: [TenantsService],