diff --git a/backend/apps/bff/src/app/__tests__/app.controller.spec.ts b/backend/apps/bff/src/app/__tests__/app.controller.spec.ts deleted file mode 100644 index 5683011..0000000 --- a/backend/apps/bff/src/app/__tests__/app.controller.spec.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { QuoteRequestDto } from '@target/interfaces'; - -import { AppController } from '../app.controller'; -import { QuoteService } from '../services/quote/quote.service'; - -jest.mock('../services/quote/quote.service'); - -describe('AppController', () => { - let appController: AppController; - let quoteService: jest.Mocked; - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - controllers: [AppController], - providers: [QuoteService], - }).compile(); - - appController = module.get(AppController); - quoteService = module.get(QuoteService); - }); - - describe('getQuote', () => { - it('should call quoteService.getQuote with the provided DTO', async () => { - const mockQuoteDto: QuoteRequestDto = { - // Add required properties based on your DTO - rentenzahlungsweise: 'Monatliche Renten', - } as QuoteRequestDto; - const expectedResult = { - basisdaten: { - geburtsdatum: '1990-01-01', - versicherungsbeginn: '2024-01-01', - garantieniveau: '100%', - alterBeiRentenbeginn: 67, - aufschubdauer: 30, - beitragszahlungsdauer: 30 - }, - leistungsmerkmale: { - garantierteMindestrente: 1000, - einmaligesGarantiekapital: 50000, - todesfallleistungAbAltersrentenbezug: 40000 - }, - beitrag: { - einmalbeitrag: 50000, - beitragsdynamik: '3%' - } - }; - - quoteService.getQuote.mockResolvedValue(expectedResult); - - const result = await appController.getQuote(mockQuoteDto); - - expect(quoteService.getQuote).toHaveBeenCalledWith(mockQuoteDto); - expect(result).toBe(expectedResult); - }); - }); -}); diff --git a/backend/apps/bff/src/app/app.controller.ts b/backend/apps/bff/src/app/app.controller.ts deleted file mode 100644 index 5195446..0000000 --- a/backend/apps/bff/src/app/app.controller.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Body, Controller, Post } from '@nestjs/common'; -import { QuoteRequestDto, QuoteResponseDto } from '@target/interfaces'; -import { InputDtoSchema } from '@target/validations'; - -import { ValidationPipe } from './pipes/validation.pipe'; -import { QuoteService } from './services/quote/quote.service'; - -@Controller() -export class AppController { - constructor(private readonly quoteService: QuoteService) {} - - @Post('/quote') - getQuote(@Body(new ValidationPipe(InputDtoSchema)) quoteDto: QuoteRequestDto): Promise { - return this.quoteService.getQuote(quoteDto); - } -} diff --git a/backend/apps/bff/src/app/app.module.ts b/backend/apps/bff/src/app/app.module.ts index 4de312e..d9db422 100644 --- a/backend/apps/bff/src/app/app.module.ts +++ b/backend/apps/bff/src/app/app.module.ts @@ -1,11 +1,8 @@ import { Module } from '@nestjs/common'; -import { AppController } from './app.controller'; -import { QuoteService } from './services/quote/quote.service'; +import { QuoteModule } from '../quote/quote.module'; @Module({ - imports: [], - controllers: [AppController], - providers: [QuoteService], + imports: [QuoteModule], }) export class AppModule {} diff --git a/backend/apps/bff/src/app/services/quote/__tests__/quote.service.spec.ts b/backend/apps/bff/src/app/services/quote/__tests__/quote.service.spec.ts deleted file mode 100644 index 3adec3a..0000000 --- a/backend/apps/bff/src/app/services/quote/__tests__/quote.service.spec.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Test } from '@nestjs/testing'; - -import { QuoteService } from '../quote.service'; - -describe('QuoteService', () => { - let service: QuoteService; - - beforeAll(async () => { - const app = await Test.createTestingModule({ - providers: [QuoteService], - }).compile(); - - service = app.get(QuoteService); - }); - - describe('getQuote', () => { - it('should return quote details for given contribution amount', async () => { - const expectedQuote = { - basisdaten: { - geburtsdatum: '1990-01-01', - versicherungsbeginn: '2025-02-01', - garantieniveau: '90%', - alterBeiRentenbeginn: 67, - aufschubdauer: 30, - beitragszahlungsdauer: 10 - }, - leistungsmerkmale: { - garantierteMindestrente: 50000, - einmaligesGarantiekapital: 500, - todesfallleistungAbAltersrentenbezug: 67 - }, - beitrag: { - einmalbeitrag: 1000, - beitragsdynamik: '1,5%' - } - }; - const result = await service.getQuote({ beitrag: 1000 }); - - expect(result).toEqual(expectedQuote); - }); - }); -}); diff --git a/backend/apps/bff/src/app/services/quote/quote.service.ts b/backend/apps/bff/src/app/services/quote/quote.service.ts deleted file mode 100644 index 2e99452..0000000 --- a/backend/apps/bff/src/app/services/quote/quote.service.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { QuoteRequestDto, QuoteResponseDto } from '@target/interfaces'; - -@Injectable() -export class QuoteService { - async getQuote({ beitrag }: QuoteRequestDto): Promise { - await this.sleep(Math.random() * 4000); // Simulate a real quote service delay 😅 - - return { - basisdaten: { - geburtsdatum: '1990-01-01', - versicherungsbeginn: '2025-02-01', - garantieniveau: '90%', - alterBeiRentenbeginn: 67, - aufschubdauer: 30, - beitragszahlungsdauer: 10 - }, - leistungsmerkmale: { - garantierteMindestrente: beitrag * 50, - einmaligesGarantiekapital: beitrag / 2, - todesfallleistungAbAltersrentenbezug: 67 - }, - beitrag: { - einmalbeitrag: beitrag, - beitragsdynamik: '1,5%' - } - }; - } - - private async sleep(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)); - } -} diff --git a/backend/apps/bff/src/common/common.module.ts b/backend/apps/bff/src/common/common.module.ts new file mode 100644 index 0000000..183bf4f --- /dev/null +++ b/backend/apps/bff/src/common/common.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; + +import { LongStringGeneratorService } from './services/long-string-generator/long-string-generator.service'; +import { TimingService } from './services/timing/timing.service'; + +@Module({ + providers: [LongStringGeneratorService, TimingService], + exports: [LongStringGeneratorService, TimingService], +}) +export class CommonModule {} diff --git a/backend/apps/bff/src/common/services/long-string-generator/long-string-generator.service.spec.ts b/backend/apps/bff/src/common/services/long-string-generator/long-string-generator.service.spec.ts new file mode 100644 index 0000000..2238ca8 --- /dev/null +++ b/backend/apps/bff/src/common/services/long-string-generator/long-string-generator.service.spec.ts @@ -0,0 +1,21 @@ +import { Test, TestingModule } from '@nestjs/testing'; + +import { LongStringGeneratorService } from './long-string-generator.service'; + +describe('LongStringGeneratorService', () => { + let service: LongStringGeneratorService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [LongStringGeneratorService], + }).compile(); + + service = module.get( + LongStringGeneratorService + ); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/backend/apps/bff/src/common/services/long-string-generator/long-string-generator.service.ts b/backend/apps/bff/src/common/services/long-string-generator/long-string-generator.service.ts new file mode 100644 index 0000000..3b27e4f --- /dev/null +++ b/backend/apps/bff/src/common/services/long-string-generator/long-string-generator.service.ts @@ -0,0 +1,9 @@ +import { Injectable } from '@nestjs/common'; +import { randomBytes } from 'crypto'; + +@Injectable() +export class LongStringGeneratorService { + generate(size = 10) { + return randomBytes(size).toString('hex'); + } +} diff --git a/backend/apps/bff/src/common/services/timing/timing.service.spec.ts b/backend/apps/bff/src/common/services/timing/timing.service.spec.ts new file mode 100644 index 0000000..d6c8acb --- /dev/null +++ b/backend/apps/bff/src/common/services/timing/timing.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { TimingService } from './timing.service'; + +describe('TimingService', () => { + let service: TimingService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [TimingService], + }).compile(); + + service = module.get(TimingService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/backend/apps/bff/src/common/services/timing/timing.service.ts b/backend/apps/bff/src/common/services/timing/timing.service.ts new file mode 100644 index 0000000..5c66b5e --- /dev/null +++ b/backend/apps/bff/src/common/services/timing/timing.service.ts @@ -0,0 +1,5 @@ +export class TimingService { + static async sleep(ms: number = 4000): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); + } +} diff --git a/backend/apps/bff/src/quote/controllers/__tests__/quote.controller.spec.ts b/backend/apps/bff/src/quote/controllers/__tests__/quote.controller.spec.ts new file mode 100644 index 0000000..4e02c85 --- /dev/null +++ b/backend/apps/bff/src/quote/controllers/__tests__/quote.controller.spec.ts @@ -0,0 +1,91 @@ +import { fakerEN } from '@faker-js/faker'; +import { Test, TestingModule } from '@nestjs/testing'; +import { QuoteRequestDto } from '@target/interfaces'; + +import { QuoteSchema } from '../../data/schema/quote.schema'; +import { QuoteService } from '../../services/quote/quote.service'; +import { QuoteController } from '../quoteController'; + +describe('QuotaController', () => { + let appController: QuoteController; + let quoteService: jest.Mocked; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [QuoteController], + providers: [ + { + provide: QuoteService, + useValue: { calculateQuote: jest.fn(), createQuote: jest.fn() }, + }, + ], + }).compile(); + + appController = module.get(QuoteController); + quoteService = module.get(QuoteService); + }); + + it('should be defined', () => { + expect(appController).toBeDefined(); + expect(quoteService).toBeDefined(); + }); + + describe('getQuote', () => { + it('should call process quote with a valid birthday', async () => { + const mockQuoteDto: QuoteRequestDto = { + geburtsdatum: '2020-01-01', + leistungsVorgabe: 'Beitrag', + beitrag: 1000, + berechnungDerLaufzeit: 'Alter bei Rentenbeginn', + laufzeit: 10, + beitragszahlungsweise: 'Einmalbeitrag', + rentenzahlungsweise: 'Monatliche Renten', + }; + + const calculatedQuote: Omit = { + basisdaten: { + geburtsdatum: '2020-01-01', + versicherungsbeginn: '2024-01-01', + garantieniveau: '100%', + alterBeiRentenbeginn: 67, + aufschubdauer: 30, + beitragszahlungsdauer: 30, + }, + leistungsmerkmale: { + garantierteMindestrente: 1000, + einmaligesGarantiekapital: 50000, + todesfallleistungAbAltersrentenbezug: 40000, + }, + beitrag: { + einmalbeitrag: 50000, + beitragsdynamik: '3%', + }, + }; + + const expectedQuote = { + ...calculatedQuote, + id: fakerEN.string.alpha({ length: 15 }), + }; + + jest + .spyOn(quoteService, 'calculateQuote') + .mockReturnValueOnce(calculatedQuote); + jest + .spyOn(quoteService, 'createQuote') + .mockResolvedValueOnce(expectedQuote); + + const result = await appController.post(mockQuoteDto); + + expect(quoteService.calculateQuote).toHaveBeenCalledWith({ + beitrag: mockQuoteDto.beitrag, + geburtsdatum: mockQuoteDto.geburtsdatum, + }); + expect(quoteService.calculateQuote).toHaveBeenCalledTimes(1); + + expect(quoteService.createQuote).toHaveBeenCalledWith(calculatedQuote); + expect(quoteService.createQuote).toHaveBeenCalledTimes(1); + + expect(result).toBe(expectedQuote); + }); + }); +}); diff --git a/backend/apps/bff/src/quote/controllers/quoteController.ts b/backend/apps/bff/src/quote/controllers/quoteController.ts new file mode 100644 index 0000000..4a49e0e --- /dev/null +++ b/backend/apps/bff/src/quote/controllers/quoteController.ts @@ -0,0 +1,29 @@ +import { Body, Controller, Get, Param, Post } from '@nestjs/common'; +import { QuoteRequestDto, QuoteResponseDto } from '@target/interfaces'; +import { InputDtoSchema } from '@target/validations'; + +import { QuoteSchema } from '../data/schema/quote.schema'; +import { ValidationPipe } from '../pipes/validation.pipe'; +import { QuoteService } from '../services/quote/quote.service'; + +@Controller('/quote') +export class QuoteController { + constructor(private readonly quoteService: QuoteService) {} + + @Post() + async post( + @Body(new ValidationPipe(InputDtoSchema)) quoteDto: QuoteRequestDto + ): Promise { + const quote = this.quoteService.calculateQuote({ + beitrag: quoteDto.beitrag, + geburtsdatum: quoteDto.geburtsdatum, + }); + + return await this.quoteService.createQuote(quote); + } + + @Get('/:id') + async get(@Param('id') id: string): Promise { + return await this.quoteService.findOnById(id); + } +} diff --git a/backend/apps/bff/src/quote/dao/quote/__tests__/quote.repository.service.spec.ts b/backend/apps/bff/src/quote/dao/quote/__tests__/quote.repository.service.spec.ts new file mode 100644 index 0000000..f858547 --- /dev/null +++ b/backend/apps/bff/src/quote/dao/quote/__tests__/quote.repository.service.spec.ts @@ -0,0 +1,78 @@ +import { fakerEN } from '@faker-js/faker'; +import { Test, TestingModule } from '@nestjs/testing'; + +import { QuoteSchema } from '../../../data/schema/quote.schema'; +import { QuoteStore } from '../../../store/quote.store'; +import { QuoteRepository } from '../quote.repository'; + +describe('QuoteRepositoryService', () => { + let service: QuoteRepository; + let quoteStore: QuoteStore; + + const entityMock: QuoteSchema = { + id: fakerEN.string.alpha({ length: 15 }), + basisdaten: { + geburtsdatum: '1990-01-01', + versicherungsbeginn: '2025-02-01', + garantieniveau: '90%', + alterBeiRentenbeginn: 67, + aufschubdauer: 30, + beitragszahlungsdauer: 10, + }, + leistungsmerkmale: { + garantierteMindestrente: 1000 * 50, + einmaligesGarantiekapital: 1000 / 2, + todesfallleistungAbAltersrentenbezug: 67, + }, + beitrag: { + einmalbeitrag: 1000, + beitragsdynamik: '1,5%', + }, + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + QuoteRepository, + { + provide: QuoteStore, + useValue: { insert: jest.fn(), retrieve: jest.fn() }, + }, + ], + }).compile(); + + service = module.get(QuoteRepository); + quoteStore = module.get(QuoteStore); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + expect(quoteStore).toBeDefined(); + }); + + describe('create', () => { + it('should create quote', async () => { + jest.spyOn(quoteStore, 'insert').mockResolvedValueOnce(entityMock); + + const res = await service.create(entityMock); + + expect(quoteStore.insert).toHaveBeenCalledWith(entityMock); + expect(quoteStore.insert).toHaveBeenCalledTimes(1); + + expect(res).toEqual(entityMock); + }); + }); + + describe('retrieve', () => { + it('should retrieve quote', async () => { + jest.spyOn(quoteStore, 'retrieve').mockResolvedValueOnce(entityMock); + + const res = await service.findOneById(entityMock.id); + + expect(quoteStore.retrieve).toHaveBeenCalledWith(entityMock.id); + expect(quoteStore.retrieve).toHaveBeenCalledTimes(1); + + expect(res).toEqual(entityMock); + }); + }); +}); diff --git a/backend/apps/bff/src/quote/dao/quote/quote.repository.ts b/backend/apps/bff/src/quote/dao/quote/quote.repository.ts new file mode 100644 index 0000000..ec9bc8e --- /dev/null +++ b/backend/apps/bff/src/quote/dao/quote/quote.repository.ts @@ -0,0 +1,17 @@ +import { Injectable } from '@nestjs/common'; + +import { QuoteSchema } from '../../data/schema/quote.schema'; +import { QuoteStore } from '../../store/quote.store'; + +@Injectable() +export class QuoteRepository { + constructor(private quotaStore: QuoteStore) {} + + async create(entity: QuoteSchema): Promise { + return await this.quotaStore.insert(entity); + } + + async findOneById(id: string): Promise { + return await this.quotaStore.retrieve(id); + } +} diff --git a/backend/apps/bff/src/quote/data/mock/quote.mock.ts b/backend/apps/bff/src/quote/data/mock/quote.mock.ts new file mode 100644 index 0000000..cb2af12 --- /dev/null +++ b/backend/apps/bff/src/quote/data/mock/quote.mock.ts @@ -0,0 +1,19 @@ +export const quoteMock = { + basisdaten: { + geburtsdatum: '1990-01-01', + versicherungsbeginn: '2025-02-01', + garantieniveau: '90%', + alterBeiRentenbeginn: 67, + aufschubdauer: 30, + beitragszahlungsdauer: 10, + }, + leistungsmerkmale: { + garantierteMindestrente: 1000 * 50, + einmaligesGarantiekapital: 1000 / 2, + todesfallleistungAbAltersrentenbezug: 67, + }, + beitrag: { + einmalbeitrag: 1000, + beitragsdynamik: '1,5%', + }, +}; diff --git a/backend/apps/bff/src/quote/data/schema/quote.schema.ts b/backend/apps/bff/src/quote/data/schema/quote.schema.ts new file mode 100644 index 0000000..9a6a77d --- /dev/null +++ b/backend/apps/bff/src/quote/data/schema/quote.schema.ts @@ -0,0 +1,5 @@ +import { QuoteResponseDto } from '@target/interfaces'; + +export interface QuoteSchema extends QuoteResponseDto { + id?: string; +} diff --git a/backend/apps/bff/src/app/pipes/validation.pipe.ts b/backend/apps/bff/src/quote/pipes/validation.pipe.ts similarity index 100% rename from backend/apps/bff/src/app/pipes/validation.pipe.ts rename to backend/apps/bff/src/quote/pipes/validation.pipe.ts diff --git a/backend/apps/bff/src/quote/quote.module.ts b/backend/apps/bff/src/quote/quote.module.ts new file mode 100644 index 0000000..dd3d57f --- /dev/null +++ b/backend/apps/bff/src/quote/quote.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; + +import { CommonModule } from '../common/common.module'; +import { QuoteController } from './controllers/quoteController'; +import { QuoteRepository } from './dao/quote/quote.repository'; +import { QuoteService } from './services/quote/quote.service'; +import { QuoteStore } from './store/quote.store'; + +@Module({ + imports: [CommonModule], + controllers: [QuoteController], + providers: [QuoteRepository, QuoteService, QuoteStore], +}) +export class QuoteModule {} diff --git a/backend/apps/bff/src/quote/services/quote/__tests__/quote.service.spec.ts b/backend/apps/bff/src/quote/services/quote/__tests__/quote.service.spec.ts new file mode 100644 index 0000000..b3ebe22 --- /dev/null +++ b/backend/apps/bff/src/quote/services/quote/__tests__/quote.service.spec.ts @@ -0,0 +1,139 @@ +import { fakerEN } from '@faker-js/faker'; +import { Test } from '@nestjs/testing'; + +import { LongStringGeneratorService } from '../../../../common/services/long-string-generator/long-string-generator.service'; +import { QuoteRepository } from '../../../dao/quote/quote.repository'; +import { QuoteService } from '../quote.service'; + +describe('QuoteService', () => { + let service: QuoteService; + let quoteRepository: QuoteRepository; + let longStringGeneratorService: LongStringGeneratorService; + + const entityMock = { + basisdaten: { + geburtsdatum: '2000-01-01', + versicherungsbeginn: '2025-02-01', + garantieniveau: '90%', + alterBeiRentenbeginn: 67, + aufschubdauer: 30, + beitragszahlungsdauer: 10, + }, + leistungsmerkmale: { + garantierteMindestrente: 5000, + einmaligesGarantiekapital: 500, + todesfallleistungAbAltersrentenbezug: 67, + }, + beitrag: { + einmalbeitrag: 1000, + beitragsdynamik: '1,5%', + }, + }; + + beforeEach(async () => { + const app = await Test.createTestingModule({ + providers: [ + QuoteService, + { + provide: QuoteRepository, + useValue: { create: jest.fn(), findOneById: jest.fn() }, + }, + { + provide: LongStringGeneratorService, + useValue: { generate: jest.fn() }, + }, + ], + }).compile(); + + service = app.get(QuoteService); + quoteRepository = app.get(QuoteRepository); + longStringGeneratorService = app.get( + LongStringGeneratorService + ); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + expect(quoteRepository).toBeDefined(); + expect(longStringGeneratorService).toBeDefined(); + }); + + describe('findOnById', () => { + it('should return quote details for given quote id', async () => { + const id = fakerEN.string.alpha({ length: 15 }); + + const expectedQuote = { + id, + ...entityMock, + }; + + jest + .spyOn(quoteRepository, 'findOneById') + .mockResolvedValue(expectedQuote); + + const result = await service.findOnById(id); + + expect(quoteRepository.findOneById).toHaveBeenCalledWith(id); + expect(quoteRepository.findOneById).toHaveBeenCalledTimes(1); + + expect(result).toEqual(expectedQuote); + }); + }); + + describe('createQuote', () => { + it('should save quote details from a quote schema', async () => { + const id = fakerEN.string.alpha({ length: 15 }); + + const expectedQuote = { + id, + ...entityMock, + }; + + jest.spyOn(quoteRepository, 'create').mockResolvedValue(expectedQuote); + jest.spyOn(longStringGeneratorService, 'generate').mockReturnValue(id); + + const result = await service.createQuote(entityMock); + + expect(quoteRepository.create).toHaveBeenCalledWith(expectedQuote); + expect(quoteRepository.create).toHaveBeenCalledTimes(1); + + expect(longStringGeneratorService.generate).toHaveBeenCalledTimes(1); + + expect(result).toEqual(expectedQuote); + }); + }); + + describe('calculateQuote', () => { + it('should calculate quote from initial params', async () => { + const geburtsdatum = '2000-01-01'; + const beitrag = 1000; + + const entityMock = { + basisdaten: { + geburtsdatum: geburtsdatum, + versicherungsbeginn: '2025-02-01', + garantieniveau: '90%', + alterBeiRentenbeginn: 67, + aufschubdauer: 30, + beitragszahlungsdauer: 10, + }, + leistungsmerkmale: { + garantierteMindestrente: beitrag * 50, + einmaligesGarantiekapital: beitrag / 2, + todesfallleistungAbAltersrentenbezug: 67, + }, + beitrag: { + einmalbeitrag: beitrag, + beitragsdynamik: '1,5%', + }, + }; + + const result = service.calculateQuote({ + beitrag, + geburtsdatum, + }); + + expect(result).toEqual(entityMock); + }); + }); +}); diff --git a/backend/apps/bff/src/quote/services/quote/quote.service.ts b/backend/apps/bff/src/quote/services/quote/quote.service.ts new file mode 100644 index 0000000..2f12c86 --- /dev/null +++ b/backend/apps/bff/src/quote/services/quote/quote.service.ts @@ -0,0 +1,54 @@ +import { HttpException, Injectable, NotFoundException } from '@nestjs/common'; +import { QuoteRequestDto } from '@target/interfaces'; + +import { LongStringGeneratorService } from '../../../common/services/long-string-generator/long-string-generator.service'; +import { QuoteRepository } from '../../dao/quote/quote.repository'; +import { quoteMock } from '../../data/mock/quote.mock'; +import { QuoteSchema } from '../../data/schema/quote.schema'; + +@Injectable() +export class QuoteService { + constructor( + private quoteRepository: QuoteRepository, + private longStringGeneratorService: LongStringGeneratorService + ) {} + + calculateQuote({ + beitrag, + geburtsdatum, + }: QuoteRequestDto): Omit { + return { + basisdaten: { ...quoteMock.basisdaten, geburtsdatum: geburtsdatum }, + leistungsmerkmale: { + ...quoteMock.leistungsmerkmale, + garantierteMindestrente: beitrag * 50, + einmaligesGarantiekapital: beitrag / 2, + }, + beitrag: { + ...quoteMock.beitrag, + einmalbeitrag: beitrag, + }, + } as Omit; + } + + async createQuote(quote: Omit): Promise { + try { + const _quote = { + ...quote, + id: this.longStringGeneratorService.generate(), + }; + + return await this.quoteRepository.create(_quote); + } catch (entityInsertError) { + throw new HttpException(entityInsertError.message, entityInsertError); + } + } + + async findOnById(id: string): Promise { + try { + return await this.quoteRepository.findOneById(id); + } catch (entityFindError) { + throw new NotFoundException(entityFindError.message); + } + } +} diff --git a/backend/apps/bff/src/quote/store/__tests__/quote.store.spec.ts b/backend/apps/bff/src/quote/store/__tests__/quote.store.spec.ts new file mode 100644 index 0000000..a56e79b --- /dev/null +++ b/backend/apps/bff/src/quote/store/__tests__/quote.store.spec.ts @@ -0,0 +1,60 @@ +import { fakerEN } from '@faker-js/faker'; +import { Test, TestingModule } from '@nestjs/testing'; + +import { QuoteSchema } from '../../data/schema/quote.schema'; +import { QuoteStore } from '../quote.store'; + +describe('QuoteStore', () => { + let service: QuoteStore; + + const entityMock: QuoteSchema = { + id: fakerEN.string.alpha({ length: 15 }), + basisdaten: { + geburtsdatum: '1990-01-01', + versicherungsbeginn: '2025-02-01', + garantieniveau: '90%', + alterBeiRentenbeginn: 67, + aufschubdauer: 30, + beitragszahlungsdauer: 10, + }, + leistungsmerkmale: { + garantierteMindestrente: 50000, + einmaligesGarantiekapital: 500, + todesfallleistungAbAltersrentenbezug: 67, + }, + beitrag: { + einmalbeitrag: 1000, + beitragsdynamik: '1,5%', + }, + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [QuoteStore], + }).compile(); + + service = module.get(QuoteStore); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('insert', () => { + it('should create quote', async () => { + const res = await service.insert(entityMock); + + expect(res).toEqual(entityMock); + }); + }); + + describe('retrieve', () => { + it('should get quote', async () => { + service['quotaStorage'].set(entityMock.id, entityMock); + + const res = await service.retrieve(entityMock.id); + + expect(res).toEqual(entityMock); + }); + }); +}); diff --git a/backend/apps/bff/src/quote/store/quote.store.ts b/backend/apps/bff/src/quote/store/quote.store.ts new file mode 100644 index 0000000..ed0658d --- /dev/null +++ b/backend/apps/bff/src/quote/store/quote.store.ts @@ -0,0 +1,23 @@ +import { Injectable } from '@nestjs/common'; + +import { TimingService } from '../../common/services/timing/timing.service'; +import { QuoteSchema } from '../data/schema/quote.schema'; + +@Injectable() +export class QuoteStore { + private quotaStorage = new Map(); + + async insert(entity: QuoteSchema): Promise { + const _entity = this.quotaStorage.set(entity.id, entity); + + await TimingService.sleep(); + + return _entity.get(entity.id); + } + + async retrieve(id: string): Promise { + await TimingService.sleep(); + + return this.quotaStorage.get(id); + } +} diff --git a/eslint.config.cjs b/eslint.config.cjs index 0a97e3a..3495815 100644 --- a/eslint.config.cjs +++ b/eslint.config.cjs @@ -38,13 +38,7 @@ module.exports = [ ], }, ], - 'padding-line-between-statements': [ - 'error', - { blankLine: 'always', prev: 'const', next: '*' }, - { blankLine: 'never', prev: 'const', next: 'const' }, - { blankLine: 'always', prev: 'let', next: '*' }, - { blankLine: 'never', prev: 'let', next: 'let' }, - ], + 'padding-line-between-statements': 0, '@typescript-eslint/ban-ts-comment': 0, '@typescript-eslint/ban-types': 0, '@typescript-eslint/default-param-last': 0, diff --git a/frontend/apps/shell/src/app/app.routes.ts b/frontend/apps/shell/src/app/app.routes.ts index f60d3a3..7b3bd0d 100644 --- a/frontend/apps/shell/src/app/app.routes.ts +++ b/frontend/apps/shell/src/app/app.routes.ts @@ -1,8 +1,11 @@ import { Route } from '@angular/router'; import { InputLibComponent } from '@target/input-lib'; +import { quoteViewResolver } from '@target/quote-resolver-lib'; +import { ViewLibComponent } from '@target/view-lib'; const ROUTES = { INPUTS: 'inputs', + VIEW: 'view/:id', }; export const appRoutes: Route[] = [ @@ -15,6 +18,11 @@ export const appRoutes: Route[] = [ path: ROUTES.INPUTS, component: InputLibComponent, }, + { + path: ROUTES.VIEW, + component: ViewLibComponent, + resolve: [quoteViewResolver], + }, { path: '**', redirectTo: ROUTES.INPUTS, diff --git a/frontend/libs/date-picker-lib/README.md b/frontend/libs/date-picker-lib/README.md new file mode 100644 index 0000000..c4a547e --- /dev/null +++ b/frontend/libs/date-picker-lib/README.md @@ -0,0 +1,7 @@ +# date-picker-lib + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test date-picker-lib` to execute the unit tests. diff --git a/frontend/libs/date-picker-lib/eslint.config.cjs b/frontend/libs/date-picker-lib/eslint.config.cjs new file mode 100644 index 0000000..39a2d94 --- /dev/null +++ b/frontend/libs/date-picker-lib/eslint.config.cjs @@ -0,0 +1,34 @@ +const nx = require('@nx/eslint-plugin'); +const baseConfig = require('../../../eslint.config.cjs'); + +module.exports = [ + ...baseConfig, + ...nx.configs['flat/angular'], + ...nx.configs['flat/angular-template'], + { + files: ['**/*.ts'], + rules: { + '@angular-eslint/directive-selector': [ + 'error', + { + type: 'attribute', + prefix: 'lib', + style: 'camelCase', + }, + ], + '@angular-eslint/component-selector': [ + 'error', + { + type: 'element', + prefix: 'lib', + style: 'kebab-case', + }, + ], + }, + }, + { + files: ['**/*.html'], + // Override or add rules here + rules: {}, + }, +]; diff --git a/frontend/libs/date-picker-lib/jest.config.ts b/frontend/libs/date-picker-lib/jest.config.ts new file mode 100644 index 0000000..cff5969 --- /dev/null +++ b/frontend/libs/date-picker-lib/jest.config.ts @@ -0,0 +1,21 @@ +export default { + displayName: 'date-picker-lib', + preset: '../../../jest.preset.js', + setupFilesAfterEnv: ['/src/test-setup.ts'], + coverageDirectory: '../../../coverage/frontend/libs/date-picker-lib', + transform: { + '^.+\\.(ts|mjs|js|html)$': [ + 'jest-preset-angular', + { + tsconfig: '/tsconfig.spec.json', + stringifyContentPathRegex: '\\.(html|svg)$', + }, + ], + }, + transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'], + snapshotSerializers: [ + 'jest-preset-angular/build/serializers/no-ng-attributes', + 'jest-preset-angular/build/serializers/ng-snapshot', + 'jest-preset-angular/build/serializers/html-comment', + ], +}; diff --git a/frontend/libs/date-picker-lib/project.json b/frontend/libs/date-picker-lib/project.json new file mode 100644 index 0000000..acdd697 --- /dev/null +++ b/frontend/libs/date-picker-lib/project.json @@ -0,0 +1,20 @@ +{ + "name": "date-picker-lib", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "frontend/libs/date-picker-lib/src", + "prefix": "lib", + "projectType": "library", + "tags": ["ui"], + "targets": { + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "frontend/libs/date-picker-lib/jest.config.ts" + } + }, + "lint": { + "executor": "@nx/eslint:lint" + } + } +} diff --git a/frontend/libs/date-picker-lib/src/index.ts b/frontend/libs/date-picker-lib/src/index.ts new file mode 100644 index 0000000..2baf6e9 --- /dev/null +++ b/frontend/libs/date-picker-lib/src/index.ts @@ -0,0 +1 @@ +export * from './lib/date-picker-lib/datepicker.component' diff --git a/frontend/libs/date-picker-lib/src/lib/date-picker-lib/__tests__/datepicker.component.spec.ts b/frontend/libs/date-picker-lib/src/lib/date-picker-lib/__tests__/datepicker.component.spec.ts new file mode 100644 index 0000000..f659110 --- /dev/null +++ b/frontend/libs/date-picker-lib/src/lib/date-picker-lib/__tests__/datepicker.component.spec.ts @@ -0,0 +1,70 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import moment from 'moment/moment'; + +import { DatepickerComponent } from '../datepicker.component'; + +describe('DatepickerComponent', () => { + let component: DatepickerComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DatepickerComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(DatepickerComponent); + component = fixture.componentInstance; + + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should have default empty value', () => { + expect(component.value).toBe(''); + }); + + it('should set min date to 100 years ago', () => { + const expectedMinDate = moment().subtract(100, 'years').startOf('day'); + const componentMinDate = component['minDate'].startOf('day'); + + expect(componentMinDate.isSame(expectedMinDate, 'day')).toBeTruthy(); + }); + + it('should set max date to 18 years ago', () => { + const expectedMaxDate = moment().subtract(18, 'years').startOf('day'); + const componentMaxDate = component['maxDate'].startOf('day'); + + expect(componentMaxDate.isSame(expectedMaxDate, 'day')).toBeTruthy(); + }); + + it('should call onChange when value changes', () => { + const date = '2000-01-01'; + + const onChangeMock = jest.fn(); + component.registerOnChange(onChangeMock); + + component.value = moment(date); + + expect(onChangeMock).toHaveBeenCalledWith(date); + expect(component.value).toBe(date); + }); + + it('should handle writeValue correctly', () => { + const date = '2001-02-03'; + component.writeValue(moment(date)); + + expect(component.value).toBe(date); + }); + + it('should call onTouched when value changes', () => { + const onTouchedMock = jest.fn(); + component.registerOnTouched(onTouchedMock); + + component.value = moment('2000-01-01'); + + expect(onTouchedMock).toHaveBeenCalled(); + }); +}); diff --git a/frontend/libs/date-picker-lib/src/lib/date-picker-lib/datepicker.component.html b/frontend/libs/date-picker-lib/src/lib/date-picker-lib/datepicker.component.html new file mode 100644 index 0000000..794cfb6 --- /dev/null +++ b/frontend/libs/date-picker-lib/src/lib/date-picker-lib/datepicker.component.html @@ -0,0 +1,17 @@ + + + MM/DD/YYYY + Please enter a valid date. + + + diff --git a/frontend/libs/date-picker-lib/src/lib/date-picker-lib/datepicker.component.ts b/frontend/libs/date-picker-lib/src/lib/date-picker-lib/datepicker.component.ts new file mode 100644 index 0000000..28b4794 --- /dev/null +++ b/frontend/libs/date-picker-lib/src/lib/date-picker-lib/datepicker.component.ts @@ -0,0 +1,86 @@ +import { CommonModule } from '@angular/common'; +import { + ChangeDetectionStrategy, + Component, + forwardRef, + inject, +} from '@angular/core'; +import { + ControlValueAccessor, + FormsModule, + NG_VALUE_ACCESSOR, +} from '@angular/forms'; +import { NxErrorComponent } from '@aposin/ng-aquila/base'; +import { + NxDateAdapter, + NxDatefieldDirective, + NxDatepickerComponent, + NxDatepickerToggleComponent, +} from '@aposin/ng-aquila/datefield'; +import { NxFormfieldComponent } from '@aposin/ng-aquila/formfield'; +import { NxInputModule } from '@aposin/ng-aquila/input'; +import { NxMomentDateModule } from '@aposin/ng-aquila/moment-date-adapter'; +import moment, { Moment } from 'moment/moment'; + +@Component({ + selector: 'lib-date-picker', + standalone: true, + imports: [ + CommonModule, + NxInputModule, + NxMomentDateModule, + NxDatefieldDirective, + NxDatepickerComponent, + NxDatepickerToggleComponent, + NxErrorComponent, + NxFormfieldComponent, + FormsModule, + ], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DatepickerComponent), + multi: true, + }, + ], + templateUrl: './datepicker.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class DatepickerComponent implements ControlValueAccessor { + protected readonly adapter = inject(NxDateAdapter); + + protected minDate = moment().subtract(100, 'years'); + protected maxDate = moment().subtract(18, 'years'); + + private _value: string = ''; + + private onChange: any = () => {}; + private onTouched: any = () => {}; + + get value(): string { + return this._value; + } + + set value(val: Moment) { + this._value = val?.format('YYYY-MM-DD'); + + this.onChange(val?.format('YYYY-MM-DD')); + this.onTouched(); + } + + writeValue(value: Moment): void { + if (value) { + this._value = value.format('YYYY-MM-DD'); + } + } + + registerOnChange(fn: any): void { + this.onChange = fn; + } + + registerOnTouched(fn: any): void { + this.onTouched = fn; + } + + setDisabledState?(_: boolean): void {} +} diff --git a/frontend/libs/date-picker-lib/src/test-setup.ts b/frontend/libs/date-picker-lib/src/test-setup.ts new file mode 100644 index 0000000..ea41401 --- /dev/null +++ b/frontend/libs/date-picker-lib/src/test-setup.ts @@ -0,0 +1,6 @@ +import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone'; + +setupZoneTestEnv({ + errorOnUnknownElements: true, + errorOnUnknownProperties: true, +}); diff --git a/frontend/libs/date-picker-lib/tsconfig.json b/frontend/libs/date-picker-lib/tsconfig.json new file mode 100644 index 0000000..866de3d --- /dev/null +++ b/frontend/libs/date-picker-lib/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "target": "es2022", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "esModuleInterop": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "extends": "../../../tsconfig.base.json", + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/frontend/libs/date-picker-lib/tsconfig.lib.json b/frontend/libs/date-picker-lib/tsconfig.lib.json new file mode 100644 index 0000000..9b49be7 --- /dev/null +++ b/frontend/libs/date-picker-lib/tsconfig.lib.json @@ -0,0 +1,17 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "declaration": true, + "declarationMap": true, + "inlineSources": true, + "types": [] + }, + "exclude": [ + "src/**/*.spec.ts", + "src/test-setup.ts", + "jest.config.ts", + "src/**/*.test.ts" + ], + "include": ["src/**/*.ts"] +} diff --git a/frontend/libs/date-picker-lib/tsconfig.spec.json b/frontend/libs/date-picker-lib/tsconfig.spec.json new file mode 100644 index 0000000..f858ef7 --- /dev/null +++ b/frontend/libs/date-picker-lib/tsconfig.spec.json @@ -0,0 +1,16 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "module": "commonjs", + "target": "es2016", + "types": ["jest", "node"] + }, + "files": ["src/test-setup.ts"], + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/frontend/libs/error-box-lib/README.md b/frontend/libs/error-box-lib/README.md new file mode 100644 index 0000000..5cfb36d --- /dev/null +++ b/frontend/libs/error-box-lib/README.md @@ -0,0 +1,7 @@ +# error-box-lib + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test error-box-lib` to execute the unit tests. diff --git a/frontend/libs/error-box-lib/eslint.config.cjs b/frontend/libs/error-box-lib/eslint.config.cjs new file mode 100644 index 0000000..39a2d94 --- /dev/null +++ b/frontend/libs/error-box-lib/eslint.config.cjs @@ -0,0 +1,34 @@ +const nx = require('@nx/eslint-plugin'); +const baseConfig = require('../../../eslint.config.cjs'); + +module.exports = [ + ...baseConfig, + ...nx.configs['flat/angular'], + ...nx.configs['flat/angular-template'], + { + files: ['**/*.ts'], + rules: { + '@angular-eslint/directive-selector': [ + 'error', + { + type: 'attribute', + prefix: 'lib', + style: 'camelCase', + }, + ], + '@angular-eslint/component-selector': [ + 'error', + { + type: 'element', + prefix: 'lib', + style: 'kebab-case', + }, + ], + }, + }, + { + files: ['**/*.html'], + // Override or add rules here + rules: {}, + }, +]; diff --git a/frontend/libs/error-box-lib/jest.config.ts b/frontend/libs/error-box-lib/jest.config.ts new file mode 100644 index 0000000..6425c1a --- /dev/null +++ b/frontend/libs/error-box-lib/jest.config.ts @@ -0,0 +1,21 @@ +export default { + displayName: 'error-box-lib', + preset: '../../../jest.preset.js', + setupFilesAfterEnv: ['/src/test-setup.ts'], + coverageDirectory: '../../../coverage/frontend/libs/error-box-lib', + transform: { + '^.+\\.(ts|mjs|js|html)$': [ + 'jest-preset-angular', + { + tsconfig: '/tsconfig.spec.json', + stringifyContentPathRegex: '\\.(html|svg)$', + }, + ], + }, + transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'], + snapshotSerializers: [ + 'jest-preset-angular/build/serializers/no-ng-attributes', + 'jest-preset-angular/build/serializers/ng-snapshot', + 'jest-preset-angular/build/serializers/html-comment', + ], +}; diff --git a/frontend/libs/error-box-lib/project.json b/frontend/libs/error-box-lib/project.json new file mode 100644 index 0000000..92767a5 --- /dev/null +++ b/frontend/libs/error-box-lib/project.json @@ -0,0 +1,20 @@ +{ + "name": "error-box-lib", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "frontend/libs/error-box-lib/src", + "prefix": "lib", + "projectType": "library", + "tags": ["ui"], + "targets": { + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "frontend/libs/error-box-lib/jest.config.ts" + } + }, + "lint": { + "executor": "@nx/eslint:lint" + } + } +} diff --git a/frontend/libs/error-box-lib/src/index.ts b/frontend/libs/error-box-lib/src/index.ts new file mode 100644 index 0000000..9fa9e98 --- /dev/null +++ b/frontend/libs/error-box-lib/src/index.ts @@ -0,0 +1 @@ +export * from './lib/error-box-lib/error-box.component'; diff --git a/frontend/libs/error-box-lib/src/lib/error-box-lib/__tests__/error-box.component.spec.ts b/frontend/libs/error-box-lib/src/lib/error-box-lib/__tests__/error-box.component.spec.ts new file mode 100644 index 0000000..bc20021 --- /dev/null +++ b/frontend/libs/error-box-lib/src/lib/error-box-lib/__tests__/error-box.component.spec.ts @@ -0,0 +1,46 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ErrorBoxComponent } from '../error-box.component'; + +describe('ErrorBoxComponent', () => { + let component: ErrorBoxComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ErrorBoxComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(ErrorBoxComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should set simple server error message correctly', () => { + const error = { statusCode: 500, message: 'An error occurred' }; + const expected = { statusCode: 500, message: ['An error occurred'] }; + + component.errorMessage = error; + + expect(component['apiErrorMessage']()).toEqual(expected); + }); + + it('should set composed server error message correctly', () => { + const error = { + statusCode: 500, + message: ['field1: error 1', 'field2: error 2'], + }; + const expected = { + statusCode: 500, + message: ['field1: error 1', 'field2: error 2'], + }; + + component.errorMessage = error; + + expect(component['apiErrorMessage']()).toEqual(expected); + }); +}); diff --git a/frontend/libs/error-box-lib/src/lib/error-box-lib/error-box.component.html b/frontend/libs/error-box-lib/src/lib/error-box-lib/error-box.component.html new file mode 100644 index 0000000..34a03bc --- /dev/null +++ b/frontend/libs/error-box-lib/src/lib/error-box-lib/error-box.component.html @@ -0,0 +1,10 @@ +@if (apiErrorMessage().message.length) { + + The following errors have been reported: +
+
+ {{ item }} +
+
+
+} diff --git a/frontend/libs/error-box-lib/src/lib/error-box-lib/error-box.component.ts b/frontend/libs/error-box-lib/src/lib/error-box-lib/error-box.component.ts new file mode 100644 index 0000000..eba9385 --- /dev/null +++ b/frontend/libs/error-box-lib/src/lib/error-box-lib/error-box.component.ts @@ -0,0 +1,37 @@ +import { CommonModule } from '@angular/common'; +import { + ChangeDetectionStrategy, + Component, + Input, + signal, +} from '@angular/core'; +import { NxErrorComponent } from '@aposin/ng-aquila/base'; +import { NxFormfieldErrorDirective } from '@aposin/ng-aquila/formfield'; +import { ApiError, ApiErrorResponse } from '@target/interfaces'; + +@Component({ + selector: 'lib-error-box', + imports: [CommonModule, NxErrorComponent, NxFormfieldErrorDirective], + templateUrl: './error-box.component.html', + standalone: true, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ErrorBoxComponent { + @Input() + set errorMessage(value: ApiErrorResponse) { + if (!value) { + this.apiErrorMessage.set({ message: [] } as ApiError); + } + + const error: ApiError = Array.isArray(value?.message) + ? (value as ApiError) + : { + ...value, + message: value?.message ? [value.message] : [], + }; + + this.apiErrorMessage.set(error); + } + + protected apiErrorMessage = signal({ message: [] } as ApiError); +} diff --git a/frontend/libs/error-box-lib/src/test-setup.ts b/frontend/libs/error-box-lib/src/test-setup.ts new file mode 100644 index 0000000..ea41401 --- /dev/null +++ b/frontend/libs/error-box-lib/src/test-setup.ts @@ -0,0 +1,6 @@ +import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone'; + +setupZoneTestEnv({ + errorOnUnknownElements: true, + errorOnUnknownProperties: true, +}); diff --git a/frontend/libs/error-box-lib/tsconfig.json b/frontend/libs/error-box-lib/tsconfig.json new file mode 100644 index 0000000..fde35ea --- /dev/null +++ b/frontend/libs/error-box-lib/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "es2022", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "extends": "../../../tsconfig.base.json", + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/frontend/libs/error-box-lib/tsconfig.lib.json b/frontend/libs/error-box-lib/tsconfig.lib.json new file mode 100644 index 0000000..9b49be7 --- /dev/null +++ b/frontend/libs/error-box-lib/tsconfig.lib.json @@ -0,0 +1,17 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "declaration": true, + "declarationMap": true, + "inlineSources": true, + "types": [] + }, + "exclude": [ + "src/**/*.spec.ts", + "src/test-setup.ts", + "jest.config.ts", + "src/**/*.test.ts" + ], + "include": ["src/**/*.ts"] +} diff --git a/frontend/libs/error-box-lib/tsconfig.spec.json b/frontend/libs/error-box-lib/tsconfig.spec.json new file mode 100644 index 0000000..f858ef7 --- /dev/null +++ b/frontend/libs/error-box-lib/tsconfig.spec.json @@ -0,0 +1,16 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "module": "commonjs", + "target": "es2016", + "types": ["jest", "node"] + }, + "files": ["src/test-setup.ts"], + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/frontend/libs/input-lib/src/index.ts b/frontend/libs/input-lib/src/index.ts index 836bf52..e3d8778 100644 --- a/frontend/libs/input-lib/src/index.ts +++ b/frontend/libs/input-lib/src/index.ts @@ -1,3 +1 @@ -import { InputLibComponent } from "./lib/input-lib/input-lib.component"; - -export { InputLibComponent }; +export * from './lib/input-lib/input-lib.component'; diff --git a/frontend/libs/input-lib/src/lib/input-lib/__tests__/input-lib.component.spec.ts b/frontend/libs/input-lib/src/lib/input-lib/__tests__/input-lib.component.spec.ts index e36e65e..a30c689 100644 --- a/frontend/libs/input-lib/src/lib/input-lib/__tests__/input-lib.component.spec.ts +++ b/frontend/libs/input-lib/src/lib/input-lib/__tests__/input-lib.component.spec.ts @@ -1,135 +1,81 @@ -import { HttpClient, provideHttpClient } from '@angular/common/http'; +import { provideHttpClient } from '@angular/common/http'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { of } from 'rxjs'; +import { ReactiveFormsModule } from '@angular/forms'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { Router } from '@angular/router'; +import { InputStore } from '@target/input-store-lib'; +import { QuoteService } from '@target/service-lib'; -import { InputLibComponent } from '../input-lib.component'; -import { InputStore } from '../store/input.store'; -import { Input, InputState } from '../store/input.store.interfaces'; -import { QuoteService } from '../store/services/quote.service'; - -jest.mock('../store/input.store'); -jest.mock('../store/services/quote.service'); +import { InputLibComponent } from '../../../index'; describe('InputLibComponent', () => { let component: InputLibComponent; let fixture: ComponentFixture; - let inputStore: any; - let _quoteService: QuoteService; + + // @ts-ignore + let consoleSpy: any; + + const mockInputStore = { + uiState: jest.fn().mockReturnValue({ + geburtsdatum: { value: '' }, + leistungsVorgabe: { value: '' }, + beitrag: { value: '' }, + berechnungDerLaufzeit: { value: '' }, + laufzeit: { value: '' }, + beitragszahlungsweise: { value: '' }, + rentenzahlungsweise: { value: '' }, + }), + updateInputs: jest.fn().mockResolvedValue(undefined), + calculate: jest.fn().mockResolvedValue('1234'), + processErrors: jest.fn(), + }; + + const mockRouter = { + navigate: jest.fn().mockResolvedValue(true), + }; beforeEach(async () => { - const mockHttp = { - post: jest.fn() - } as Partial; - const mockQuoteService = { - calculateQuote: jest.fn().mockReturnValue(of({ - basisdaten: { - geburtsdatum: '1990-01-01', - versicherungsbeginn: '2024-01-01', - garantieniveau: '90%', - alterBeiRentenbeginn: 67, - aufschubdauer: 30, - beitragszahlungsdauer: 30 - }, - leistungsmerkmale: { - garantierteMindestrente: 1000, - einmaligesGarantiekapital: 100000, - todesfallleistungAbAltersrentenbezug: 50000 - }, - beitrag: { - einmalbeitrag: 50000, - beitragsdynamik: '3%' + // Prevent 'Could not parse CSS stylesheet' exception from running the test. Apparently a bug with nx-spinner css library + consoleSpy = jest + .spyOn(global.console, 'error') + .mockImplementation((message) => { + if (!message.toString().includes('Could not parse CSS stylesheet')) { + global.console.warn(message); } - })), - http: mockHttp as HttpClient - }; - const mockState: InputState = { - leistungsVorgabe: { value: 'Beitrag', valid: true, error: null }, - beitrag: { value: 1000, valid: true, error: null }, - berechnungDerLaufzeit: { value: 'Alter bei Rentenbeginn', valid: true, error: null }, - laufzeit: { value: 10, valid: true, error: null }, - beitragszahlungsweise: { value: 'Einmalbeitrag', valid: true, error: null }, - rentenzahlungsweise: { value: 'Monatliche Renten', valid: true, error: null }, - quote: { - basisdaten: { - geburtsdatum: '', - versicherungsbeginn: '', - garantieniveau: '', - alterBeiRentenbeginn: 0, - aufschubdauer: 0, - beitragszahlungsdauer: 0 - }, - leistungsmerkmale: { - garantierteMindestrente: 0, - einmaligesGarantiekapital: 0, - todesfallleistungAbAltersrentenbezug: 0 - }, - beitrag: { - einmalbeitrag: 0, - beitragsdynamik: '' - }, - }, - }; + }); await TestBed.configureTestingModule({ - imports: [InputLibComponent], + imports: [ReactiveFormsModule, NoopAnimationsModule, InputLibComponent], providers: [ provideHttpClient(), - { provide: QuoteService, useValue: mockQuoteService }, - { provide: InputStore, useValue: { - updateInputs: jest.fn(), - calculate: jest.fn(), - uiState: jest.fn(() => mockState) - }} - ] + { + provide: QuoteService, + useValue: { calculateQuote: jest.fn(), fetchQuote: jest.fn() }, + }, + { + provide: InputStore, + useValue: mockInputStore, + }, + { + provide: Router, + useValue: mockRouter, + }, + ], }).compileComponents(); fixture = TestBed.createComponent(InputLibComponent); component = fixture.componentInstance; - inputStore = TestBed.inject(InputStore); - _quoteService = TestBed.inject(QuoteService); - fixture.detectChanges(); }); + afterAll(() => consoleSpy.mockRestore()); + it('should create', () => { expect(component).toBeTruthy(); }); - it('should update inputs through the store', async () => { - const input: Input = { - key: 'beitrag', - value: 2000 - }; - - await component.updateInputs(input); - - expect(inputStore.updateInputs).toHaveBeenCalledWith(input); - }); - - it('should calculate through the store', async () => { - await component.calculate(); - - expect(inputStore.calculate).toHaveBeenCalled(); - }); - - it('should handle string inputs', async () => { - const input: Input = { - key: 'leistungsVorgabe', - value: 'Rente' - }; - - await component.updateInputs(input); - - expect(inputStore.updateInputs).toHaveBeenCalledWith(input); - }); - - it('should handle numeric inputs', async () => { - const input: Input = { - key: 'laufzeit', - value: 15 - }; - - await component.updateInputs(input); + it('should calculate through the store', () => { + component.calculate(); - expect(inputStore.updateInputs).toHaveBeenCalledWith(input); + expect(mockInputStore.calculate).toHaveBeenCalled(); }); }); diff --git a/frontend/libs/input-lib/src/lib/input-lib/input-lib.component.html b/frontend/libs/input-lib/src/lib/input-lib/input-lib.component.html index d983d24..de52a0f 100644 --- a/frontend/libs/input-lib/src/lib/input-lib/input-lib.component.html +++ b/frontend/libs/input-lib/src/lib/input-lib/input-lib.component.html @@ -1,33 +1,35 @@ -
+
- +
+ @if (inputStore.uiState().geburtsdatum.error) { +
+
+ + {{ inputStore.uiState().geburtsdatum.error }} + +
+
+ }
- - - Beitrag - Einmalbeitrag - Garantierte Mindestrente - Garantiekapital - Gesamtkapital - Gesamtrente - + +
- +
@@ -46,17 +48,12 @@
- - Alter bei Rentenbeginn - Aufschubdauer - +
- +
@@ -74,11 +71,7 @@
- - Einmalbeitrag - Monatliche Beiträge - +
@@ -86,13 +79,7 @@
- - Monatliche Renten - Vierteljährliche Renten - Halbjährliche Renten - Jährliche Renten - +
@@ -100,21 +87,19 @@
- + @if (isProcessingData()) { + + } + @if (!isProcessingData()) { + + }
- @if (inputStore.uiState().quote.beitrag.einmalbeitrag || inputStore.uiState().quote.leistungsmerkmale.garantierteMindestrente) { -
-
- - Einmalbeitrag: {{ inputStore.uiState().quote.beitrag.einmalbeitrag | currency : 'EUR' }} - - - Garantierte Mindestrente: {{ inputStore.uiState().quote.leistungsmerkmale.garantierteMindestrente | currency : 'EUR' }} - -
-
- } +
-
+ diff --git a/frontend/libs/input-lib/src/lib/input-lib/input-lib.component.ts b/frontend/libs/input-lib/src/lib/input-lib/input-lib.component.ts index 92155a0..472ca33 100644 --- a/frontend/libs/input-lib/src/lib/input-lib/input-lib.component.ts +++ b/frontend/libs/input-lib/src/lib/input-lib/input-lib.component.ts @@ -1,14 +1,31 @@ import { CommonModule } from '@angular/common'; -import { Component, inject } from '@angular/core'; +import { Component, DestroyRef, inject, OnInit, signal } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { FormBuilder, FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { Router } from '@angular/router'; import { NxErrorModule } from '@aposin/ng-aquila/base'; import { NxButtonModule } from '@aposin/ng-aquila/button'; -import { NxDropdownComponent, NxDropdownItemComponent } from '@aposin/ng-aquila/dropdown'; +import { NxDropdownComponent } from '@aposin/ng-aquila/dropdown'; import { NxFormfieldComponent } from '@aposin/ng-aquila/formfield'; -import { NxColComponent, NxLayoutComponent, NxRowComponent } from '@aposin/ng-aquila/grid'; +import { + NxColComponent, + NxLayoutComponent, + NxRowComponent, +} from '@aposin/ng-aquila/grid'; import { NxInputModule } from '@aposin/ng-aquila/input'; - -import { InputStore } from './store/input.store'; -import { Input } from './store/input.store.interfaces'; +import { NxIsoDateModule } from '@aposin/ng-aquila/iso-date-adapter'; +import { NxSpinnerComponent } from '@aposin/ng-aquila/spinner'; +import { DatepickerComponent } from '@target/date-picker-lib'; +import { ErrorBoxComponent } from '@target/error-box-lib'; +import { Input, InputStore } from '@target/input-store-lib'; +import { ApiErrorResponse } from '@target/interfaces'; +import { + beitragszahlungsweiseOpts, + berechnungDerLaufzeitOpts, + leistungsVorgabeOpts, + rentenzahlungsweiseOpts, +} from '@target/validations'; +import { debounceTime } from 'rxjs'; @Component({ selector: 'lib-input-lib', @@ -19,22 +36,83 @@ import { Input } from './store/input.store.interfaces'; NxRowComponent, NxColComponent, NxFormfieldComponent, + NxIsoDateModule, NxDropdownComponent, - NxDropdownItemComponent, NxInputModule, NxErrorModule, NxButtonModule, + NxSpinnerComponent, + FormsModule, + ReactiveFormsModule, + DatepickerComponent, + ErrorBoxComponent, ], templateUrl: './input-lib.component.html', }) -export class InputLibComponent { +export class InputLibComponent implements OnInit { protected readonly inputStore = inject(InputStore); - updateInputs(input: Input): void { - this.inputStore.updateInputs(input); + private readonly formBuilder = inject(FormBuilder); + private readonly router = inject(Router); + private readonly destroyRef = inject(DestroyRef); + + protected isProcessingData = signal(false); + protected errorResponse = signal({} as ApiErrorResponse); + + protected readonly leistungsVorgabeOpts = leistungsVorgabeOpts; + protected readonly berechnungDerLaufzeitOpts = berechnungDerLaufzeitOpts; + protected readonly beitragszahlungsweiseOpts = beitragszahlungsweiseOpts; + protected readonly rentenzahlungsweiseOpts = rentenzahlungsweiseOpts; + + protected profile = this.formBuilder.group({ + geburtsdatum: [this.inputStore.uiState()['geburtsdatum'].value], + leistungsVorgabe: [this.inputStore.uiState()['leistungsVorgabe'].value], + beitrag: [this.inputStore.uiState()['beitrag'].value], + berechnungDerLaufzeit: [ + this.inputStore.uiState().berechnungDerLaufzeit.value, + ], + laufzeit: [this.inputStore.uiState().laufzeit.value], + beitragszahlungsweise: [ + this.inputStore.uiState().beitragszahlungsweise.value, + ], + rentenzahlungsweise: [this.inputStore.uiState().rentenzahlungsweise.value], + }); + + ngOnInit(): void { + this.profile.valueChanges + .pipe(takeUntilDestroyed(this.destroyRef), debounceTime(100)) + .subscribe((values) => { + this.isProcessingData.set(true); + + const input: Input[] = Object.entries(values).map( + ([key, value]) => + ({ + key, + value, + } as Input) + ); + + this.inputStore + .updateInputs(input) + .then(() => this.isProcessingData.set(false)); + }); } - async calculate(): Promise { - await this.inputStore.calculate() + calculate(): void { + this.isProcessingData.set(true); + + this.inputStore + .calculate() + .then(async (res) => { + await this.router.navigate(['/view', res]); + this.isProcessingData.set(false); + }) + .catch((err) => { + if (err.status === 400) + this.inputStore.processErrors(err.error.message); + else this.errorResponse.set(err.error); + + this.isProcessingData.set(false); + }); } } diff --git a/frontend/libs/input-lib/src/lib/input-lib/store/input.store.ts b/frontend/libs/input-lib/src/lib/input-lib/store/input.store.ts deleted file mode 100644 index e4777a4..0000000 --- a/frontend/libs/input-lib/src/lib/input-lib/store/input.store.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { inject } from '@angular/core'; -import { patchState, signalStore, withMethods, withState } from '@ngrx/signals'; -import { QuoteRequestDto } from '@target/interfaces'; -import { InputDtoSchema } from '@target/validations'; -import { lastValueFrom } from 'rxjs'; - -import { Input, InputState } from './input.store.interfaces'; -import { QuoteService } from './services/quote.service'; - -const initialState: InputState = { - leistungsVorgabe: { value: 'Beitrag', valid: true, error: null }, - beitrag: { value: 1000, valid: true, error: null }, - berechnungDerLaufzeit: { value: 'Alter bei Rentenbeginn', valid: true, error: null }, - laufzeit: { value: 10, valid: true, error: null }, - beitragszahlungsweise: { value: 'Einmalbeitrag', valid: true, error: null }, - rentenzahlungsweise: { value: 'Monatliche Renten', valid: true, error: null }, - quote: { - basisdaten: { - geburtsdatum: '', - versicherungsbeginn: '', - garantieniveau: '', - alterBeiRentenbeginn: 0, - aufschubdauer: 0, - beitragszahlungsdauer: 0 - }, - leistungsmerkmale: { - garantierteMindestrente: 0, - einmaligesGarantiekapital: 0, - todesfallleistungAbAltersrentenbezug: 0 - }, - beitrag: { - einmalbeitrag: 0, - beitragsdynamik: '' - }, - }, -}; - -export const InputStore = signalStore( - { providedIn: 'root' }, - withState({ uiState: initialState }), - withMethods((store, quoteService = inject(QuoteService)) => ({ - updateInputs: async (input: Input): Promise => { - const initialNewState = { - ...store.uiState(), - [input.key]: { value: input.value, valid: true, error: null }, - }; - const validationResult = await InputDtoSchema.safeParseAsync(transformUiStateToInputDto(initialNewState)); - - if (validationResult.success) { - patchState(store, { uiState: initialNewState }); - return; - } - - const validatedState = validationResult.error.errors.reduce( - (state, { path, message }) => ({ - ...state, - [path[0]]: { ...state[path[0] as keyof InputState], valid: false, error: message }, - }), - initialNewState - ); - - patchState(store, { uiState: validatedState }); - }, - calculate: async (): Promise => { - const quoteDto = transformUiStateToInputDto(store.uiState()); - const validationResult = await InputDtoSchema.safeParseAsync(quoteDto); - - try { - if (!validationResult.success) { - throw new Error('Invalid input'); - } - - const quote = await lastValueFrom(quoteService.calculateQuote(quoteDto as QuoteRequestDto)); - - patchState(store, { uiState: { ...store.uiState(), quote } }); - } catch (error) { - console.error(error); - } - }, - })) -); - -const transformUiStateToInputDto = (state: InputState): QuoteRequestDto => Object.entries(state).reduce((acc, [key, { value }]) => ({ ...acc, [key]: value }), {} as QuoteRequestDto); diff --git a/frontend/libs/input-lib/src/lib/input-lib/store/services/__tests__/quote.service.spec.ts b/frontend/libs/input-lib/src/lib/input-lib/store/services/__tests__/quote.service.spec.ts deleted file mode 100644 index 95b7adf..0000000 --- a/frontend/libs/input-lib/src/lib/input-lib/store/services/__tests__/quote.service.spec.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { HttpClient } from '@angular/common/http'; -import { TestBed } from '@angular/core/testing'; -import { QuoteRequestDto, QuoteResponseDto } from '@target/interfaces'; -import { firstValueFrom, of, throwError } from 'rxjs'; - -import { QuoteService } from '../quote.service'; - -describe('QuoteService', () => { - let service: QuoteService; - let httpClient: { post: jest.Mock }; - - beforeEach(() => { - httpClient = { post: jest.fn() }; - TestBed.configureTestingModule({ - providers: [ - QuoteService, - { provide: HttpClient, useValue: httpClient } - ] - }); - service = TestBed.inject(QuoteService); - }); - - it('should be created', () => { - expect(service).toBeDefined(); - }); - - describe('calculateQuote', () => { - it('should call the correct endpoint with quote request data', async () => { - const mockRequest: QuoteRequestDto = { - beitrag: 1000, - laufzeit: 12, - leistungsVorgabe: 'Beitrag', - berechnungDerLaufzeit: 'Alter bei Rentenbeginn', - beitragszahlungsweise: 'Monatliche Beiträge' - }; - const mockResponse: QuoteResponseDto = { - basisdaten: { - geburtsdatum: '1990-01-01', - versicherungsbeginn: '2024-01-01', - garantieniveau: '90%', - alterBeiRentenbeginn: 67, - aufschubdauer: 30, - beitragszahlungsdauer: 10 - }, - leistungsmerkmale: { - garantierteMindestrente: 50000, - einmaligesGarantiekapital: 25000, - todesfallleistungAbAltersrentenbezug: 40000 - }, - beitrag: { - einmalbeitrag: 0, - beitragsdynamik: '1,5%' - } - }; - - httpClient.post.mockReturnValue(of(mockResponse)); - - const response = await firstValueFrom(service.calculateQuote(mockRequest)); - - expect(response).toEqual(mockResponse); - expect(httpClient.post).toHaveBeenCalledWith('/api/quote', mockRequest); - }); - - it('should propagate errors from the API', async () => { - const mockRequest: QuoteRequestDto = { - beitrag: 1000, - laufzeit: 12, - leistungsVorgabe: 'Beitrag', - berechnungDerLaufzeit: 'Alter bei Rentenbeginn', - beitragszahlungsweise: 'Monatliche Beiträge' - }; - const errorResponse = new Error('API Error'); - - httpClient.post.mockReturnValue(throwError(() => errorResponse)); - - await expect(firstValueFrom(service.calculateQuote(mockRequest))) - .rejects.toBe(errorResponse); - expect(httpClient.post).toHaveBeenCalledWith('/api/quote', mockRequest); - }); - }); -}); - diff --git a/frontend/libs/input-lib/src/lib/input-lib/store/services/__tests__/quote.servicespec.ts b/frontend/libs/input-lib/src/lib/input-lib/store/services/__tests__/quote.servicespec.ts deleted file mode 100644 index 5531f5a..0000000 --- a/frontend/libs/input-lib/src/lib/input-lib/store/services/__tests__/quote.servicespec.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { HttpClient } from '@angular/common/http'; -import { inject, Injectable } from '@angular/core'; -import { QuoteRequestDto, QuoteResponseDto } from '@target/interfaces'; -import { Observable } from 'rxjs'; - -@Injectable({ providedIn: 'root' }) -export class QuoteService { - private readonly http = inject(HttpClient); - - calculateQuote(quoteDto: QuoteRequestDto): Observable { - return this.http.post('/api/quote', quoteDto); - } -} diff --git a/frontend/libs/input-lib/tsconfig.json b/frontend/libs/input-lib/tsconfig.json index fde35ea..2898c84 100644 --- a/frontend/libs/input-lib/tsconfig.json +++ b/frontend/libs/input-lib/tsconfig.json @@ -6,7 +6,8 @@ "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, - "noFallthroughCasesInSwitch": true + "noFallthroughCasesInSwitch": true, + "esModuleInterop": true, }, "files": [], "include": [], diff --git a/frontend/libs/input-store-lib/README.md b/frontend/libs/input-store-lib/README.md new file mode 100644 index 0000000..22c9c52 --- /dev/null +++ b/frontend/libs/input-store-lib/README.md @@ -0,0 +1,7 @@ +# input-store-lib + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test input-store-lib` to execute the unit tests. diff --git a/frontend/libs/input-store-lib/eslint.config.cjs b/frontend/libs/input-store-lib/eslint.config.cjs new file mode 100644 index 0000000..39a2d94 --- /dev/null +++ b/frontend/libs/input-store-lib/eslint.config.cjs @@ -0,0 +1,34 @@ +const nx = require('@nx/eslint-plugin'); +const baseConfig = require('../../../eslint.config.cjs'); + +module.exports = [ + ...baseConfig, + ...nx.configs['flat/angular'], + ...nx.configs['flat/angular-template'], + { + files: ['**/*.ts'], + rules: { + '@angular-eslint/directive-selector': [ + 'error', + { + type: 'attribute', + prefix: 'lib', + style: 'camelCase', + }, + ], + '@angular-eslint/component-selector': [ + 'error', + { + type: 'element', + prefix: 'lib', + style: 'kebab-case', + }, + ], + }, + }, + { + files: ['**/*.html'], + // Override or add rules here + rules: {}, + }, +]; diff --git a/frontend/libs/input-store-lib/jest.config.ts b/frontend/libs/input-store-lib/jest.config.ts new file mode 100644 index 0000000..1f11ef3 --- /dev/null +++ b/frontend/libs/input-store-lib/jest.config.ts @@ -0,0 +1,21 @@ +export default { + displayName: 'input-store-lib', + preset: '../../../jest.preset.js', + setupFilesAfterEnv: ['/src/test-setup.ts'], + coverageDirectory: '../../../coverage/frontend/libs/input-store-lib', + transform: { + '^.+\\.(ts|mjs|js|html)$': [ + 'jest-preset-angular', + { + tsconfig: '/tsconfig.spec.json', + stringifyContentPathRegex: '\\.(html|svg)$', + }, + ], + }, + transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'], + snapshotSerializers: [ + 'jest-preset-angular/build/serializers/no-ng-attributes', + 'jest-preset-angular/build/serializers/ng-snapshot', + 'jest-preset-angular/build/serializers/html-comment', + ], +}; diff --git a/frontend/libs/input-store-lib/project.json b/frontend/libs/input-store-lib/project.json new file mode 100644 index 0000000..49d1e56 --- /dev/null +++ b/frontend/libs/input-store-lib/project.json @@ -0,0 +1,20 @@ +{ + "name": "input-store-lib", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "frontend/libs/input-store-lib/src", + "prefix": "lib", + "projectType": "library", + "tags": ["store"], + "targets": { + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "frontend/libs/input-store-lib/jest.config.ts" + } + }, + "lint": { + "executor": "@nx/eslint:lint" + } + } +} diff --git a/frontend/libs/input-store-lib/src/index.ts b/frontend/libs/input-store-lib/src/index.ts new file mode 100644 index 0000000..a66ae1f --- /dev/null +++ b/frontend/libs/input-store-lib/src/index.ts @@ -0,0 +1,2 @@ +export * from './lib/input-store-lib/input.store'; +export * from './lib/input-store-lib/interface/input.store.interfaces'; diff --git a/frontend/libs/input-lib/src/lib/input-lib/store/__tests__/input.store.spec.ts b/frontend/libs/input-store-lib/src/lib/input-store-lib/__tests__/input.store.spec.ts similarity index 60% rename from frontend/libs/input-lib/src/lib/input-lib/store/__tests__/input.store.spec.ts rename to frontend/libs/input-store-lib/src/lib/input-store-lib/__tests__/input.store.spec.ts index 547004b..0082b9f 100644 --- a/frontend/libs/input-lib/src/lib/input-lib/store/__tests__/input.store.spec.ts +++ b/frontend/libs/input-store-lib/src/lib/input-store-lib/__tests__/input.store.spec.ts @@ -1,10 +1,11 @@ import { TestBed } from '@angular/core/testing'; +import { fakerEN } from '@faker-js/faker'; import { QuoteResponseDto } from '@target/interfaces'; +import { QuoteService } from '@target/service-lib'; import { InputDtoSchema } from '@target/validations'; -import { of, throwError } from 'rxjs'; +import { of } from 'rxjs'; import { InputStore } from '../input.store'; -import { QuoteService } from '../services/quote.service'; jest.mock('@target/validations', () => ({ InputDtoSchema: { @@ -14,49 +15,52 @@ jest.mock('@target/validations', () => ({ describe('InputStore', () => { let store: any; - let quoteService: jest.Mocked; + let quoteService: QuoteService; const mockQuoteResponse: QuoteResponseDto = { + id: fakerEN.string.alpha({ length: 15 }), basisdaten: { geburtsdatum: '1990-01-01', versicherungsbeginn: '2024-01-01', garantieniveau: '90%', alterBeiRentenbeginn: 67, aufschubdauer: 30, - beitragszahlungsdauer: 10 + beitragszahlungsdauer: 10, }, leistungsmerkmale: { garantierteMindestrente: 50000, einmaligesGarantiekapital: 25000, - todesfallleistungAbAltersrentenbezug: 40000 + todesfallleistungAbAltersrentenbezug: 40000, }, beitrag: { einmalbeitrag: 0, - beitragsdynamik: '1,5%' - } + beitragsdynamik: '1,5%', + }, }; beforeEach(() => { - quoteService = { - calculateQuote: jest.fn(), - } as unknown as jest.Mocked; - TestBed.configureTestingModule({ providers: [ - { provide: QuoteService, useValue: quoteService } - ] + { + provide: QuoteService, + useValue: { calculateQuote: jest.fn(), fetchQuote: jest.fn() }, + }, + ], }); store = TestBed.inject(InputStore); + quoteService = TestBed.inject(QuoteService); }); describe('updateInputs', () => { it('should update state when validation succeeds', async () => { const input = { key: 'beitrag', value: 2000 }; - (InputDtoSchema.safeParseAsync as jest.Mock).mockResolvedValue({ success: true }); + (InputDtoSchema.safeParseAsync as jest.Mock).mockResolvedValue({ + success: true, + }); - await (store as any).updateInputs(input); + await (store as any).updateInputs([input]); expect(store.uiState().beitrag.value).toBe(2000); expect(store.uiState().beitrag.valid).toBe(true); @@ -69,11 +73,11 @@ describe('InputStore', () => { (InputDtoSchema.safeParseAsync as jest.Mock).mockResolvedValue({ success: false, error: { - errors: [{ path: ['beitrag'], message: 'Beitrag must be positive' }] - } + errors: [{ path: ['beitrag'], message: 'Beitrag must be positive' }], + }, }); - await (store as any).updateInputs(input); + await (store as any).updateInputs([input]); expect(store.uiState().beitrag.value).toBe(-1); expect(store.uiState().beitrag.valid).toBe(false); @@ -83,42 +87,29 @@ describe('InputStore', () => { describe('calculate', () => { it('should update quote when calculation succeeds', async () => { - (InputDtoSchema.safeParseAsync as jest.Mock).mockResolvedValue({ success: true }); - quoteService.calculateQuote.mockReturnValue(of(mockQuoteResponse)); + (InputDtoSchema.safeParseAsync as jest.Mock).mockResolvedValue({ + success: true, + }); + + jest + .spyOn(quoteService, 'calculateQuote') + .mockReturnValue(of(mockQuoteResponse)); await (store as any).calculate(); - expect(store.uiState().quote).toEqual(mockQuoteResponse); expect(quoteService.calculateQuote).toHaveBeenCalled(); }); it('should handle validation failure', async () => { - const consoleSpy = jest.spyOn(console, 'error').mockImplementation(); - - (InputDtoSchema.safeParseAsync as jest.Mock).mockResolvedValue({ success: false }); + (InputDtoSchema.safeParseAsync as jest.Mock).mockResolvedValue({ + success: false, + }); - await (store as any).calculate(); + await expect((store as any).calculate()).rejects.toThrow( + new Error('Invalid input') + ); - expect(consoleSpy).toHaveBeenCalledWith(new Error('Invalid input')); expect(quoteService.calculateQuote).not.toHaveBeenCalled(); - - consoleSpy.mockRestore(); - }); - - it('should handle API errors', async () => { - const consoleSpy = jest.spyOn(console, 'error').mockImplementation(); - - (InputDtoSchema.safeParseAsync as jest.Mock).mockResolvedValue({ success: true }); - const error = new Error('API Error'); - - quoteService.calculateQuote.mockReturnValue(throwError(() => error)); - - await (store as any).calculate(); - - expect(consoleSpy).toHaveBeenCalledWith(error); - expect(quoteService.calculateQuote).toHaveBeenCalled(); - - consoleSpy.mockRestore(); }); }); }); diff --git a/frontend/libs/input-store-lib/src/lib/input-store-lib/input.store.ts b/frontend/libs/input-store-lib/src/lib/input-store-lib/input.store.ts new file mode 100644 index 0000000..e6531ce --- /dev/null +++ b/frontend/libs/input-store-lib/src/lib/input-store-lib/input.store.ts @@ -0,0 +1,148 @@ +import { inject } from '@angular/core'; +import { patchState, signalStore, withMethods, withState } from '@ngrx/signals'; +import { QuoteRequestDto } from '@target/interfaces'; +import { QuoteService } from '@target/service-lib'; +import { InputDtoSchema } from '@target/validations'; +import { lastValueFrom } from 'rxjs'; + +import { Input, InputState } from './interface/input.store.interfaces'; + +const initialState: InputState = { + geburtsdatum: { value: '', valid: true, error: null }, + leistungsVorgabe: { value: 'Beitrag', valid: true, error: null }, + beitrag: { value: 1000, valid: true, error: null }, + berechnungDerLaufzeit: { + value: 'Alter bei Rentenbeginn', + valid: true, + error: null, + }, + laufzeit: { value: 10, valid: true, error: null }, + beitragszahlungsweise: { value: 'Einmalbeitrag', valid: true, error: null }, + rentenzahlungsweise: { value: 'Monatliche Renten', valid: true, error: null }, +}; + +export const InputStore = signalStore( + { providedIn: 'root' }, + withState({ uiState: initialState }), + withMethods((store, quoteService = inject(QuoteService)) => ({ + updateInputs: async (input: Input[]): Promise => { + const initialNewState = { + ...store.uiState(), + ...transformFromInputArrayToInputState(input), + }; + + const validationResult = await InputDtoSchema.safeParseAsync( + transformUiStateToInputDto(initialNewState) + ); + + if (validationResult.success) { + patchState(store, { uiState: initialNewState }); + + return; + } + + const validatedState = validationResult.error.errors.reduce( + (state, { path, message }) => ({ + ...state, + [path[0]]: { + ...state[path[0] as keyof InputState], + valid: false, + error: message, + }, + }), + initialNewState + ); + + patchState(store, { uiState: validatedState }); + }, + + processErrors: (messages: string[]): void => { + const errorArr = messages?.map((it) => { + const errArr = it.split(':'); + + return { + path: errArr[0].trim(), + message: errArr[1].trim(), + }; + }); + + errorArr?.forEach(({ path, message }) => { + patchState(store, { + uiState: { + ...store.uiState(), + [path]: { + ...store.uiState()[path as keyof InputState], + valid: false, + error: message, + }, + }, + }); + }); + }, + + calculate: async (): Promise => { + const quoteDto = transformUiStateToInputDto(store.uiState()); + const validationResult = await InputDtoSchema.safeParseAsync(quoteDto); + + if (!validationResult.success) { + const validatedState = { ...store.uiState() }; + + // Modify the state for all keys to reset errors tpo ensure and fresh validation + Object.keys(validatedState).forEach((key) => { + patchState(store, { + uiState: { + ...validatedState, + [key]: { + ...validatedState[key as keyof InputState], + valid: true, + error: null, + }, + }, + }); + }); + + // Modify the state for the keys that have errors + validationResult.error?.errors.forEach(({ path, message }) => { + patchState(store, { + uiState: { + ...validatedState, + [path[0]]: { + ...validatedState[path[0] as keyof InputState], + valid: false, + error: message, + }, + }, + }); + }); + + throw new Error('Invalid input'); + } + + return ( + ( + await lastValueFrom( + quoteService.calculateQuote(quoteDto as QuoteRequestDto) + ) + ).id + ); + }, + })) +); + +const transformUiStateToInputDto = (state: InputState): QuoteRequestDto => + Object.entries(state).reduce( + (acc, [key, { value }]) => ({ + ...acc, + [key]: value, + }), + {} as QuoteRequestDto + ); + +const transformFromInputArrayToInputState = (input: Input[]): {} => + input.reduce( + (acc, { key, value }) => ({ + ...acc, + [key]: { value: value, valid: true, error: null }, + }), + {} as InputState + ); diff --git a/frontend/libs/input-lib/src/lib/input-lib/store/input.store.interfaces.ts b/frontend/libs/input-store-lib/src/lib/input-store-lib/interface/input.store.interfaces.ts similarity index 73% rename from frontend/libs/input-lib/src/lib/input-lib/store/input.store.interfaces.ts rename to frontend/libs/input-store-lib/src/lib/input-store-lib/interface/input.store.interfaces.ts index eb12b30..d391a09 100644 --- a/frontend/libs/input-lib/src/lib/input-lib/store/input.store.interfaces.ts +++ b/frontend/libs/input-store-lib/src/lib/input-store-lib/interface/input.store.interfaces.ts @@ -1,22 +1,22 @@ -import { QuoteResponseDto } from "@target/interfaces"; - interface InputField { value: T; valid: boolean; error: string | null; } -export interface InputState { +interface InputState { + geburtsdatum: InputField; leistungsVorgabe: InputField; beitrag: InputField; berechnungDerLaufzeit: InputField; laufzeit: InputField; beitragszahlungsweise: InputField; rentenzahlungsweise: InputField; - quote: QuoteResponseDto; } -export interface Input { +interface Input { key: keyof InputState; value: string | number; } + +export { Input,InputState }; diff --git a/frontend/libs/input-store-lib/src/test-setup.ts b/frontend/libs/input-store-lib/src/test-setup.ts new file mode 100644 index 0000000..ea41401 --- /dev/null +++ b/frontend/libs/input-store-lib/src/test-setup.ts @@ -0,0 +1,6 @@ +import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone'; + +setupZoneTestEnv({ + errorOnUnknownElements: true, + errorOnUnknownProperties: true, +}); diff --git a/frontend/libs/input-store-lib/tsconfig.json b/frontend/libs/input-store-lib/tsconfig.json new file mode 100644 index 0000000..fde35ea --- /dev/null +++ b/frontend/libs/input-store-lib/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "es2022", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "extends": "../../../tsconfig.base.json", + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/frontend/libs/input-store-lib/tsconfig.lib.json b/frontend/libs/input-store-lib/tsconfig.lib.json new file mode 100644 index 0000000..9b49be7 --- /dev/null +++ b/frontend/libs/input-store-lib/tsconfig.lib.json @@ -0,0 +1,17 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "declaration": true, + "declarationMap": true, + "inlineSources": true, + "types": [] + }, + "exclude": [ + "src/**/*.spec.ts", + "src/test-setup.ts", + "jest.config.ts", + "src/**/*.test.ts" + ], + "include": ["src/**/*.ts"] +} diff --git a/frontend/libs/input-store-lib/tsconfig.spec.json b/frontend/libs/input-store-lib/tsconfig.spec.json new file mode 100644 index 0000000..f858ef7 --- /dev/null +++ b/frontend/libs/input-store-lib/tsconfig.spec.json @@ -0,0 +1,16 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "module": "commonjs", + "target": "es2016", + "types": ["jest", "node"] + }, + "files": ["src/test-setup.ts"], + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/frontend/libs/quota-store-lib/README.md b/frontend/libs/quota-store-lib/README.md new file mode 100644 index 0000000..a3fc7b6 --- /dev/null +++ b/frontend/libs/quota-store-lib/README.md @@ -0,0 +1,7 @@ +# quota-store-lib + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test quota-store-lib` to execute the unit tests. diff --git a/frontend/libs/quota-store-lib/eslint.config.cjs b/frontend/libs/quota-store-lib/eslint.config.cjs new file mode 100644 index 0000000..39a2d94 --- /dev/null +++ b/frontend/libs/quota-store-lib/eslint.config.cjs @@ -0,0 +1,34 @@ +const nx = require('@nx/eslint-plugin'); +const baseConfig = require('../../../eslint.config.cjs'); + +module.exports = [ + ...baseConfig, + ...nx.configs['flat/angular'], + ...nx.configs['flat/angular-template'], + { + files: ['**/*.ts'], + rules: { + '@angular-eslint/directive-selector': [ + 'error', + { + type: 'attribute', + prefix: 'lib', + style: 'camelCase', + }, + ], + '@angular-eslint/component-selector': [ + 'error', + { + type: 'element', + prefix: 'lib', + style: 'kebab-case', + }, + ], + }, + }, + { + files: ['**/*.html'], + // Override or add rules here + rules: {}, + }, +]; diff --git a/frontend/libs/quota-store-lib/jest.config.ts b/frontend/libs/quota-store-lib/jest.config.ts new file mode 100644 index 0000000..c974be1 --- /dev/null +++ b/frontend/libs/quota-store-lib/jest.config.ts @@ -0,0 +1,21 @@ +export default { + displayName: 'quota-store-lib', + preset: '../../../jest.preset.js', + setupFilesAfterEnv: ['/src/test-setup.ts'], + coverageDirectory: '../../../coverage/frontend/libs/quota-store-lib', + transform: { + '^.+\\.(ts|mjs|js|html)$': [ + 'jest-preset-angular', + { + tsconfig: '/tsconfig.spec.json', + stringifyContentPathRegex: '\\.(html|svg)$', + }, + ], + }, + transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'], + snapshotSerializers: [ + 'jest-preset-angular/build/serializers/no-ng-attributes', + 'jest-preset-angular/build/serializers/ng-snapshot', + 'jest-preset-angular/build/serializers/html-comment', + ], +}; diff --git a/frontend/libs/quota-store-lib/project.json b/frontend/libs/quota-store-lib/project.json new file mode 100644 index 0000000..d3a6a35 --- /dev/null +++ b/frontend/libs/quota-store-lib/project.json @@ -0,0 +1,20 @@ +{ + "name": "quota-store-lib", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "frontend/libs/quota-store-lib/src", + "prefix": "lib", + "projectType": "library", + "tags": ["store"], + "targets": { + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "frontend/libs/quota-store-lib/jest.config.ts" + } + }, + "lint": { + "executor": "@nx/eslint:lint" + } + } +} diff --git a/frontend/libs/quota-store-lib/src/index.ts b/frontend/libs/quota-store-lib/src/index.ts new file mode 100644 index 0000000..a2bc78d --- /dev/null +++ b/frontend/libs/quota-store-lib/src/index.ts @@ -0,0 +1 @@ +export * from './lib/quota-store-lib/quote.store'; diff --git a/frontend/libs/quota-store-lib/src/lib/quota-store-lib/__tests__/quote.store.spec.ts b/frontend/libs/quota-store-lib/src/lib/quota-store-lib/__tests__/quote.store.spec.ts new file mode 100644 index 0000000..248dc07 --- /dev/null +++ b/frontend/libs/quota-store-lib/src/lib/quota-store-lib/__tests__/quote.store.spec.ts @@ -0,0 +1,58 @@ +import { TestBed } from '@angular/core/testing'; +import { fakerEN } from '@faker-js/faker'; +import { QuoteResponseDto } from '@target/interfaces'; +import { QuoteService } from '@target/service-lib'; +import { of } from 'rxjs'; + +import { QuoteStore } from '../quote.store'; + +describe('QuoteStore', () => { + let store: any; + let quoteService: QuoteService; + + const mockQuoteResponse: QuoteResponseDto = { + id: fakerEN.string.alpha({ length: 15 }), + basisdaten: { + geburtsdatum: '2000-01-01', + versicherungsbeginn: '2024-01-01', + garantieniveau: '90%', + alterBeiRentenbeginn: 67, + aufschubdauer: 30, + beitragszahlungsdauer: 10, + }, + leistungsmerkmale: { + garantierteMindestrente: 50000, + einmaligesGarantiekapital: 25000, + todesfallleistungAbAltersrentenbezug: 40000, + }, + beitrag: { + einmalbeitrag: 0, + beitragsdynamik: '1,5%', + }, + }; + + beforeEach(() => { + quoteService = { + fetchQuote: jest.fn(), + } as unknown as jest.Mocked; + + TestBed.configureTestingModule({ + providers: [{ provide: QuoteService, useValue: quoteService }], + }); + + store = TestBed.inject(QuoteStore); + }); + + it('should fetch quote and update state', async () => { + jest + .spyOn(quoteService, 'fetchQuote') + .mockReturnValue(of(mockQuoteResponse)); + + await store.fetchQuote(mockQuoteResponse.id); + + expect(quoteService.fetchQuote).toHaveBeenCalledWith(mockQuoteResponse.id); + expect(quoteService.fetchQuote).toHaveBeenCalledTimes(1); + + expect(store.quoteState()).toEqual(mockQuoteResponse); + }); +}); diff --git a/frontend/libs/quota-store-lib/src/lib/quota-store-lib/interface/quote.store.interface.ts b/frontend/libs/quota-store-lib/src/lib/quota-store-lib/interface/quote.store.interface.ts new file mode 100644 index 0000000..d8c7a49 --- /dev/null +++ b/frontend/libs/quota-store-lib/src/lib/quota-store-lib/interface/quote.store.interface.ts @@ -0,0 +1,3 @@ +import { QuoteResponseDto } from '@target/interfaces'; + +export interface QuoteState extends QuoteResponseDto {} diff --git a/frontend/libs/quota-store-lib/src/lib/quota-store-lib/quote.store.ts b/frontend/libs/quota-store-lib/src/lib/quota-store-lib/quote.store.ts new file mode 100644 index 0000000..7631ea4 --- /dev/null +++ b/frontend/libs/quota-store-lib/src/lib/quota-store-lib/quote.store.ts @@ -0,0 +1,37 @@ +import { inject } from '@angular/core'; +import { patchState, signalStore, withMethods, withState } from '@ngrx/signals'; +import { QuoteService } from '@target/service-lib'; +import { lastValueFrom } from 'rxjs'; + +import { QuoteState } from './interface/quote.store.interface'; + +const initialState: QuoteState = { + basisdaten: { + geburtsdatum: '', + versicherungsbeginn: '', + garantieniveau: '', + alterBeiRentenbeginn: 0, + aufschubdauer: 0, + beitragszahlungsdauer: 0, + }, + leistungsmerkmale: { + garantierteMindestrente: 0, + einmaligesGarantiekapital: 0, + todesfallleistungAbAltersrentenbezug: 0, + }, + beitrag: { + einmalbeitrag: 0, + beitragsdynamik: '', + }, +}; + +export const QuoteStore = signalStore( + { providedIn: 'root' }, + withState({ quoteState: initialState }), + withMethods((store, quoteService = inject(QuoteService)) => ({ + fetchQuote: async (quoteId: string): Promise => { + const quote = await lastValueFrom(quoteService.fetchQuote(quoteId)); + patchState(store, { quoteState: { ...quote } }); + }, + })) +); diff --git a/frontend/libs/quota-store-lib/src/test-setup.ts b/frontend/libs/quota-store-lib/src/test-setup.ts new file mode 100644 index 0000000..ea41401 --- /dev/null +++ b/frontend/libs/quota-store-lib/src/test-setup.ts @@ -0,0 +1,6 @@ +import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone'; + +setupZoneTestEnv({ + errorOnUnknownElements: true, + errorOnUnknownProperties: true, +}); diff --git a/frontend/libs/quota-store-lib/tsconfig.json b/frontend/libs/quota-store-lib/tsconfig.json new file mode 100644 index 0000000..fde35ea --- /dev/null +++ b/frontend/libs/quota-store-lib/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "es2022", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "extends": "../../../tsconfig.base.json", + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/frontend/libs/quota-store-lib/tsconfig.lib.json b/frontend/libs/quota-store-lib/tsconfig.lib.json new file mode 100644 index 0000000..9b49be7 --- /dev/null +++ b/frontend/libs/quota-store-lib/tsconfig.lib.json @@ -0,0 +1,17 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "declaration": true, + "declarationMap": true, + "inlineSources": true, + "types": [] + }, + "exclude": [ + "src/**/*.spec.ts", + "src/test-setup.ts", + "jest.config.ts", + "src/**/*.test.ts" + ], + "include": ["src/**/*.ts"] +} diff --git a/frontend/libs/quota-store-lib/tsconfig.spec.json b/frontend/libs/quota-store-lib/tsconfig.spec.json new file mode 100644 index 0000000..f858ef7 --- /dev/null +++ b/frontend/libs/quota-store-lib/tsconfig.spec.json @@ -0,0 +1,16 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "module": "commonjs", + "target": "es2016", + "types": ["jest", "node"] + }, + "files": ["src/test-setup.ts"], + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/frontend/libs/quote-resolver-lib/README.md b/frontend/libs/quote-resolver-lib/README.md new file mode 100644 index 0000000..2cdc009 --- /dev/null +++ b/frontend/libs/quote-resolver-lib/README.md @@ -0,0 +1,7 @@ +# quote-resolver-lib + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test quote-resolver-lib` to execute the unit tests. diff --git a/frontend/libs/quote-resolver-lib/eslint.config.cjs b/frontend/libs/quote-resolver-lib/eslint.config.cjs new file mode 100644 index 0000000..39a2d94 --- /dev/null +++ b/frontend/libs/quote-resolver-lib/eslint.config.cjs @@ -0,0 +1,34 @@ +const nx = require('@nx/eslint-plugin'); +const baseConfig = require('../../../eslint.config.cjs'); + +module.exports = [ + ...baseConfig, + ...nx.configs['flat/angular'], + ...nx.configs['flat/angular-template'], + { + files: ['**/*.ts'], + rules: { + '@angular-eslint/directive-selector': [ + 'error', + { + type: 'attribute', + prefix: 'lib', + style: 'camelCase', + }, + ], + '@angular-eslint/component-selector': [ + 'error', + { + type: 'element', + prefix: 'lib', + style: 'kebab-case', + }, + ], + }, + }, + { + files: ['**/*.html'], + // Override or add rules here + rules: {}, + }, +]; diff --git a/frontend/libs/quote-resolver-lib/jest.config.ts b/frontend/libs/quote-resolver-lib/jest.config.ts new file mode 100644 index 0000000..5d54c69 --- /dev/null +++ b/frontend/libs/quote-resolver-lib/jest.config.ts @@ -0,0 +1,21 @@ +export default { + displayName: 'quote-resolver-lib', + preset: '../../../jest.preset.js', + setupFilesAfterEnv: ['/src/test-setup.ts'], + coverageDirectory: '../../../coverage/frontend/libs/quote-resolver-lib', + transform: { + '^.+\\.(ts|mjs|js|html)$': [ + 'jest-preset-angular', + { + tsconfig: '/tsconfig.spec.json', + stringifyContentPathRegex: '\\.(html|svg)$', + }, + ], + }, + transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'], + snapshotSerializers: [ + 'jest-preset-angular/build/serializers/no-ng-attributes', + 'jest-preset-angular/build/serializers/ng-snapshot', + 'jest-preset-angular/build/serializers/html-comment', + ], +}; diff --git a/frontend/libs/quote-resolver-lib/project.json b/frontend/libs/quote-resolver-lib/project.json new file mode 100644 index 0000000..e494aac --- /dev/null +++ b/frontend/libs/quote-resolver-lib/project.json @@ -0,0 +1,20 @@ +{ + "name": "quote-resolver-lib", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "frontend/libs/quote-resolver-lib/src", + "prefix": "lib", + "projectType": "library", + "tags": ["resolver"], + "targets": { + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "frontend/libs/quote-resolver-lib/jest.config.ts" + } + }, + "lint": { + "executor": "@nx/eslint:lint" + } + } +} diff --git a/frontend/libs/quote-resolver-lib/src/index.ts b/frontend/libs/quote-resolver-lib/src/index.ts new file mode 100644 index 0000000..e6d5a81 --- /dev/null +++ b/frontend/libs/quote-resolver-lib/src/index.ts @@ -0,0 +1 @@ +export * from './lib/quote-resolver-lib/quote-view.resolver' diff --git a/frontend/libs/quote-resolver-lib/src/lib/quote-resolver-lib/__tests__/quote-view-resolver.resolver.spec.ts b/frontend/libs/quote-resolver-lib/src/lib/quote-resolver-lib/__tests__/quote-view-resolver.resolver.spec.ts new file mode 100644 index 0000000..52f6686 --- /dev/null +++ b/frontend/libs/quote-resolver-lib/src/lib/quote-resolver-lib/__tests__/quote-view-resolver.resolver.spec.ts @@ -0,0 +1,19 @@ +import { TestBed } from '@angular/core/testing'; +import { ResolveFn } from '@angular/router'; + +import { quoteViewResolver } from '../quote-view.resolver'; + +describe('quoteViewResolverResolver', () => { + const executeResolver: ResolveFn = (...resolverParameters) => + TestBed.runInInjectionContext(() => + quoteViewResolver(...resolverParameters) + ); + + beforeEach(() => { + TestBed.configureTestingModule({}); + }); + + it('should be created', () => { + expect(executeResolver).toBeTruthy(); + }); +}); diff --git a/frontend/libs/quote-resolver-lib/src/lib/quote-resolver-lib/quote-view.resolver.ts b/frontend/libs/quote-resolver-lib/src/lib/quote-resolver-lib/quote-view.resolver.ts new file mode 100644 index 0000000..0dedf84 --- /dev/null +++ b/frontend/libs/quote-resolver-lib/src/lib/quote-resolver-lib/quote-view.resolver.ts @@ -0,0 +1,9 @@ +import { inject } from '@angular/core'; +import { ResolveFn } from '@angular/router'; +import { QuoteStore } from '@target/quota-store-lib'; + +export const quoteViewResolver: ResolveFn = (route, _) => { + const quoteStore = inject(QuoteStore); + + return quoteStore.fetchQuote(route.params['id']); +}; diff --git a/frontend/libs/quote-resolver-lib/src/test-setup.ts b/frontend/libs/quote-resolver-lib/src/test-setup.ts new file mode 100644 index 0000000..ea41401 --- /dev/null +++ b/frontend/libs/quote-resolver-lib/src/test-setup.ts @@ -0,0 +1,6 @@ +import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone'; + +setupZoneTestEnv({ + errorOnUnknownElements: true, + errorOnUnknownProperties: true, +}); diff --git a/frontend/libs/quote-resolver-lib/tsconfig.json b/frontend/libs/quote-resolver-lib/tsconfig.json new file mode 100644 index 0000000..fde35ea --- /dev/null +++ b/frontend/libs/quote-resolver-lib/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "es2022", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "extends": "../../../tsconfig.base.json", + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/frontend/libs/quote-resolver-lib/tsconfig.lib.json b/frontend/libs/quote-resolver-lib/tsconfig.lib.json new file mode 100644 index 0000000..9b49be7 --- /dev/null +++ b/frontend/libs/quote-resolver-lib/tsconfig.lib.json @@ -0,0 +1,17 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "declaration": true, + "declarationMap": true, + "inlineSources": true, + "types": [] + }, + "exclude": [ + "src/**/*.spec.ts", + "src/test-setup.ts", + "jest.config.ts", + "src/**/*.test.ts" + ], + "include": ["src/**/*.ts"] +} diff --git a/frontend/libs/quote-resolver-lib/tsconfig.spec.json b/frontend/libs/quote-resolver-lib/tsconfig.spec.json new file mode 100644 index 0000000..f858ef7 --- /dev/null +++ b/frontend/libs/quote-resolver-lib/tsconfig.spec.json @@ -0,0 +1,16 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "module": "commonjs", + "target": "es2016", + "types": ["jest", "node"] + }, + "files": ["src/test-setup.ts"], + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/frontend/libs/service-lib/README.md b/frontend/libs/service-lib/README.md new file mode 100644 index 0000000..830b315 --- /dev/null +++ b/frontend/libs/service-lib/README.md @@ -0,0 +1,7 @@ +# service-lib + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test service-lib` to execute the unit tests. diff --git a/frontend/libs/service-lib/eslint.config.cjs b/frontend/libs/service-lib/eslint.config.cjs new file mode 100644 index 0000000..39a2d94 --- /dev/null +++ b/frontend/libs/service-lib/eslint.config.cjs @@ -0,0 +1,34 @@ +const nx = require('@nx/eslint-plugin'); +const baseConfig = require('../../../eslint.config.cjs'); + +module.exports = [ + ...baseConfig, + ...nx.configs['flat/angular'], + ...nx.configs['flat/angular-template'], + { + files: ['**/*.ts'], + rules: { + '@angular-eslint/directive-selector': [ + 'error', + { + type: 'attribute', + prefix: 'lib', + style: 'camelCase', + }, + ], + '@angular-eslint/component-selector': [ + 'error', + { + type: 'element', + prefix: 'lib', + style: 'kebab-case', + }, + ], + }, + }, + { + files: ['**/*.html'], + // Override or add rules here + rules: {}, + }, +]; diff --git a/frontend/libs/service-lib/jest.config.ts b/frontend/libs/service-lib/jest.config.ts new file mode 100644 index 0000000..b7ba779 --- /dev/null +++ b/frontend/libs/service-lib/jest.config.ts @@ -0,0 +1,21 @@ +export default { + displayName: 'service-lib', + preset: '../../../jest.preset.js', + setupFilesAfterEnv: ['/src/test-setup.ts'], + coverageDirectory: '../../../coverage/frontend/libs/service-lib', + transform: { + '^.+\\.(ts|mjs|js|html)$': [ + 'jest-preset-angular', + { + tsconfig: '/tsconfig.spec.json', + stringifyContentPathRegex: '\\.(html|svg)$', + }, + ], + }, + transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'], + snapshotSerializers: [ + 'jest-preset-angular/build/serializers/no-ng-attributes', + 'jest-preset-angular/build/serializers/ng-snapshot', + 'jest-preset-angular/build/serializers/html-comment', + ], +}; diff --git a/frontend/libs/service-lib/project.json b/frontend/libs/service-lib/project.json new file mode 100644 index 0000000..41231cb --- /dev/null +++ b/frontend/libs/service-lib/project.json @@ -0,0 +1,20 @@ +{ + "name": "service-lib", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "frontend/libs/service-lib/src", + "prefix": "lib", + "projectType": "library", + "tags": ["service"], + "targets": { + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "frontend/libs/service-lib/jest.config.ts" + } + }, + "lint": { + "executor": "@nx/eslint:lint" + } + } +} diff --git a/frontend/libs/service-lib/src/index.ts b/frontend/libs/service-lib/src/index.ts new file mode 100644 index 0000000..2852973 --- /dev/null +++ b/frontend/libs/service-lib/src/index.ts @@ -0,0 +1 @@ +export * from './lib/service-lib/quote.service'; diff --git a/frontend/libs/service-lib/src/lib/service-lib/__tests__/quote.service.spec.ts b/frontend/libs/service-lib/src/lib/service-lib/__tests__/quote.service.spec.ts new file mode 100644 index 0000000..d9f60ce --- /dev/null +++ b/frontend/libs/service-lib/src/lib/service-lib/__tests__/quote.service.spec.ts @@ -0,0 +1,104 @@ +import { HttpClient } from '@angular/common/http'; +import { TestBed } from '@angular/core/testing'; +import { fakerEN } from '@faker-js/faker'; +import { QuoteRequestDto, QuoteResponseDto } from '@target/interfaces'; +import { firstValueFrom, of, throwError } from 'rxjs'; + +import { QuoteService } from '../quote.service'; + +describe('QuoteService', () => { + let service: QuoteService; + let httpClient: { post: jest.Mock; get: jest.Mock }; + + const mockResponse: QuoteResponseDto = { + basisdaten: { + geburtsdatum: '1990-01-01', + versicherungsbeginn: '2024-01-01', + garantieniveau: '90%', + alterBeiRentenbeginn: 67, + aufschubdauer: 30, + beitragszahlungsdauer: 10, + }, + leistungsmerkmale: { + garantierteMindestrente: 50000, + einmaligesGarantiekapital: 25000, + todesfallleistungAbAltersrentenbezug: 40000, + }, + beitrag: { + einmalbeitrag: 0, + beitragsdynamik: '1,5%', + }, + }; + + beforeEach(() => { + httpClient = { post: jest.fn(), get: jest.fn() }; + TestBed.configureTestingModule({ + providers: [QuoteService, { provide: HttpClient, useValue: httpClient }], + }); + service = TestBed.inject(QuoteService); + }); + + it('should be created', () => { + expect(service).toBeDefined(); + }); + + describe('calculateQuote', () => { + it('should call the correct endpoint with quote request data', async () => { + const geburtsdatum = '2000-01-01'; + + const mockRequest: QuoteRequestDto = { + geburtsdatum, + beitrag: 1000, + laufzeit: 12, + leistungsVorgabe: 'Beitrag', + berechnungDerLaufzeit: 'Alter bei Rentenbeginn', + beitragszahlungsweise: 'Monatliche Beiträge', + }; + + httpClient.post.mockReturnValue(of(mockResponse)); + + const response = await firstValueFrom( + service.calculateQuote(mockRequest) + ); + + expect(response).toEqual(mockResponse); + expect(httpClient.post).toHaveBeenCalledWith('/api/quote', mockRequest); + }); + + it('should propagate errors from the API', async () => { + const geburtsdatum = '2000-01-01'; + + const mockRequest: QuoteRequestDto = { + geburtsdatum, + beitrag: 1000, + laufzeit: 12, + leistungsVorgabe: 'Beitrag', + berechnungDerLaufzeit: 'Alter bei Rentenbeginn', + beitragszahlungsweise: 'Monatliche Beiträge', + }; + const errorResponse = new Error('API Error'); + + httpClient.post.mockReturnValue(throwError(() => errorResponse)); + + await expect( + firstValueFrom(service.calculateQuote(mockRequest)) + ).rejects.toBe(errorResponse); + expect(httpClient.post).toHaveBeenCalledWith('/api/quote', mockRequest); + }); + }); + + describe('fetchQuote', () => { + it('should fetch quote data', async () => { + const id = fakerEN.string.alpha({ length: 15 }); + + const mockTestResponse: QuoteResponseDto = { ...mockResponse, id }; + + httpClient.get.mockReturnValue(of(mockTestResponse)); + + const response = await firstValueFrom(service.fetchQuote(id)); + + expect(response).toEqual(mockTestResponse); + expect(httpClient.get).toHaveBeenCalledWith(`/api/quote/${id}`); + }); + }); +}); diff --git a/frontend/libs/input-lib/src/lib/input-lib/store/services/quote.service.ts b/frontend/libs/service-lib/src/lib/service-lib/quote.service.ts similarity index 77% rename from frontend/libs/input-lib/src/lib/input-lib/store/services/quote.service.ts rename to frontend/libs/service-lib/src/lib/service-lib/quote.service.ts index 5531f5a..a722dda 100644 --- a/frontend/libs/input-lib/src/lib/input-lib/store/services/quote.service.ts +++ b/frontend/libs/service-lib/src/lib/service-lib/quote.service.ts @@ -10,4 +10,8 @@ export class QuoteService { calculateQuote(quoteDto: QuoteRequestDto): Observable { return this.http.post('/api/quote', quoteDto); } + + fetchQuote(quoteId: string): Observable { + return this.http.get(`/api/quote/${quoteId}`); + } } diff --git a/frontend/libs/service-lib/src/test-setup.ts b/frontend/libs/service-lib/src/test-setup.ts new file mode 100644 index 0000000..ea41401 --- /dev/null +++ b/frontend/libs/service-lib/src/test-setup.ts @@ -0,0 +1,6 @@ +import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone'; + +setupZoneTestEnv({ + errorOnUnknownElements: true, + errorOnUnknownProperties: true, +}); diff --git a/frontend/libs/service-lib/tsconfig.json b/frontend/libs/service-lib/tsconfig.json new file mode 100644 index 0000000..fde35ea --- /dev/null +++ b/frontend/libs/service-lib/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "es2022", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "extends": "../../../tsconfig.base.json", + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/frontend/libs/service-lib/tsconfig.lib.json b/frontend/libs/service-lib/tsconfig.lib.json new file mode 100644 index 0000000..9b49be7 --- /dev/null +++ b/frontend/libs/service-lib/tsconfig.lib.json @@ -0,0 +1,17 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "declaration": true, + "declarationMap": true, + "inlineSources": true, + "types": [] + }, + "exclude": [ + "src/**/*.spec.ts", + "src/test-setup.ts", + "jest.config.ts", + "src/**/*.test.ts" + ], + "include": ["src/**/*.ts"] +} diff --git a/frontend/libs/service-lib/tsconfig.spec.json b/frontend/libs/service-lib/tsconfig.spec.json new file mode 100644 index 0000000..f858ef7 --- /dev/null +++ b/frontend/libs/service-lib/tsconfig.spec.json @@ -0,0 +1,16 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "module": "commonjs", + "target": "es2016", + "types": ["jest", "node"] + }, + "files": ["src/test-setup.ts"], + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/frontend/libs/view-lib/README.md b/frontend/libs/view-lib/README.md new file mode 100644 index 0000000..7b24128 --- /dev/null +++ b/frontend/libs/view-lib/README.md @@ -0,0 +1,7 @@ +# view-lib + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test view-lib` to execute the unit tests. diff --git a/frontend/libs/view-lib/eslint.config.cjs b/frontend/libs/view-lib/eslint.config.cjs new file mode 100644 index 0000000..39a2d94 --- /dev/null +++ b/frontend/libs/view-lib/eslint.config.cjs @@ -0,0 +1,34 @@ +const nx = require('@nx/eslint-plugin'); +const baseConfig = require('../../../eslint.config.cjs'); + +module.exports = [ + ...baseConfig, + ...nx.configs['flat/angular'], + ...nx.configs['flat/angular-template'], + { + files: ['**/*.ts'], + rules: { + '@angular-eslint/directive-selector': [ + 'error', + { + type: 'attribute', + prefix: 'lib', + style: 'camelCase', + }, + ], + '@angular-eslint/component-selector': [ + 'error', + { + type: 'element', + prefix: 'lib', + style: 'kebab-case', + }, + ], + }, + }, + { + files: ['**/*.html'], + // Override or add rules here + rules: {}, + }, +]; diff --git a/frontend/libs/view-lib/jest.config.ts b/frontend/libs/view-lib/jest.config.ts new file mode 100644 index 0000000..e185fe7 --- /dev/null +++ b/frontend/libs/view-lib/jest.config.ts @@ -0,0 +1,21 @@ +export default { + displayName: 'view-lib', + preset: '../../../jest.preset.js', + setupFilesAfterEnv: ['/src/test-setup.ts'], + coverageDirectory: '../../../coverage/frontend/libs/view-lib', + transform: { + '^.+\\.(ts|mjs|js|html)$': [ + 'jest-preset-angular', + { + tsconfig: '/tsconfig.spec.json', + stringifyContentPathRegex: '\\.(html|svg)$', + }, + ], + }, + transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'], + snapshotSerializers: [ + 'jest-preset-angular/build/serializers/no-ng-attributes', + 'jest-preset-angular/build/serializers/ng-snapshot', + 'jest-preset-angular/build/serializers/html-comment', + ], +}; diff --git a/frontend/libs/view-lib/project.json b/frontend/libs/view-lib/project.json new file mode 100644 index 0000000..2405cec --- /dev/null +++ b/frontend/libs/view-lib/project.json @@ -0,0 +1,24 @@ +{ + "name": "view-lib", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "frontend/libs/view-lib/src", + "prefix": "lib", + "projectType": "library", + "tags": [ + "ui" + ], + "targets": { + "test": { + "executor": "@nx/jest:jest", + "outputs": [ + "{workspaceRoot}/coverage/{projectRoot}" + ], + "options": { + "jestConfig": "frontend/libs/view-lib/jest.config.ts" + } + }, + "lint": { + "executor": "@nx/eslint:lint" + } + } +} diff --git a/frontend/libs/view-lib/src/index.ts b/frontend/libs/view-lib/src/index.ts new file mode 100644 index 0000000..86fd00c --- /dev/null +++ b/frontend/libs/view-lib/src/index.ts @@ -0,0 +1 @@ +export * from './lib/view-lib/view-lib.component'; diff --git a/frontend/libs/view-lib/src/lib/view-lib/__tests__/view-lib.component.spec.ts b/frontend/libs/view-lib/src/lib/view-lib/__tests__/view-lib.component.spec.ts new file mode 100644 index 0000000..102d93f --- /dev/null +++ b/frontend/libs/view-lib/src/lib/view-lib/__tests__/view-lib.component.spec.ts @@ -0,0 +1,70 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { QuoteStore } from '@target/quota-store-lib'; + +import { ViewLibComponent } from '../view-lib.component'; + +describe('ViewLibComponent', () => { + let component: ViewLibComponent; + let fixture: ComponentFixture; + + // @ts-ignore + let consoleSpy: any; + + const mockQuoteState = { + basisdaten: { + geburtsdatum: '2020-01-01', + versicherungsbeginn: '2025-01-01', + garantieniveau: '', + alterBeiRentenbeginn: 0, + aufschubdauer: 0, + beitragszahlungsdauer: 0, + }, + leistungsmerkmale: { + garantierteMindestrente: 0, + einmaligesGarantiekapital: 0, + todesfallleistungAbAltersrentenbezug: 0, + }, + beitrag: { + einmalbeitrag: 0, + beitragsdynamik: '', + }, + }; + + beforeEach(async () => { + // Prevent 'Could not parse CSS stylesheet' exception from running the test. Apparently a bug with nx-spinner css library + consoleSpy = jest + .spyOn(global.console, 'error') + .mockImplementation((message) => { + if (!message.toString().includes('Could not parse CSS stylesheet')) { + global.console.warn(message); + } + }); + + await TestBed.configureTestingModule({ + imports: [ViewLibComponent], + providers: [ + { + provide: QuoteStore, + useValue: { + quoteState: jest.fn().mockReturnValue(mockQuoteState), + fetchQuote: jest.fn().mockResolvedValue(undefined), + }, + }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(ViewLibComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + afterAll(() => consoleSpy.mockRestore()); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should have state property with QuoteStore data', () => { + expect(component['state']).toEqual(mockQuoteState); + }); +}); diff --git a/frontend/libs/view-lib/src/lib/view-lib/view-lib.component.html b/frontend/libs/view-lib/src/lib/view-lib/view-lib.component.html new file mode 100644 index 0000000..b42f26c --- /dev/null +++ b/frontend/libs/view-lib/src/lib/view-lib/view-lib.component.html @@ -0,0 +1,80 @@ +
+
+
+
+
+ + Einmalbeitrag: + {{ state.beitrag.einmalbeitrag | currency : 'EUR' }} + +
+
+
+
+ + Garantierte + Mindestrente: {{ state.leistungsmerkmale.garantierteMindestrente | currency : 'EUR' }} + +
+
+
+
+ +
+
+

Basisdaten

+
+
+ {{ state.basisdaten.geburtsdatum }} +
+
+ {{ state.basisdaten.versicherungsbeginn }} +
+
+ {{ state.basisdaten.garantieniveau }} +
+
+ {{ state.basisdaten.alterBeiRentenbeginn }} +
+
+ {{ state.basisdaten.aufschubdauer }} +
+
+ {{ state.basisdaten.beitragszahlungsdauer }} + +
+
+ +

Leistungsmerkmale

+
+
+ {{ state.leistungsmerkmale.garantierteMindestrente }} + +
+
+ {{ state.leistungsmerkmale.einmaligesGarantiekapital }} + +
+
+ {{ state.leistungsmerkmale.todesfallleistungAbAltersrentenbezug }} + +
+
+ +

Beitrag

+
+
+ {{ state.beitrag.einmalbeitrag }} +
+
+ {{ state.beitrag.beitragsdynamik }} +
+
+
+ +
+
diff --git a/frontend/libs/view-lib/src/lib/view-lib/view-lib.component.ts b/frontend/libs/view-lib/src/lib/view-lib/view-lib.component.ts new file mode 100644 index 0000000..079248b --- /dev/null +++ b/frontend/libs/view-lib/src/lib/view-lib/view-lib.component.ts @@ -0,0 +1,28 @@ +import { CommonModule } from '@angular/common'; +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; +import { NxDataDisplayComponent } from '@aposin/ng-aquila/data-display'; +import { + NxColComponent, + NxLayoutComponent, + NxRowComponent, +} from '@aposin/ng-aquila/grid'; +import { QuoteStore } from '@target/quota-store-lib'; + +@Component({ + selector: 'lib-view-lib', + imports: [ + CommonModule, + NxColComponent, + NxLayoutComponent, + NxRowComponent, + NxDataDisplayComponent, + ], + templateUrl: './view-lib.component.html', + standalone: true, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ViewLibComponent { + protected readonly quoteStore = inject(QuoteStore); + + protected readonly state = this.quoteStore.quoteState(); +} diff --git a/frontend/libs/view-lib/src/test-setup.ts b/frontend/libs/view-lib/src/test-setup.ts new file mode 100644 index 0000000..ea41401 --- /dev/null +++ b/frontend/libs/view-lib/src/test-setup.ts @@ -0,0 +1,6 @@ +import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone'; + +setupZoneTestEnv({ + errorOnUnknownElements: true, + errorOnUnknownProperties: true, +}); diff --git a/frontend/libs/view-lib/tsconfig.json b/frontend/libs/view-lib/tsconfig.json new file mode 100644 index 0000000..fde35ea --- /dev/null +++ b/frontend/libs/view-lib/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "es2022", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "extends": "../../../tsconfig.base.json", + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/frontend/libs/view-lib/tsconfig.lib.json b/frontend/libs/view-lib/tsconfig.lib.json new file mode 100644 index 0000000..64216c9 --- /dev/null +++ b/frontend/libs/view-lib/tsconfig.lib.json @@ -0,0 +1,17 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../dist/out-tsc", + "declaration": true, + "declarationMap": true, + "inlineSources": true, + "types": [] + }, + "exclude": [ + "src/**/*.spec.ts", + "src/test-setup.ts", + "jest.config.ts", + "src/**/*.test.ts" + ], + "include": ["src/**/*.ts"] +} diff --git a/frontend/libs/view-lib/tsconfig.spec.json b/frontend/libs/view-lib/tsconfig.spec.json new file mode 100644 index 0000000..aad9b06 --- /dev/null +++ b/frontend/libs/view-lib/tsconfig.spec.json @@ -0,0 +1,18 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../dist/out-tsc", + "module": "commonjs", + "target": "es2016", + "types": ["jest", "node"] + }, + "files": [ + "src/test-setup.ts" + ], + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/package-lock.json b/package-lock.json index ad9517c..b5c7cd3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,15 +21,15 @@ "@nestjs/common": "^10.0.2", "@nestjs/core": "^10.0.2", "@nestjs/platform-express": "^10.0.2", - "@softarc/native-federation-node": "^2.0.10", "axios": "^1.6.0", + "dayjs": "^1.11.13", "es-module-shims": "^1.5.12", + "moment": "^2.30.1", "reflect-metadata": "^0.1.13", "rxjs": "~7.8.0", "zone.js": "~0.15.0" }, "devDependencies": { - "@angular-architects/native-federation": "^19.0.2", "@angular-devkit/build-angular": "~19.0.0", "@angular-devkit/core": "~19.0.0", "@angular-devkit/schematics": "~19.0.0", @@ -37,7 +37,9 @@ "@angular/compiler-cli": "~19.0.0", "@angular/language-service": "~19.0.0", "@aposin/ng-aquila": "^18.6.1", + "@eslint/eslintrc": "^3.2.0", "@eslint/js": "^9.8.0", + "@faker-js/faker": "^9.6.0", "@nestjs/schematics": "^10.0.1", "@nestjs/testing": "^10.0.2", "@ngrx/signals": "^19.0.0", @@ -62,6 +64,7 @@ "eslint": "^9.8.0", "eslint-config-prettier": "^9.0.0", "eslint-plugin-html": "^8.1.2", + "eslint-plugin-jest-formatting": "^3.1.0", "eslint-plugin-simple-import-sort": "^12.1.1", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", @@ -97,24 +100,6 @@ "node": ">=6.0.0" } }, - "node_modules/@angular-architects/native-federation": { - "version": "19.0.4", - "resolved": "https://registry.npmjs.org/@angular-architects/native-federation/-/native-federation-19.0.4.tgz", - "integrity": "sha512-NzhLuwZVU7W48pI9rGKbZsmusbkWcG1aABFwjye6CbT75xPnBNlVQUTxUE2PrRIA1DIj6XXtNn2bkT6TSVHT0g==", - "dev": true, - "dependencies": { - "@babel/core": "^7.19.0", - "@chialab/esbuild-plugin-commonjs": "^0.18.0", - "@softarc/native-federation": "2.0.17", - "@softarc/native-federation-runtime": "2.0.17", - "@types/browser-sync": "^2.29.0", - "browser-sync": "^3.0.2", - "esbuild": "^0.19.5", - "mrmime": "^1.0.1", - "npmlog": "^6.0.2", - "process": "0.11.10" - } - }, "node_modules/@angular-devkit/architect": { "version": "0.1900.7", "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1900.7.tgz", @@ -3371,65 +3356,6 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, - "node_modules/@chialab/cjs-to-esm": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/@chialab/cjs-to-esm/-/cjs-to-esm-0.18.0.tgz", - "integrity": "sha512-fm8X9NhPO5pyUB7gxOZgwxb8lVq1UD4syDJCpqh6x4zGME6RTck7BguWZ4Zgv3GML4fQ4KZtyRwP5eoDgNGrmA==", - "dev": true, - "dependencies": { - "@chialab/estransform": "^0.18.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@chialab/esbuild-plugin-commonjs": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/@chialab/esbuild-plugin-commonjs/-/esbuild-plugin-commonjs-0.18.0.tgz", - "integrity": "sha512-qZjIsNr1dVEJk6NLyza3pJLHeY7Fz0xjmYteKXElCnlFSKR7vVg6d18AsxVpRnP5qNbvx3XlOvs9U8j97ZQ6bw==", - "dev": true, - "dependencies": { - "@chialab/cjs-to-esm": "^0.18.0", - "@chialab/esbuild-rna": "^0.18.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@chialab/esbuild-rna": { - "version": "0.18.2", - "resolved": "https://registry.npmjs.org/@chialab/esbuild-rna/-/esbuild-rna-0.18.2.tgz", - "integrity": "sha512-ckzskez7bxstVQ4c5cxbx0DRP2teldzrcSGQl2KPh1VJGdO2ZmRrb6vNkBBD5K3dx9tgTyvskWp4dV+Fbg07Ag==", - "dev": true, - "dependencies": { - "@chialab/estransform": "^0.18.0", - "@chialab/node-resolve": "^0.18.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@chialab/estransform": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@chialab/estransform/-/estransform-0.18.1.tgz", - "integrity": "sha512-W/WmjpQL2hndD0/XfR0FcPBAUj+aLNeoAVehOjV/Q9bSnioz0GVSAXXhzp59S33ZynxJBBfn8DNiMTVNJmk4Aw==", - "dev": true, - "dependencies": { - "@parcel/source-map": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@chialab/node-resolve": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/@chialab/node-resolve/-/node-resolve-0.18.0.tgz", - "integrity": "sha512-eV1m70Qn9pLY9xwFmZ2FlcOzwiaUywsJ7NB/ud8VB7DouvCQtIHkQ3Om7uPX0ojXGEG1LCyO96kZkvbNTxNu0Q==", - "dev": true, - "engines": { - "node": ">=18" - } - }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -4070,6 +3996,22 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@faker-js/faker": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.6.0.tgz", + "integrity": "sha512-3vm4by+B5lvsFPSyep3ELWmZfE3kicDtmemVpuwl1yH7tqtnHdsA6hG8fbXedMVdkzgtvzWoRgjSB4Q+FHnZiw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/fakerjs" + } + ], + "engines": { + "node": ">=18.0.0", + "npm": ">=9.0.0" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -8012,18 +7954,6 @@ "yargs-parser": "21.1.1" } }, - "node_modules/@parcel/source-map": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@parcel/source-map/-/source-map-2.1.1.tgz", - "integrity": "sha512-Ejx1P/mj+kMjQb8/y5XxDUn4reGdr+WyKYloBljpppUy8gs42T+BNoEOuRYqDVdgPc6NxduzIDoJS9pOFfV5Ew==", - "dev": true, - "dependencies": { - "detect-libc": "^1.0.3" - }, - "engines": { - "node": "^12.18.3 || >=14" - } - }, "node_modules/@parcel/watcher": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.0.tgz", @@ -8910,37 +8840,6 @@ "@sinonjs/commons": "^3.0.0" } }, - "node_modules/@socket.io/component-emitter": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", - "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", - "dev": true - }, - "node_modules/@softarc/native-federation": { - "version": "2.0.17", - "resolved": "https://registry.npmjs.org/@softarc/native-federation/-/native-federation-2.0.17.tgz", - "integrity": "sha512-62KfvQNUBQheur45e+zaIgkTtIcQY2Xp7qsnxuVTVyv07xKY0y2aSTC3jWX3XnCwnV6do5YVwxlVUWhT4xzgyA==", - "dev": true, - "dependencies": { - "@softarc/native-federation-runtime": "2.0.17", - "json5": "^2.2.0", - "npmlog": "^6.0.2" - } - }, - "node_modules/@softarc/native-federation-node": { - "version": "2.0.17", - "resolved": "https://registry.npmjs.org/@softarc/native-federation-node/-/native-federation-node-2.0.17.tgz", - "integrity": "sha512-JKM1nLeFp3rwQZoqb+ANkyYUj7tX6Bo4JZzzZHpnsEZ6f/atuB3MK8E2rBnADn+fAsbOBjQHnYFfnW9KrswrkQ==" - }, - "node_modules/@softarc/native-federation-runtime": { - "version": "2.0.17", - "resolved": "https://registry.npmjs.org/@softarc/native-federation-runtime/-/native-federation-runtime-2.0.17.tgz", - "integrity": "sha512-FepqL1l5kpcQSVGdcix1SyVY4bfxpWTeLM0EGp+b2B8IJz6YDo0ErEzuMy7VIgCXoBIu895+pjAo+WnbHddiwQ==", - "dev": true, - "dependencies": { - "tslib": "^2.3.0" - } - }, "node_modules/@swc-node/core": { "version": "1.13.3", "resolved": "https://registry.npmjs.org/@swc-node/core/-/core-1.13.3.tgz", @@ -9360,78 +9259,6 @@ "@types/node": "*" } }, - "node_modules/@types/browser-sync": { - "version": "2.29.0", - "resolved": "https://registry.npmjs.org/@types/browser-sync/-/browser-sync-2.29.0.tgz", - "integrity": "sha512-d2V8FDX/LbDCSm343N2VChzDxvll0h76I8oSigYpdLgPDmcdcR6fywTggKBkUiDM3qAbHOq7NZvepj/HJM5e2g==", - "dev": true, - "dependencies": { - "@types/micromatch": "^2", - "@types/node": "*", - "@types/serve-static": "*", - "chokidar": "^3.0.0" - } - }, - "node_modules/@types/browser-sync/node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/@types/browser-sync/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/@types/browser-sync/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/@types/browser-sync/node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, "node_modules/@types/connect": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", @@ -9451,21 +9278,6 @@ "@types/node": "*" } }, - "node_modules/@types/cookie": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", - "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==", - "dev": true - }, - "node_modules/@types/cors": { - "version": "2.8.17", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", - "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/eslint": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", @@ -9603,15 +9415,6 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, - "node_modules/@types/micromatch": { - "version": "2.3.35", - "resolved": "https://registry.npmjs.org/@types/micromatch/-/micromatch-2.3.35.tgz", - "integrity": "sha512-J749bHo/Zu56w0G0NI/IGHLQPiSsjx//0zJhfEVAN95K/xM5C8ZDmhkXtU3qns0sBOao7HuQzr8XV1/2o5LbXA==", - "dev": true, - "dependencies": { - "@types/parse-glob": "*" - } - }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -9633,12 +9436,6 @@ "@types/node": "*" } }, - "node_modules/@types/parse-glob": { - "version": "3.0.32", - "resolved": "https://registry.npmjs.org/@types/parse-glob/-/parse-glob-3.0.32.tgz", - "integrity": "sha512-n4xmml2WKR12XeQprN8L/sfiVPa8FHS3k+fxp4kSr/PA2GsGUgFND+bvISJxM0y5QdvzNEGjEVU3eIrcKks/pA==", - "dev": true - }, "node_modules/@types/parse-json": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", @@ -10479,40 +10276,6 @@ "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==" }, - "node_modules/aproba": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", - "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", - "dev": true - }, - "node_modules/are-we-there-yet": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", - "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", - "deprecated": "This package is no longer supported.", - "dev": true, - "dependencies": { - "delegates": "^1.0.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/are-we-there-yet/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -10563,15 +10326,6 @@ "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", "dev": true }, - "node_modules/async-each-series": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/async-each-series/-/async-each-series-0.1.1.tgz", - "integrity": "sha512-p4jj6Fws4Iy2m0iCmI2am2ZNZCgbdgE+P8F/8csmn2vx7ixXrO2zGcuNsD46X5uZSVecmkEy/M06X2vG8KD6dQ==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -10886,15 +10640,6 @@ } ] }, - "node_modules/base64id": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", - "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", - "dev": true, - "engines": { - "node": "^4.5.0 || >= 5.9" - } - }, "node_modules/basic-auth": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", @@ -11054,241 +10799,75 @@ "node": ">=8" } }, - "node_modules/browser-sync": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/browser-sync/-/browser-sync-3.0.3.tgz", - "integrity": "sha512-91hoBHKk1C4pGeD+oE9Ld222k2GNQEAsI5AElqR8iLLWNrmZR2LPP8B0h8dpld9u7kro5IEUB3pUb0DJ3n1cRQ==", - "dev": true, - "dependencies": { - "browser-sync-client": "^3.0.3", - "browser-sync-ui": "^3.0.3", - "bs-recipes": "1.3.4", - "chalk": "4.1.2", - "chokidar": "^3.5.1", - "connect": "3.6.6", - "connect-history-api-fallback": "^1", - "dev-ip": "^1.0.1", - "easy-extender": "^2.3.4", - "eazy-logger": "^4.0.1", - "etag": "^1.8.1", - "fresh": "^0.5.2", - "fs-extra": "3.0.1", - "http-proxy": "^1.18.1", - "immutable": "^3", - "micromatch": "^4.0.8", - "opn": "5.3.0", - "portscanner": "2.2.0", - "raw-body": "^2.3.2", - "resp-modifier": "6.0.2", - "rx": "4.1.0", - "send": "^0.19.0", - "serve-index": "^1.9.1", - "serve-static": "^1.16.2", - "server-destroy": "1.0.1", - "socket.io": "^4.4.1", - "ua-parser-js": "^1.0.33", - "yargs": "^17.3.1" + "node_modules/browserslist": { + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", + "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001688", + "electron-to-chromium": "^1.5.73", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.1" }, "bin": { - "browser-sync": "dist/bin.js" + "browserslist": "cli.js" }, "engines": { - "node": ">= 8.0.0" + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/browser-sync-client": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/browser-sync-client/-/browser-sync-client-3.0.3.tgz", - "integrity": "sha512-TOEXaMgYNjBYIcmX5zDlOdjEqCeCN/d7opf/fuyUD/hhGVCfP54iQIDhENCi012AqzYZm3BvuFl57vbwSTwkSQ==", + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", "dev": true, "dependencies": { - "etag": "1.8.1", - "fresh": "0.5.2", - "mitt": "^1.1.3" + "fast-json-stable-stringify": "2.x" }, "engines": { - "node": ">=8.0.0" + "node": ">= 6" } }, - "node_modules/browser-sync-ui": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/browser-sync-ui/-/browser-sync-ui-3.0.3.tgz", - "integrity": "sha512-FcGWo5lP5VodPY6O/f4pXQy5FFh4JK0f2/fTBsp0Lx1NtyBWs/IfPPJbW8m1ujTW/2r07oUXKTF2LYZlCZktjw==", + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", "dev": true, "dependencies": { - "async-each-series": "0.1.1", - "chalk": "4.1.2", - "connect-history-api-fallback": "^1", - "immutable": "^3", - "server-destroy": "1.0.1", - "socket.io-client": "^4.4.1", - "stream-throttle": "^0.1.3" + "node-int64": "^0.4.0" } }, - "node_modules/browser-sync/node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "node_modules/btoa": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz", + "integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==", "dev": true, - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" + "bin": { + "btoa": "bin/btoa.js" }, "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" + "node": ">= 0.4.0" } }, - "node_modules/browser-sync/node_modules/fs-extra": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-3.0.1.tgz", - "integrity": "sha512-V3Z3WZWVUYd8hoCL5xfXJCaHWYzmtwW5XWYSlLgERi8PWd8bx1kUHUk8L1BT57e49oKnDDD180mjfrHc1yA9rg==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.1.2", - "jsonfile": "^3.0.0", - "universalify": "^0.1.0" - } - }, - "node_modules/browser-sync/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/browser-sync/node_modules/jsonfile": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-3.0.1.tgz", - "integrity": "sha512-oBko6ZHlubVB5mRFkur5vgYR1UyqX+S6Y/oCfLhqNdcc2fYFlDpIoNc7AfKS1KOGcnNAkvsr0grLck9ANM815w==", - "dev": true, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/browser-sync/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/browser-sync/node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/browser-sync/node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "dev": true, - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/browserslist": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", - "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "caniuse-lite": "^1.0.30001688", - "electron-to-chromium": "^1.5.73", - "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.1" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/bs-logger": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", - "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", - "dev": true, - "dependencies": { - "fast-json-stable-stringify": "2.x" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/bs-recipes": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/bs-recipes/-/bs-recipes-1.3.4.tgz", - "integrity": "sha512-BXvDkqhDNxXEjeGM8LFkSbR+jzmP/CYpCiVKYn+soB1dDldeU15EBNDkwVXndKuX35wnNUaPd0qSoQEAkmQtMw==", - "dev": true - }, - "node_modules/bser": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", - "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", - "dev": true, - "dependencies": { - "node-int64": "^0.4.0" - } - }, - "node_modules/btoa": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz", - "integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==", - "dev": true, - "bin": { - "btoa": "bin/btoa.js" - }, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", "dev": true, "funding": [ { @@ -11815,15 +11394,6 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, - "node_modules/color-support": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", - "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", - "dev": true, - "bin": { - "color-support": "bin.js" - } - }, "node_modules/colord": { "version": "2.9.3", "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", @@ -11968,56 +11538,11 @@ "integrity": "sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==", "dev": true }, - "node_modules/connect": { - "version": "3.6.6", - "resolved": "https://registry.npmjs.org/connect/-/connect-3.6.6.tgz", - "integrity": "sha512-OO7axMmPpu/2XuX1+2Yrg0ddju31B6xLZMWkJ5rYBu4YRmRVlOjvlY6kw2FJKiAzyxGwnrDUAG4s1Pf0sbBMCQ==", - "dev": true, - "dependencies": { - "debug": "2.6.9", - "finalhandler": "1.1.0", - "parseurl": "~1.3.2", - "utils-merge": "1.0.1" - }, - "engines": { - "node": ">= 0.10.0" - } - }, - "node_modules/connect-history-api-fallback": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz", - "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==", - "dev": true, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/connect/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/connect/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - }, "node_modules/consola": { "version": "2.15.3", "resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz", "integrity": "sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==" }, - "node_modules/console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", - "dev": true - }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -12513,6 +12038,11 @@ "node": ">=4.0" } }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==" + }, "node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", @@ -12659,6 +12189,7 @@ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", "dev": true, + "optional": true, "bin": { "detect-libc": "bin/detect-libc.js" }, @@ -12698,18 +12229,6 @@ "node": ">= 4.0.0" } }, - "node_modules/dev-ip": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dev-ip/-/dev-ip-1.0.1.tgz", - "integrity": "sha512-LmVkry/oDShEgSZPNgqCIp2/TlqtExeGmymru3uCELnfyjY11IzpAproLYs+1X88fXO6DBoYP3ul2Xo2yz2j6A==", - "dev": true, - "bin": { - "dev-ip": "lib/dev-ip.js" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", @@ -12866,30 +12385,6 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true }, - "node_modules/easy-extender": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/easy-extender/-/easy-extender-2.3.4.tgz", - "integrity": "sha512-8cAwm6md1YTiPpOvDULYJL4ZS6WfM5/cTeVVh4JsvyYZAoqlRVUpHL9Gr5Fy7HA6xcSZicUia3DeAgO3Us8E+Q==", - "dev": true, - "dependencies": { - "lodash": "^4.17.10" - }, - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/eazy-logger": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/eazy-logger/-/eazy-logger-4.0.1.tgz", - "integrity": "sha512-2GSFtnnC6U4IEKhEI7+PvdxrmjJ04mdsj3wHZTFiw0tUtG4HCWzTr13ZYTk8XOGnA1xQMaDljoBOYlk3D/MMSw==", - "dev": true, - "dependencies": { - "chalk": "4.1.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -12981,134 +12476,6 @@ "once": "^1.4.0" } }, - "node_modules/engine.io": { - "version": "6.6.2", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.2.tgz", - "integrity": "sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw==", - "dev": true, - "dependencies": { - "@types/cookie": "^0.4.1", - "@types/cors": "^2.8.12", - "@types/node": ">=10.0.0", - "accepts": "~1.3.4", - "base64id": "2.0.0", - "cookie": "~0.7.2", - "cors": "~2.8.5", - "debug": "~4.3.1", - "engine.io-parser": "~5.2.1", - "ws": "~8.17.1" - }, - "engines": { - "node": ">=10.2.0" - } - }, - "node_modules/engine.io-client": { - "version": "6.6.2", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.2.tgz", - "integrity": "sha512-TAr+NKeoVTjEVW8P3iHguO1LO6RlUz9O5Y8o7EY0fU+gY1NYqas7NN3slpFtbXEsLMHk0h90fJMfKjRkQ0qUIw==", - "dev": true, - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1", - "engine.io-parser": "~5.2.1", - "ws": "~8.17.1", - "xmlhttprequest-ssl": "~2.1.1" - } - }, - "node_modules/engine.io-client/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dev": true, - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/engine.io-client/node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", - "dev": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/engine.io-parser": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", - "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", - "dev": true, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/engine.io/node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/engine.io/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dev": true, - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/engine.io/node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", - "dev": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/enhanced-resolve": { "version": "5.18.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.0.tgz", @@ -13251,6 +12618,7 @@ "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", "dev": true, "hasInstallScript": true, + "optional": true, "bin": { "esbuild": "bin/esbuild" }, @@ -13435,6 +12803,18 @@ "node": ">=16.0.0" } }, + "node_modules/eslint-plugin-jest-formatting": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest-formatting/-/eslint-plugin-jest-formatting-3.1.0.tgz", + "integrity": "sha512-XyysraZ1JSgGbLSDxjj5HzKKh0glgWf+7CkqxbTqb7zEhW7X2WHo5SBQ8cGhnszKN+2Lj3/oevBlHNbHezoc/A==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": ">=0.8.0" + } + }, "node_modules/eslint-plugin-simple-import-sort": { "version": "12.1.1", "resolved": "https://registry.npmjs.org/eslint-plugin-simple-import-sort/-/eslint-plugin-simple-import-sort-12.1.1.tgz", @@ -14079,69 +13459,6 @@ "node": ">=8" } }, - "node_modules/finalhandler": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.0.tgz", - "integrity": "sha512-ejnvM9ZXYzp6PUPUyQBMBf0Co5VX2gr5H2VQe2Ui2jWXNlxv+PYZo8wpAymJNJdLsG1R4p+M4aynF8KuoUEwRw==", - "dev": true, - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~1.0.1", - "escape-html": "~1.0.3", - "on-finished": "~2.3.0", - "parseurl": "~1.3.2", - "statuses": "~1.3.1", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/finalhandler/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/finalhandler/node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/finalhandler/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - }, - "node_modules/finalhandler/node_modules/on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", - "dev": true, - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/finalhandler/node_modules/statuses": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", - "integrity": "sha512-wuTCPGlJONk/a1kqZ4fQM2+908lC7fa7nPYpTC1EhnvqLX/IICbeP1OZGDtA374trpSq68YubKUMo8oRhN46yg==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/find-cache-dir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-4.0.0.tgz", @@ -14571,61 +13888,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gauge": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", - "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", - "deprecated": "This package is no longer supported.", - "dev": true, - "dependencies": { - "aproba": "^1.0.3 || ^2.0.0", - "color-support": "^1.1.3", - "console-control-strings": "^1.1.0", - "has-unicode": "^2.0.1", - "signal-exit": "^3.0.7", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wide-align": "^1.1.5" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/gauge/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "node_modules/gauge/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/gauge/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true - }, - "node_modules/gauge/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -14952,12 +14214,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", - "dev": true - }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -15357,15 +14613,6 @@ "node": ">=0.10.0" } }, - "node_modules/immutable": { - "version": "3.8.2", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz", - "integrity": "sha512-15gZoQ38eYjEjxkorfbcgBKBL6R7T459OuK+CpcWt7O3KF4uPCx2tD0uFETlUDIyo+1789crbMhTvQBSR5yBMg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -15642,15 +14889,6 @@ "node": ">=0.12.0" } }, - "node_modules/is-number-like": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/is-number-like/-/is-number-like-1.0.8.tgz", - "integrity": "sha512-6rZi3ezCyFcn5L71ywzz2bS5b2Igl1En3eTlZlvKjpz1n3IZLAYMbKYAIQgFmEu0GENg92ziU/faEOA/aixjbA==", - "dev": true, - "dependencies": { - "lodash.isfinite": "^3.3.2" - } - }, "node_modules/is-plain-obj": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", @@ -17108,12 +16346,6 @@ "url": "https://github.com/sponsors/antonk52" } }, - "node_modules/limiter": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", - "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==", - "dev": true - }, "node_modules/lines-and-columns": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-2.0.3.tgz", @@ -17276,12 +16508,6 @@ "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", "dev": true }, - "node_modules/lodash.isfinite": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/lodash.isfinite/-/lodash.isfinite-3.3.2.tgz", - "integrity": "sha512-7FGG40uhC8Mm633uKW1r58aElFlBlxCrg9JfSi3P6aYiWmfiWF0PgMd86ZUsxE5GwWPdHoS2+48bwTh2VPkIQA==", - "dev": true - }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -17878,12 +17104,6 @@ "node": ">= 18" } }, - "node_modules/mitt": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mitt/-/mitt-1.2.0.tgz", - "integrity": "sha512-r6lj77KlwqLhIUku9UWYes7KJtsczvolZkzp8hbaDPPaE24OmWl5s539Mytlj22siEQKosZ26qCBgda2PKwoJw==", - "dev": true - }, "node_modules/mkdirp": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", @@ -17895,13 +17115,12 @@ "mkdirp": "bin/cmd.js" } }, - "node_modules/mrmime": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz", - "integrity": "sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==", - "dev": true, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", "engines": { - "node": ">=10" + "node": "*" } }, "node_modules/ms": { @@ -18451,22 +17670,6 @@ "node": ">=8" } }, - "node_modules/npmlog": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", - "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", - "deprecated": "This package is no longer supported.", - "dev": true, - "dependencies": { - "are-we-there-yet": "^3.0.0", - "console-control-strings": "^1.1.0", - "gauge": "^4.0.3", - "set-blocking": "^2.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, "node_modules/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", @@ -18811,27 +18014,6 @@ "opener": "bin/opener-bin.js" } }, - "node_modules/opn": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/opn/-/opn-5.3.0.tgz", - "integrity": "sha512-bYJHo/LOmoTd+pfiYhfZDnf9zekVJrY+cnS2a5F2x+w5ppvTqObojTP7WiFG+kVZs9Inw+qQ/lw7TroWwhdd2g==", - "dev": true, - "dependencies": { - "is-wsl": "^1.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/opn/node_modules/is-wsl": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", - "integrity": "sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -19394,36 +18576,13 @@ "lodash": "^4.17.14" } }, - "node_modules/portfinder/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/portscanner": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/portscanner/-/portscanner-2.2.0.tgz", - "integrity": "sha512-IFroCz/59Lqa2uBvzK3bKDbDDIEaAY8XJ1jFxcLWTqosrsc32//P4VuSB2vZXoHiHqOmx8B5L5hnKOxL/7FlPw==", - "dev": true, - "dependencies": { - "async": "^2.6.0", - "is-number-like": "^1.0.3" - }, - "engines": { - "node": ">=0.4", - "npm": ">=1.0.0" - } - }, - "node_modules/portscanner/node_modules/async": { - "version": "2.6.4", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", - "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "node_modules/portfinder/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, "dependencies": { - "lodash": "^4.17.14" + "ms": "^2.1.1" } }, "node_modules/postcss": { @@ -20116,15 +19275,6 @@ "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", - "dev": true, - "engines": { - "node": ">= 0.6.0" - } - }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -20582,56 +19732,6 @@ "node": ">=10" } }, - "node_modules/resp-modifier": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/resp-modifier/-/resp-modifier-6.0.2.tgz", - "integrity": "sha512-U1+0kWC/+4ncRFYqQWTx/3qkfE6a4B/h3XXgmXypfa0SPZ3t7cbbaFk297PjQS/yov24R18h6OZe6iZwj3NSLw==", - "dev": true, - "dependencies": { - "debug": "^2.2.0", - "minimatch": "^3.0.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/resp-modifier/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/resp-modifier/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/resp-modifier/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/resp-modifier/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - }, "node_modules/restore-cursor": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", @@ -20810,12 +19910,6 @@ "queue-microtask": "^1.2.2" } }, - "node_modules/rx": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/rx/-/rx-4.1.0.tgz", - "integrity": "sha512-CiaiuN6gapkdl+cZUr67W6I8jquN4lkak3vtIsIWCl4XIPP8ffsoyN6/+PuGXnQy8Cu8W2y9Xxh31Rq4M6wUug==", - "dev": true - }, "node_modules/rxjs": { "version": "7.8.1", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", @@ -21022,45 +20116,6 @@ "node": ">=10" } }, - "node_modules/send": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.1.tgz", - "integrity": "sha512-p4rRk4f23ynFEfcD9LA0xRYngj+IyGiEYyqqOak8kaN0TvNmuxC2dcVeBn62GpCeR2CpWqyHCNScTP91QbAVFg==", - "dev": true, - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/send/node_modules/debug/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - }, "node_modules/serialize-javascript": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", @@ -21206,18 +20261,6 @@ "node": ">= 0.8" } }, - "node_modules/server-destroy": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/server-destroy/-/server-destroy-1.0.1.tgz", - "integrity": "sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ==", - "dev": true - }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", - "dev": true - }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -21418,151 +20461,6 @@ "npm": ">= 3.0.0" } }, - "node_modules/socket.io": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", - "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", - "dev": true, - "dependencies": { - "accepts": "~1.3.4", - "base64id": "~2.0.0", - "cors": "~2.8.5", - "debug": "~4.3.2", - "engine.io": "~6.6.0", - "socket.io-adapter": "~2.5.2", - "socket.io-parser": "~4.2.4" - }, - "engines": { - "node": ">=10.2.0" - } - }, - "node_modules/socket.io-adapter": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", - "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", - "dev": true, - "dependencies": { - "debug": "~4.3.4", - "ws": "~8.17.1" - } - }, - "node_modules/socket.io-adapter/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dev": true, - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/socket.io-adapter/node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", - "dev": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/socket.io-client": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz", - "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==", - "dev": true, - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.2", - "engine.io-client": "~6.6.1", - "socket.io-parser": "~4.2.4" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/socket.io-client/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dev": true, - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/socket.io-parser": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", - "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", - "dev": true, - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/socket.io-parser/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dev": true, - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/socket.io/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dev": true, - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "node_modules/sockjs": { "version": "0.3.24", "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", @@ -21800,22 +20698,6 @@ "node": ">= 0.8" } }, - "node_modules/stream-throttle": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/stream-throttle/-/stream-throttle-0.1.3.tgz", - "integrity": "sha512-889+B9vN9dq7/vLbGyuHeZ6/ctf5sNuGWsDy89uNxkFTAgzy0eK7+w5fL3KLNRTkLle7EgZGvHUphZW0Q26MnQ==", - "dev": true, - "dependencies": { - "commander": "^2.2.0", - "limiter": "^1.0.5" - }, - "bin": { - "throttleproxy": "bin/throttleproxy.js" - }, - "engines": { - "node": ">= 0.10.0" - } - }, "node_modules/streamroller": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.5.tgz", @@ -22862,32 +21744,6 @@ "typescript": ">=4.8.4 <5.8.0" } }, - "node_modules/ua-parser-js": { - "version": "1.0.40", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.40.tgz", - "integrity": "sha512-z6PJ8Lml+v3ichVojCiB8toQJBuwR42ySM4ezjXIqXK3M0HczmKQ3LF4rhU55PfD99KEEXQG6yb7iOMyvYuHew==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/ua-parser-js" - }, - { - "type": "paypal", - "url": "https://paypal.me/faisalman" - }, - { - "type": "github", - "url": "https://github.com/sponsors/faisalman" - } - ], - "bin": { - "ua-parser-js": "script/cli.js" - }, - "engines": { - "node": "*" - } - }, "node_modules/uid": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/uid/-/uid-2.0.2.tgz", @@ -24221,44 +23077,6 @@ "node": ">= 8" } }, - "node_modules/wide-align": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", - "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", - "dev": true, - "dependencies": { - "string-width": "^1.0.2 || 2 || 3 || 4" - } - }, - "node_modules/wide-align/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "node_modules/wide-align/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/wide-align/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/wildcard": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", @@ -24425,15 +23243,6 @@ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "dev": true }, - "node_modules/xmlhttprequest-ssl": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", - "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index 880f3a4..330a707 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "start": "npx nx run-many --target=serve --all", "start:shell": "nx run shell:serve", "start:bff": "nx run bff:serve", - "test": "nx run-many -t test --coverage" + "test": "nx run-many -t test --coverage", + "lint": "nx run-many -t lint" }, "private": true, "dependencies": { @@ -24,7 +25,9 @@ "@nestjs/core": "^10.0.2", "@nestjs/platform-express": "^10.0.2", "axios": "^1.6.0", + "dayjs": "^1.11.13", "es-module-shims": "^1.5.12", + "moment": "^2.30.1", "reflect-metadata": "^0.1.13", "rxjs": "~7.8.0", "zone.js": "~0.15.0" @@ -39,6 +42,7 @@ "@aposin/ng-aquila": "^18.6.1", "@eslint/eslintrc": "^3.2.0", "@eslint/js": "^9.8.0", + "@faker-js/faker": "^9.6.0", "@nestjs/schematics": "^10.0.1", "@nestjs/testing": "^10.0.2", "@ngrx/signals": "^19.0.0", diff --git a/shared/interfaces/src/index.ts b/shared/interfaces/src/index.ts index 3f60786..f5c08a1 100644 --- a/shared/interfaces/src/index.ts +++ b/shared/interfaces/src/index.ts @@ -1 +1,2 @@ +export * from './lib/api-error-response.interface'; export * from './lib/quote.interfaces'; diff --git a/shared/interfaces/src/lib/api-error-response.interface.ts b/shared/interfaces/src/lib/api-error-response.interface.ts new file mode 100644 index 0000000..6b9db1e --- /dev/null +++ b/shared/interfaces/src/lib/api-error-response.interface.ts @@ -0,0 +1,9 @@ +export interface ApiErrorResponse { + message: string | string[]; + statusCode: number; +} + +export interface ApiError { + message: string[]; + statusCode?: number; +} diff --git a/shared/interfaces/src/lib/quote.interfaces.ts b/shared/interfaces/src/lib/quote.interfaces.ts index ffabedc..1839792 100644 --- a/shared/interfaces/src/lib/quote.interfaces.ts +++ b/shared/interfaces/src/lib/quote.interfaces.ts @@ -3,12 +3,13 @@ import { InputDto } from '@target/validations'; export interface QuoteRequestDto extends InputDto {} export interface QuoteResponseDto { - basisdaten: QuoteBasisdatenDto; - leistungsmerkmale: QuoteLeistungsmerkmaleDto; - beitrag: QuoteBeitragDto; + id?: string; + basisdaten: IQuoteBasisdaten; + leistungsmerkmale: IQuoteLeistungsmerkmale; + beitrag: IQuoteBeitrag; } -interface QuoteBasisdatenDto { +interface IQuoteBasisdaten { geburtsdatum: string; versicherungsbeginn: string; garantieniveau: string; @@ -17,13 +18,13 @@ interface QuoteBasisdatenDto { beitragszahlungsdauer: number; } -interface QuoteLeistungsmerkmaleDto { +interface IQuoteLeistungsmerkmale { garantierteMindestrente: number; einmaligesGarantiekapital: number; todesfallleistungAbAltersrentenbezug: number; } -interface QuoteBeitragDto { +interface IQuoteBeitrag { einmalbeitrag: number; beitragsdynamik: string; } diff --git a/shared/validations/src/lib/input/beitragszahlungsweise.ts b/shared/validations/src/lib/input/beitragszahlungsweise.ts index 16fd71e..16b9f3e 100644 --- a/shared/validations/src/lib/input/beitragszahlungsweise.ts +++ b/shared/validations/src/lib/input/beitragszahlungsweise.ts @@ -1,7 +1,20 @@ +import { NxDropdownOption } from '@aposin/ng-aquila/dropdown'; import { z } from 'zod'; -export const BeitragszahlungsweiseEinmalbeitragSchema = z.literal('Einmalbeitrag'); -export const BeitragszahlungsweiseMonatlicheBeiträgeSchema = z.literal('Monatliche Beiträge'); +export const BeitragszahlungsweiseEinmalbeitragSchema = + z.literal('Einmalbeitrag'); +export const BeitragszahlungsweiseMonatlicheBeiträgeSchema = z.literal( + 'Monatliche Beiträge' +); -export const BeitragszahlungsweiseSchema = z.enum([BeitragszahlungsweiseEinmalbeitragSchema.value, BeitragszahlungsweiseMonatlicheBeiträgeSchema.value]); -export type Beitragszahlungsweise = z.infer; +export const BeitragszahlungsweiseSchema = z.enum([ + BeitragszahlungsweiseEinmalbeitragSchema.value, + BeitragszahlungsweiseMonatlicheBeiträgeSchema.value, +]); + +export const beitragszahlungsweiseOpts: NxDropdownOption[] = Object.entries( + BeitragszahlungsweiseSchema.enum +).map(([label, value]) => ({ + label, + value, +})); diff --git a/shared/validations/src/lib/input/berechnung-der-laufzeit.ts b/shared/validations/src/lib/input/berechnung-der-laufzeit.ts index 78e3469..f11d149 100644 --- a/shared/validations/src/lib/input/berechnung-der-laufzeit.ts +++ b/shared/validations/src/lib/input/berechnung-der-laufzeit.ts @@ -1,7 +1,20 @@ +import { NxDropdownOption } from '@aposin/ng-aquila/dropdown'; import { z } from 'zod'; -export const BerechnungDerLaufzeitAlterBeiRentenbeginnSchema = z.literal('Alter bei Rentenbeginn'); -export const BerechnungDerLaufzeitAufschubdauerSchema = z.literal('Aufschubdauer'); +export const BerechnungDerLaufzeitAlterBeiRentenbeginnSchema = z.literal( + 'Alter bei Rentenbeginn' +); +export const BerechnungDerLaufzeitAufschubdauerSchema = + z.literal('Aufschubdauer'); -export const BerechnungDerLaufzeitSchema = z.enum([BerechnungDerLaufzeitAlterBeiRentenbeginnSchema.value, BerechnungDerLaufzeitAufschubdauerSchema.value]); -export type BerechnungDerLaufzeit = z.infer; +export const BerechnungDerLaufzeitSchema = z.enum([ + BerechnungDerLaufzeitAlterBeiRentenbeginnSchema.value, + BerechnungDerLaufzeitAufschubdauerSchema.value, +]); + +export const berechnungDerLaufzeitOpts: NxDropdownOption[] = Object.entries( + BerechnungDerLaufzeitSchema.enum +).map(([label, value]) => ({ + label, + value, +})); diff --git a/shared/validations/src/lib/input/geburtsdatum.ts b/shared/validations/src/lib/input/geburtsdatum.ts new file mode 100644 index 0000000..1dbcf1d --- /dev/null +++ b/shared/validations/src/lib/input/geburtsdatum.ts @@ -0,0 +1,18 @@ +import moment from 'moment/moment'; +import { z } from 'zod'; + +export const geburtsdatumSchema = z.coerce + .string() + .min(1, { message: 'Date of birth is required' }) + .refine((val) => { + if (!val) return true; + + return moment(val).isValid(); + }, 'Please provide a valid date') + .refine((val) => { + const date = moment(val); + + if (!date.isValid()) return true; + + return date.isBefore(moment().subtract(18, 'years')); + }, 'You must be at least 18 years old to proceed'); diff --git a/shared/validations/src/lib/input/input.validations.ts b/shared/validations/src/lib/input/input.validations.ts index 350b841..88f770a 100644 --- a/shared/validations/src/lib/input/input.validations.ts +++ b/shared/validations/src/lib/input/input.validations.ts @@ -2,10 +2,12 @@ import { z } from 'zod'; import { BeitragszahlungsweiseSchema } from './beitragszahlungsweise'; import { BerechnungDerLaufzeitSchema } from './berechnung-der-laufzeit'; +import { geburtsdatumSchema } from './geburtsdatum'; import { LeistungsvorgabeSchema } from './leistungsvorgabe'; import { RentenzahlungsweiseSchema } from './rentenzahlungsweise'; export const InputDtoSchema = z.object({ + geburtsdatum: geburtsdatumSchema, leistungsVorgabe: LeistungsvorgabeSchema.nullish(), beitrag: z .number() @@ -15,7 +17,7 @@ export const InputDtoSchema = z.object({ laufzeit: z .number() .min(1, 'Die Laufzeit muss mindestens 1 Jahr betragen') - .max(100, 'Die Laufzeit darf höchstens 40 Jahre betragen'), + .max(40, 'Die Laufzeit darf höchstens 40 Jahre betragen'), beitragszahlungsweise: BeitragszahlungsweiseSchema.nullish(), rentenzahlungsweise: RentenzahlungsweiseSchema.nullish(), }); diff --git a/shared/validations/src/lib/input/leistungsvorgabe.ts b/shared/validations/src/lib/input/leistungsvorgabe.ts index 0623f44..763f40e 100644 --- a/shared/validations/src/lib/input/leistungsvorgabe.ts +++ b/shared/validations/src/lib/input/leistungsvorgabe.ts @@ -1,9 +1,13 @@ -import { z } from "zod"; +import { NxDropdownOption } from '@aposin/ng-aquila/dropdown'; +import { z } from 'zod'; export const LeistungsvorgabeBeitragSchema = z.literal('Beitrag'); export const LeistungsvorgabeEinmalbeitragSchema = z.literal('Einmalbeitrag'); -export const LeistungsvorgabeGarantierteMindestrenteSchema = z.literal('Garantierte Mindestrente'); -export const LeistungsvorgabeGarantiekapitalSchema = z.literal('Garantiekapital'); +export const LeistungsvorgabeGarantierteMindestrenteSchema = z.literal( + 'Garantierte Mindestrente' +); +export const LeistungsvorgabeGarantiekapitalSchema = + z.literal('Garantiekapital'); export const LeistungsvorgabeGesamtkapitalSchema = z.literal('Gesamtkapital'); export const LeistungsvorgabeGesamtrenteSchema = z.literal('Gesamtrente'); @@ -15,4 +19,10 @@ export const LeistungsvorgabeSchema = z.enum([ LeistungsvorgabeGesamtkapitalSchema.value, LeistungsvorgabeGesamtrenteSchema.value, ]); -export type Leistungsvorgabe = z.infer; + +export const leistungsVorgabeOpts: NxDropdownOption[] = Object.entries( + LeistungsvorgabeSchema.enum +).map(([label, value]) => ({ + label, + value, +})); diff --git a/shared/validations/src/lib/input/rentenzahlungsweise.ts b/shared/validations/src/lib/input/rentenzahlungsweise.ts index a6accae..8ba0c60 100644 --- a/shared/validations/src/lib/input/rentenzahlungsweise.ts +++ b/shared/validations/src/lib/input/rentenzahlungsweise.ts @@ -1,9 +1,26 @@ +import { NxDropdownOption } from '@aposin/ng-aquila/dropdown'; import { z } from 'zod'; -export const RentenzahlungsweiseMonatlichSchema = z.literal('Monatliche Renten'); -export const RentenzahlungsweiseVierteljaehrlichSchema = z.literal('Vierteljährliche Renten'); -export const RentenzahlungsweiseHalbjaehrlichSchema = z.literal('Halbjährliche Renten'); +export const RentenzahlungsweiseMonatlichSchema = + z.literal('Monatliche Renten'); +export const RentenzahlungsweiseVierteljaehrlichSchema = z.literal( + 'Vierteljährliche Renten' +); +export const RentenzahlungsweiseHalbjaehrlichSchema = z.literal( + 'Halbjährliche Renten' +); export const RentenzahlungsweiseJaehrlichSchema = z.literal('Jährliche Renten'); -export const RentenzahlungsweiseSchema = z.union([RentenzahlungsweiseMonatlichSchema, RentenzahlungsweiseVierteljaehrlichSchema, RentenzahlungsweiseHalbjaehrlichSchema, RentenzahlungsweiseJaehrlichSchema]); -export type Rentenzahlungsweise = z.infer; +export const RentenzahlungsweiseSchema = z.enum([ + RentenzahlungsweiseMonatlichSchema.value, + RentenzahlungsweiseVierteljaehrlichSchema.value, + RentenzahlungsweiseHalbjaehrlichSchema.value, + RentenzahlungsweiseJaehrlichSchema.value, +]); + +export const rentenzahlungsweiseOpts: NxDropdownOption[] = Object.entries( + RentenzahlungsweiseSchema.enum +).map(([label, value]) => ({ + label, + value, +})); diff --git a/shared/validations/tsconfig.json b/shared/validations/tsconfig.json index 6f7169a..6adb3c1 100644 --- a/shared/validations/tsconfig.json +++ b/shared/validations/tsconfig.json @@ -7,7 +7,8 @@ "noImplicitOverride": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, - "noPropertyAccessFromIndexSignature": true + "noPropertyAccessFromIndexSignature": true, + "esModuleInterop": true }, "files": [], "include": [], diff --git a/tsconfig.base.json b/tsconfig.base.json index f82281f..13892ce 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -15,9 +15,19 @@ "skipDefaultLibCheck": true, "baseUrl": ".", "paths": { + "@target/date-picker-lib": ["frontend/libs/date-picker-lib/src/index.ts"], + "@target/error-box-lib": ["frontend/libs/error-box-lib/src/index.ts"], "@target/input-lib": ["frontend/libs/input-lib/src/index.ts"], + "@target/input-store-lib": ["frontend/libs/input-store-lib/src/index.ts"], "@target/interfaces": ["shared/interfaces/src/index.ts"], - "@target/validations": ["shared/validations/src/index.ts"] + "@target/quota-store-lib": ["frontend/libs/quota-store-lib/src/index.ts"], + "@target/quote-resolver-lib": [ + "frontend/libs/quote-resolver-lib/src/index.ts" + ], + "@target/service-lib": ["frontend/libs/service-lib/src/index.ts"], + "@target/ui-lib": ["frontend/libs/ui-lib/src/index.ts"], + "@target/validations": ["shared/validations/src/index.ts"], + "@target/view-lib": ["frontend/libs/view-lib/src/index.ts"] } }, "exclude": ["node_modules", "tmp"]