Initial commit: HOA Financial Intelligence Platform MVP

Multi-tenant financial management platform for homeowner associations featuring:
- NestJS backend with 16 modules (auth, accounts, transactions, budgets, units,
  invoices, payments, vendors, reserves, investments, capital projects, reports)
- React + Mantine frontend with dashboard, CRUD pages, and financial reports
- Schema-per-tenant PostgreSQL isolation with JWT-based tenant resolution
- Docker Compose infrastructure (nginx, backend, frontend, postgres, redis)
- Comprehensive seed data for Sunrise Valley HOA demo
- 39 API endpoints with Swagger documentation
- Double-entry bookkeeping with journal entries
- Budget vs actual reporting and Sankey cash flow visualization

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-17 19:58:04 -05:00
commit 243770cea5
118 changed files with 8569 additions and 0 deletions

View File

@@ -0,0 +1,23 @@
import { IsEmail, IsString, MinLength, IsOptional } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class CreateUserDto {
@ApiProperty({ example: 'john@example.com' })
@IsEmail()
email: string;
@ApiProperty({ example: 'SecurePass123!' })
@IsString()
@MinLength(8)
password: string;
@ApiProperty({ example: 'John', required: false })
@IsString()
@IsOptional()
firstName?: string;
@ApiProperty({ example: 'Smith', required: false })
@IsString()
@IsOptional()
lastName?: string;
}

View File

@@ -0,0 +1,57 @@
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
OneToMany,
} from 'typeorm';
import { UserOrganization } from '../../organizations/entities/user-organization.entity';
@Entity({ schema: 'shared', name: 'users' })
export class User {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ unique: true })
email: string;
@Column({ name: 'password_hash', nullable: true })
passwordHash: string;
@Column({ name: 'first_name', nullable: true })
firstName: string;
@Column({ name: 'last_name', nullable: true })
lastName: string;
@Column({ nullable: true })
phone: string;
@Column({ name: 'is_email_verified', default: false })
isEmailVerified: boolean;
@Column({ name: 'mfa_enabled', default: false })
mfaEnabled: boolean;
@Column({ name: 'mfa_secret', nullable: true })
mfaSecret: string;
@Column({ name: 'oauth_provider', nullable: true })
oauthProvider: string;
@Column({ name: 'oauth_provider_id', nullable: true })
oauthProviderId: string;
@Column({ name: 'last_login_at', type: 'timestamptz', nullable: true })
lastLoginAt: Date;
@CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
updatedAt: Date;
@OneToMany(() => UserOrganization, (uo) => uo.user)
userOrganizations: UserOrganization[];
}

View File

@@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './entities/user.entity';
import { UsersService } from './users.service';
@Module({
imports: [TypeOrmModule.forFeature([User])],
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}

View File

@@ -0,0 +1,41 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './entities/user.entity';
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private usersRepository: Repository<User>,
) {}
async findByEmail(email: string): Promise<User | null> {
return this.usersRepository.findOne({
where: { email: email.toLowerCase() },
});
}
async findById(id: string): Promise<User | null> {
return this.usersRepository.findOne({ where: { id } });
}
async findByIdWithOrgs(id: string): Promise<User | null> {
return this.usersRepository.findOne({
where: { id },
relations: ['userOrganizations', 'userOrganizations.organization'],
});
}
async create(data: Partial<User>): Promise<User> {
const user = this.usersRepository.create({
...data,
email: data.email?.toLowerCase(),
});
return this.usersRepository.save(user);
}
async updateLastLogin(id: string): Promise<void> {
await this.usersRepository.update(id, { lastLoginAt: new Date() });
}
}