[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 <noreply@anthropic.com>
This commit is contained in:
rckrdmrd 2026-01-16 12:32:17 -06:00
parent dfe6a715f0
commit 2ffe4864dd
15 changed files with 410 additions and 8 deletions

View File

@ -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],

View File

@ -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';

View File

@ -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;
}

View File

@ -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,
],

View File

@ -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';

View File

@ -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<string, unknown>;
@CreateDateColumn({ type: 'timestamp with time zone' })
created_at: Date;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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<string, unknown>;
@CreateDateColumn({ type: 'timestamp with time zone' })
created_at: Date;
}

View File

@ -1,3 +1,4 @@
export * from './feature-flag.entity';
export * from './tenant-flag.entity';
export * from './user-flag.entity';
export * from './flag-evaluation.entity';

View File

@ -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],

View File

@ -0,0 +1,2 @@
export * from './tenant.entity';
export * from './tenant-setting.entity';

View File

@ -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<string, unknown> | 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;
}

View File

@ -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],