# Repository Pattern Migration Guide

## Overview

This guide helps you migrate from direct service-to-database access to the Repository pattern, implementing proper Dependency Inversion Principle (DIP) for the ERP-Suite project.

## Why Migrate?

### Problems with Current Approach
- **Tight Coupling**: Services directly depend on concrete implementations
- **Testing Difficulty**: Hard to mock database access
- **DIP Violation**: High-level modules depend on low-level modules
- **Code Duplication**: Similar queries repeated across services

### Benefits of Repository Pattern
- **Loose Coupling**: Services depend on interfaces, not implementations
- **Testability**: Easy to mock repositories for unit tests
- **Maintainability**: Centralized data access logic
- **Flexibility**: Swap implementations without changing service code

## Architecture

```
┌─────────────────────────────────────────────────┐
│                  Service Layer                  │
│  (Depends on IRepository<T> interfaces)         │
└────────────────────┬────────────────────────────┘
                     │ (Dependency Inversion)
                     ▼
┌─────────────────────────────────────────────────┐
│            Repository Interfaces                │
│  IUserRepository, ITenantRepository, etc.       │
└────────────────────┬────────────────────────────┘
                     │
                     ▼
┌─────────────────────────────────────────────────┐
│          Repository Implementations             │
│  UserRepository, TenantRepository, etc.         │
└────────────────────┬────────────────────────────┘
                     │
                     ▼
┌─────────────────────────────────────────────────┐
│              Database (TypeORM)                 │
└─────────────────────────────────────────────────┘
```

## Step-by-Step Migration

### Step 1: Create Repository Implementation

**Before (Direct TypeORM in Service):**
```typescript
// services/user.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from '@erp-suite/core';

@Injectable()
export class UserService {
  constructor(
    @InjectRepository(User)
    private userRepo: Repository<User>,
  ) {}

  async findByEmail(email: string): Promise<User | null> {
    return this.userRepo.findOne({ where: { email } });
  }
}
```

**After (Create Repository):**
```typescript
// repositories/user.repository.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import {
  User,
  IUserRepository,
  ServiceContext,
  PaginatedResult,
  PaginationOptions,
} from '@erp-suite/core';

@Injectable()
export class UserRepository implements IUserRepository {
  constructor(
    @InjectRepository(User)
    private readonly ormRepo: Repository<User>,
  ) {}

  async findById(ctx: ServiceContext, id: string): Promise<User | null> {
    return this.ormRepo.findOne({
      where: { id, tenantId: ctx.tenantId },
    });
  }

  async findByEmail(ctx: ServiceContext, email: string): Promise<User | null> {
    return this.ormRepo.findOne({
      where: { email, tenantId: ctx.tenantId },
    });
  }

  async findByTenantId(ctx: ServiceContext, tenantId: string): Promise<User[]> {
    return this.ormRepo.find({
      where: { tenantId },
    });
  }

  async findActiveUsers(
    ctx: ServiceContext,
    filters?: PaginationOptions,
  ): Promise<PaginatedResult<User>> {
    const page = filters?.page || 1;
    const pageSize = filters?.pageSize || 20;

    const [data, total] = await this.ormRepo.findAndCount({
      where: { tenantId: ctx.tenantId, status: 'active' },
      skip: (page - 1) * pageSize,
      take: pageSize,
    });

    return {
      data,
      meta: {
        page,
        pageSize,
        totalRecords: total,
        totalPages: Math.ceil(total / pageSize),
      },
    };
  }

  async updateLastLogin(ctx: ServiceContext, userId: string): Promise<void> {
    await this.ormRepo.update(
      { id: userId, tenantId: ctx.tenantId },
      { lastLoginAt: new Date() },
    );
  }

  async updatePasswordHash(
    ctx: ServiceContext,
    userId: string,
    passwordHash: string,
  ): Promise<void> {
    await this.ormRepo.update(
      { id: userId, tenantId: ctx.tenantId },
      { passwordHash },
    );
  }

  // Implement remaining IRepository<User> methods...
  async create(ctx: ServiceContext, data: Partial<User>): Promise<User> {
    const user = this.ormRepo.create({
      ...data,
      tenantId: ctx.tenantId,
    });
    return this.ormRepo.save(user);
  }

  async update(
    ctx: ServiceContext,
    id: string,
    data: Partial<User>,
  ): Promise<User | null> {
    await this.ormRepo.update(
      { id, tenantId: ctx.tenantId },
      data,
    );
    return this.findById(ctx, id);
  }

  // ... implement other methods
}
```

### Step 2: Register Repository in Module

```typescript
// user.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from '@erp-suite/core';
import { UserService } from './services/user.service';
import { UserRepository } from './repositories/user.repository';
import { UserController } from './controllers/user.controller';

@Module({
  imports: [TypeOrmModule.forFeature([User])],
  providers: [
    UserService,
    UserRepository,
    // Register in RepositoryFactory
    {
      provide: 'REPOSITORY_FACTORY_SETUP',
      useFactory: (userRepository: UserRepository) => {
        const factory = RepositoryFactory.getInstance();
        factory.register('UserRepository', userRepository);
      },
      inject: [UserRepository],
    },
  ],
  controllers: [UserController],
  exports: [UserRepository],
})
export class UserModule {}
```

### Step 3: Update Service to Use Repository

```typescript
// services/user.service.ts
import { Injectable } from '@nestjs/common';
import {
  IUserRepository,
  ServiceContext,
  RepositoryFactory,
} from '@erp-suite/core';

@Injectable()
export class UserService {
  private readonly userRepository: IUserRepository;

  constructor() {
    const factory = RepositoryFactory.getInstance();
    this.userRepository = factory.getRequired<IUserRepository>('UserRepository');
  }

  async findByEmail(
    ctx: ServiceContext,
    email: string,
  ): Promise<User | null> {
    return this.userRepository.findByEmail(ctx, email);
  }

  async getActiveUsers(
    ctx: ServiceContext,
    page: number = 1,
    pageSize: number = 20,
  ): Promise<PaginatedResult<User>> {
    return this.userRepository.findActiveUsers(ctx, { page, pageSize });
  }

  async updateLastLogin(ctx: ServiceContext, userId: string): Promise<void> {
    await this.userRepository.updateLastLogin(ctx, userId);
  }
}
```

### Step 4: Alternative - Use Decorator Pattern

```typescript
// services/user.service.ts (with decorator)
import { Injectable } from '@nestjs/common';
import {
  IUserRepository,
  InjectRepository,
  ServiceContext,
} from '@erp-suite/core';

@Injectable()
export class UserService {
  @InjectRepository('UserRepository')
  private readonly userRepository: IUserRepository;

  async findByEmail(
    ctx: ServiceContext,
    email: string,
  ): Promise<User | null> {
    return this.userRepository.findByEmail(ctx, email);
  }
}
```

## Testing with Repositories

### Create Mock Repository

```typescript
// tests/mocks/user.repository.mock.ts
import { IUserRepository, ServiceContext, User } from '@erp-suite/core';

export class MockUserRepository implements IUserRepository {
  private users: User[] = [];

  async findById(ctx: ServiceContext, id: string): Promise<User | null> {
    return this.users.find(u => u.id === id) || null;
  }

  async findByEmail(ctx: ServiceContext, email: string): Promise<User | null> {
    return this.users.find(u => u.email === email) || null;
  }

  async create(ctx: ServiceContext, data: Partial<User>): Promise<User> {
    const user = { id: 'test-id', ...data } as User;
    this.users.push(user);
    return user;
  }

  // Implement other methods as needed
}
```

### Use Mock in Tests

```typescript
// tests/user.service.spec.ts
import { Test } from '@nestjs/testing';
import { UserService } from '../services/user.service';
import { RepositoryFactory } from '@erp-suite/core';
import { MockUserRepository } from './mocks/user.repository.mock';

describe('UserService', () => {
  let service: UserService;
  let mockRepo: MockUserRepository;
  let factory: RepositoryFactory;

  beforeEach(async () => {
    mockRepo = new MockUserRepository();
    factory = RepositoryFactory.getInstance();
    factory.clear(); // Clear previous registrations
    factory.register('UserRepository', mockRepo);

    const module = await Test.createTestingModule({
      providers: [UserService],
    }).compile();

    service = module.get<UserService>(UserService);
  });

  afterEach(() => {
    factory.clear();
  });

  it('should find user by email', async () => {
    const ctx = { tenantId: 'tenant-1', userId: 'user-1' };
    const user = await mockRepo.create(ctx, {
      email: 'test@example.com',
      fullName: 'Test User',
    });

    const found = await service.findByEmail(ctx, 'test@example.com');
    expect(found).toEqual(user);
  });
});
```

## Common Repository Patterns

### 1. Tenant-Scoped Queries

```typescript
async findAll(
  ctx: ServiceContext,
  filters?: PaginationOptions,
): Promise<PaginatedResult<T>> {
  // Always filter by tenant
  const where = { tenantId: ctx.tenantId };

  const [data, total] = await this.ormRepo.findAndCount({
    where,
    skip: ((filters?.page || 1) - 1) * (filters?.pageSize || 20),
    take: filters?.pageSize || 20,
  });

  return {
    data,
    meta: {
      page: filters?.page || 1,
      pageSize: filters?.pageSize || 20,
      totalRecords: total,
      totalPages: Math.ceil(total / (filters?.pageSize || 20)),
    },
  };
}
```

### 2. Audit Trail Integration

```typescript
async create(ctx: ServiceContext, data: Partial<T>): Promise<T> {
  const entity = this.ormRepo.create({
    ...data,
    tenantId: ctx.tenantId,
    createdBy: ctx.userId,
  });

  const saved = await this.ormRepo.save(entity);

  // Log audit trail
  await this.auditRepository.logAction(ctx, {
    tenantId: ctx.tenantId,
    userId: ctx.userId,
    action: 'CREATE',
    entityType: this.entityName,
    entityId: saved.id,
    timestamp: new Date(),
  });

  return saved;
}
```

### 3. Complex Queries with QueryBuilder

```typescript
async findWithRelations(
  ctx: ServiceContext,
  filters: any,
): Promise<User[]> {
  return this.ormRepo
    .createQueryBuilder('user')
    .leftJoinAndSelect('user.tenant', 'tenant')
    .where('user.tenantId = :tenantId', { tenantId: ctx.tenantId })
    .andWhere('user.status = :status', { status: 'active' })
    .orderBy('user.createdAt', 'DESC')
    .getMany();
}
```

## Migration Checklist

For each service:

- [ ] Identify all database access patterns
- [ ] Create repository interface (or use existing IRepository<T>)
- [ ] Implement repository class
- [ ] Register repository in module
- [ ] Update service to use repository
- [ ] Create mock repository for tests
- [ ] Update tests to use mock repository
- [ ] Verify multi-tenancy filtering
- [ ] Add audit logging if needed
- [ ] Document any custom repository methods

## Repository Factory Best Practices

### 1. Initialize Once at Startup

```typescript
// main.ts or app.module.ts
import { RepositoryFactory } from '@erp-suite/core';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // Initialize factory with all repositories
  const factory = RepositoryFactory.getInstance();

  // Repositories are registered in their respective modules
  console.log(
    `Registered repositories: ${factory.getRegisteredNames().join(', ')}`,
  );

  await app.listen(3000);
}
```

### 2. Use Dependency Injection

```typescript
// Prefer constructor injection with factory
@Injectable()
export class MyService {
  private readonly userRepo: IUserRepository;
  private readonly tenantRepo: ITenantRepository;

  constructor() {
    const factory = RepositoryFactory.getInstance();
    this.userRepo = factory.getRequired('UserRepository');
    this.tenantRepo = factory.getRequired('TenantRepository');
  }
}
```

### 3. Testing Isolation

```typescript
describe('MyService', () => {
  let factory: RepositoryFactory;

  beforeEach(() => {
    factory = RepositoryFactory.getInstance();
    factory.clear(); // Ensure clean slate
    factory.register('UserRepository', mockUserRepo);
  });

  afterEach(() => {
    factory.clear(); // Clean up
  });
});
```

## Troubleshooting

### Error: "Repository 'XYZ' not found in factory registry"

**Cause**: Repository not registered before being accessed.

**Solution**: Ensure repository is registered in module providers:

```typescript
{
  provide: 'REPOSITORY_FACTORY_SETUP',
  useFactory: (repo: XYZRepository) => {
    RepositoryFactory.getInstance().register('XYZRepository', repo);
  },
  inject: [XYZRepository],
}
```

### Error: "Repository 'XYZ' is already registered"

**Cause**: Attempting to register a repository that already exists.

**Solution**: Use `replace()` instead of `register()`, or check if already registered:

```typescript
const factory = RepositoryFactory.getInstance();
if (!factory.has('XYZRepository')) {
  factory.register('XYZRepository', repo);
}
```

### Circular Dependency Issues

**Cause**: Services and repositories depend on each other.

**Solution**: Use `forwardRef()` or restructure dependencies:

```typescript
@Injectable()
export class UserService {
  constructor(
    @Inject(forwardRef(() => UserRepository))
    private userRepo: UserRepository,
  ) {}
}
```

## Advanced Patterns

### Generic Repository Base Class

```typescript
// repositories/base.repository.ts
import { Repository } from 'typeorm';
import { IRepository, ServiceContext } from '@erp-suite/core';

export abstract class BaseRepositoryImpl<T> implements IRepository<T> {
  constructor(protected readonly ormRepo: Repository<T>) {}

  async findById(ctx: ServiceContext, id: string): Promise<T | null> {
    return this.ormRepo.findOne({
      where: { id, tenantId: ctx.tenantId } as any,
    });
  }

  // Implement common methods once...
}

// Use in specific repositories
export class UserRepository extends BaseRepositoryImpl<User> implements IUserRepository {
  async findByEmail(ctx: ServiceContext, email: string): Promise<User | null> {
    return this.ormRepo.findOne({
      where: { email, tenantId: ctx.tenantId },
    });
  }
}
```

### Repository Composition

```typescript
// Compose multiple repositories
export class OrderService {
  @InjectRepository('OrderRepository')
  private orderRepo: IOrderRepository;

  @InjectRepository('ProductRepository')
  private productRepo: IProductRepository;

  @InjectRepository('CustomerRepository')
  private customerRepo: ICustomerRepository;

  async createOrder(ctx: ServiceContext, data: CreateOrderDto) {
    const customer = await this.customerRepo.findById(ctx, data.customerId);
    const products = await this.productRepo.findMany(ctx, {
      id: In(data.productIds),
    });

    const order = await this.orderRepo.create(ctx, {
      customerId: customer.id,
      items: products.map(p => ({ productId: p.id, quantity: 1 })),
    });

    return order;
  }
}
```

## Resources

- [Dependency Inversion Principle](https://en.wikipedia.org/wiki/Dependency_inversion_principle)
- [Repository Pattern](https://martinfowler.com/eaaCatalog/repository.html)
- [Factory Pattern](https://refactoring.guru/design-patterns/factory-method)
- [ERP-Suite Core Library](/apps/shared-libs/core/README.md)

## Support

For questions or issues:
- Check existing implementations in `/apps/shared-libs/core/`
- Review test files for usage examples
- Open an issue on the project repository
