Untitled
# Model Bazy Danych i Encje dla APK ## 1. Model Bazy Danych ### QuestionGroup ```sql CREATE TABLE question_group ( id BIGINT PRIMARY KEY, code VARCHAR(50) NOT NULL UNIQUE, -- MAIN_RISKS, ADDITIONAL_RISKS active BOOLEAN DEFAULT true ); ``` ### Question ```sql CREATE TABLE question ( id BIGINT PRIMARY KEY, group_id BIGINT REFERENCES question_group(id), question_order INT NOT NULL, active BOOLEAN DEFAULT true ); ``` ### QuestionContent ```sql CREATE TABLE question_content ( id BIGINT PRIMARY KEY, question_id BIGINT REFERENCES question(id), language_code VARCHAR(5), -- pl_PL, en_GB content TEXT NOT NULL ); ``` ### PathCondition ```sql CREATE TABLE path_condition ( id BIGINT PRIMARY KEY, code VARCHAR(50) NOT NULL UNIQUE, -- MINI_MIDI_MAXI, MINI_MIDI, etc. description TEXT, active BOOLEAN DEFAULT true ); ``` ### Answer ```sql CREATE TABLE answer ( id BIGINT PRIMARY KEY, question_id BIGINT REFERENCES question(id), next_question_id BIGINT REFERENCES question(id), answer_order INT NOT NULL, path_condition_id BIGINT REFERENCES path_condition(id), code VARCHAR(50) NOT NULL, -- BUDGET_IMPORTANT, FULL_PROTECTION, etc. active BOOLEAN DEFAULT true ); ``` ### AnswerContent ```sql CREATE TABLE answer_content ( id BIGINT PRIMARY KEY, answer_id BIGINT REFERENCES answer(id), language_code VARCHAR(5), content TEXT NOT NULL ); ``` ### Recommendation ```sql CREATE TABLE recommendation ( id BIGINT PRIMARY KEY, path_condition_id BIGINT REFERENCES path_condition(id), package_code VARCHAR(50) NOT NULL, -- MINI, MIDI, MAXI description TEXT, active BOOLEAN DEFAULT true ); ``` ### CustomerQuestionnaire ```sql CREATE TABLE customer_questionnaire ( id BIGINT PRIMARY KEY, customer_id BIGINT NOT NULL, quote_id BIGINT NOT NULL, group_id BIGINT REFERENCES question_group(id), status VARCHAR(20) NOT NULL, -- IN_PROGRESS, COMPLETED created_at TIMESTAMP NOT NULL, completed_at TIMESTAMP, recommendation VARCHAR(50) ); ``` ### CustomerAnswer ```sql CREATE TABLE customer_answer ( id BIGINT PRIMARY KEY, questionnaire_id BIGINT REFERENCES customer_questionnaire(id), question_id BIGINT REFERENCES question(id), answer_id BIGINT REFERENCES answer(id), answered_at TIMESTAMP NOT NULL ); ``` ## 2. Encje Java ```java @Entity @Table(name = "question_group") @Getter @Setter public class QuestionGroupEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(unique = true) private String code; private Boolean active = true; @OneToMany(mappedBy = "group") private List<QuestionEntity> questions; } @Entity @Table(name = "question") @Getter @Setter public class QuestionEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @ManyToOne @JoinColumn(name = "group_id") private QuestionGroupEntity group; @Column(name = "question_order") private Integer order; private Boolean active = true; @OneToMany(mappedBy = "question") private List<QuestionContentEntity> contents; @OneToMany(mappedBy = "question") private List<AnswerEntity> answers; } @Entity @Table(name = "question_content") @Getter @Setter public class QuestionContentEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @ManyToOne @JoinColumn(name = "question_id") private QuestionEntity question; @Column(name = "language_code") private String languageCode; private String content; } @Entity @Table(name = "path_condition") @Getter @Setter public class PathConditionEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(unique = true) @Enumerated(EnumType.STRING) private PathCondition code; private String description; private Boolean active = true; @OneToMany(mappedBy = "pathCondition") private List<AnswerEntity> answers; @OneToMany(mappedBy = "pathCondition") private List<RecommendationEntity> recommendations; } @Entity @Table(name = "answer") @Getter @Setter public class AnswerEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @ManyToOne @JoinColumn(name = "question_id") private QuestionEntity question; @ManyToOne @JoinColumn(name = "next_question_id") private QuestionEntity nextQuestion; @Column(name = "answer_order") private Integer order; @ManyToOne @JoinColumn(name = "path_condition_id") private PathConditionEntity pathCondition; private String code; private Boolean active = true; @OneToMany(mappedBy = "answer") private List<AnswerContentEntity> contents; } @Entity @Table(name = "answer_content") @Getter @Setter public class AnswerContentEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @ManyToOne @JoinColumn(name = "answer_id") private AnswerEntity answer; @Column(name = "language_code") private String languageCode; private String content; } @Entity @Table(name = "recommendation") @Getter @Setter public class RecommendationEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @ManyToOne @JoinColumn(name = "path_condition_id") private PathConditionEntity pathCondition; @Column(name = "package_code") private String packageCode; private String description; private Boolean active = true; } @Entity @Table(name = "customer_questionnaire") @Getter @Setter public class CustomerQuestionnaireEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "customer_id") private Long customerId; @Column(name = "quote_id") private Long quoteId; @ManyToOne @JoinColumn(name = "group_id") private QuestionGroupEntity group; @Enumerated(EnumType.STRING) private QuestionnaireStatus status; @Column(name = "created_at") private LocalDateTime createdAt; @Column(name = "completed_at") private LocalDateTime completedAt; private String recommendation; @OneToMany(mappedBy = "questionnaire") private List<CustomerAnswerEntity> answers; } @Entity @Table(name = "customer_answer") @Getter @Setter public class CustomerAnswerEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @ManyToOne @JoinColumn(name = "questionnaire_id") private CustomerQuestionnaireEntity questionnaire; @ManyToOne @JoinColumn(name = "question_id") private QuestionEntity question; @ManyToOne @JoinColumn(name = "answer_id") private AnswerEntity answer; @Column(name = "answered_at") private LocalDateTime answeredAt; } // Enum dla warunków ścieżek public enum PathCondition { MINI_MIDI_MAXI, MINI_MIDI, MIDI_MAXI, MINI_MAXI } // Enum dla statusu kwestionariusza public enum QuestionnaireStatus { IN_PROGRESS, COMPLETED } ``` --- // Enum dla warunków ścieżek public enum PathCondition { MINI_MIDI_MAXI, MINI_MIDI, MIDI_MAXI, MINI_MAXI, // ... inne kombinacje } // Entity dla warunku ścieżki @Entity @Table(name = "path_condition") public class PathConditionEntity { @Id private Long id; @Enumerated(EnumType.STRING) @Column(name = "code") private PathCondition code; private String description; } // Entity dla odpowiedzi z warunkiem @Entity @Table(name = "answer") public class AnswerEntity { @Id private Long id; @ManyToOne @JoinColumn(name = "question_id") private QuestionEntity question; @ManyToOne @JoinColumn(name = "path_condition_id") private PathConditionEntity pathCondition; private Integer answerOrder; private String code; private Boolean active; } // Strategia dla warunków ścieżek public interface PathConditionStrategy { List<String> getAvailablePackages(); String getRecommendation(List<AnswerDTO> answers); } @Service public class MiniMidiMaxiStrategy implements PathConditionStrategy { @Override public List<String> getAvailablePackages() { return Arrays.asList("MINI", "MIDI", "MAXI"); } @Override public String getRecommendation(List<AnswerDTO> answers) { // Logika wyboru rekomendacji na podstawie odpowiedzi return calculateRecommendation(answers); } private String calculateRecommendation(List<AnswerDTO> answers) { // Przykładowa implementacja boolean prefersCheaper = answers.stream() .anyMatch(a -> a.getCode().equals("PRICE_SENSITIVE")); boolean needsFullCoverage = answers.stream() .anyMatch(a -> a.getCode().equals("FULL_COVERAGE")); if (prefersCheaper && !needsFullCoverage) { return "MINI"; } else if (needsFullCoverage) { return "MAXI"; } return "MIDI"; } } // Factory dla strategii @Service public class PathConditionStrategyFactory { private final Map<PathCondition, PathConditionStrategy> strategies; public PathConditionStrategyFactory(List<PathConditionStrategy> strategyList) { strategies = new EnumMap<>(PathCondition.class); // Inicjalizacja strategii strategies.put(PathCondition.MINI_MIDI_MAXI, new MiniMidiMaxiStrategy()); // ... inne strategie } public PathConditionStrategy getStrategy(PathCondition condition) { return strategies.get(condition); } } // Service dla APK @Service @Transactional public class ApkService { private final QuestionRepository questionRepository; private final AnswerRepository answerRepository; private final PathConditionStrategyFactory strategyFactory; public ApkResponseDTO getQuestionnaire(String groupCode) { // Pobierz pytania i odpowiedzi List<QuestionEntity> questions = questionRepository.findByGroupCode(groupCode); return ApkResponseDTO.builder() .questionnaireId(UUID.randomUUID().toString()) .questions(questions.stream() .map(q -> QuestionDTO.builder() .id(q.getId()) .content(q.getContent()) .answersIds(q.getAnswers().stream() .map(AnswerDTO::new) .collect(Collectors.toList())) .build()) .collect(Collectors.toList())) .answers(questions.stream() .flatMap(q -> q.getAnswers().stream()) .map(a -> AnswerDTO.builder() .id(a.getId()) .content(a.getContent()) .nextQuestionId(calculateNextQuestion(a)) .recommendation(calculateRecommendation(a)) .build()) .collect(Collectors.toList())) .initQuestionId(questions.get(0).getId()) .build(); } private Long calculateNextQuestion(AnswerEntity answer) { PathConditionStrategy strategy = strategyFactory.getStrategy(answer.getPathCondition().getCode()); // Logika wyboru następnego pytania na podstawie strategii return null; // lub ID następnego pytania } private String calculateRecommendation(AnswerEntity answer) { PathConditionStrategy strategy = strategyFactory.getStrategy(answer.getPathCondition().getCode()); // Zwróć rekomendację jeśli to ostatnia odpowiedź w ścieżce return null; // lub rekomendacja } public void saveAnswers(String questionnaireId, SaveAnswersDTO request) { // Zapisz odpowiedzi w bazie request.getAnswers().forEach(answer -> { CustomerAnswerEntity customerAnswer = new CustomerAnswerEntity(); customerAnswer.setQuestionnaireId(questionnaireId); customerAnswer.setQuestionId(answer.getQuestionId()); customerAnswer.setAnswerId(answer.getAnswerId()); customerAnswer.setAnsweredAt(LocalDateTime.now()); customerAnswerRepository.save(customerAnswer); }); } } // DTO dla odpowiedzi API @Data @Builder public class ApkResponseDTO { private String questionnaireId; private List<QuestionDTO> questions; private List<AnswerDTO> answers; private Long initQuestionId; } @Data @Builder public class QuestionDTO { private Long id; private String content; private List<AnswerIdDTO> answersIds; } @Data @Builder public class AnswerDTO { private Long id; private String content; private Long nextQuestionId; private String recommendation; } @Data @Builder public class SaveAnswersDTO { private Long initQuestionId; private List<UserAnswerDTO> answers; private String recommendation; } --- # System Design - Analiza Potrzeb Klienta (APK) ## 1. Model Bazodanowy [Bez zmian] ## 2. API Endpoints [Dokładnie taki jak w przekazanej wersji] ## 3. Flow procesu ### 3.1 Standardowy flow 1. **Inicjalizacja**: - Frontend otrzymuje oferty ubezpieczeniowe - Jeśli oferta zawiera AC lub OC+AC: - Wywołuje GET /apk/questionnaires z odpowiednim groupCode - Otrzymuje: - ID kwestionariusza - Listę wszystkich pytań z przypisanymi do nich ID odpowiedzi - Listę wszystkich odpowiedzi z informacją o kolejnym pytaniu lub rekomendacji - ID pierwszego pytania - Zapisuje w lokalnym stanie całą strukturę - Wyświetla pierwsze pytanie na podstawie initQuestionId 2. **Nawigacja po pytaniach**: - Frontend na podstawie otrzymanej struktury: - Wyświetla aktualne pytanie - Z listy answers filtruje odpowiedzi na podstawie answersIds z pytania - Po wyborze odpowiedzi: - Sprawdza nextQuestionId dla wybranej odpowiedzi - Przechodzi do kolejnego pytania lub kończy proces - Zbiera odpowiedzi w lokalnym stanie 3. **Zakończenie procesu**: - Po dotarciu do końca ścieżki (odpowiedź z rekomendacją): - Frontend wywołuje POST /apk/questionnaires/{questionnaireId} - Przekazuje: - ID pierwszego pytania - Listę wszystkich udzielonych odpowiedzi - Końcową rekomendację ### 3.2 Flow zmiany odpowiedzi 1. **Zmiana wcześniejszej odpowiedzi**: - Frontend: - Usuwa z lokalnego stanu odpowiedzi na wszystkie późniejsze pytania - Sprawdza w zapisanej mapie answers nextQuestionId dla nowej odpowiedzi - Kontynuuje proces od nowego pytania - Aktualizuje rekomendację jeśli się zmieniła 2. **Zakończenie zmienionej ścieżki**: - Frontend: - Zbiera wszystkie aktualne odpowiedzi - Zapisuje przez POST z aktualną rekomendacją ## 4. Podział odpowiedzialności ### Backend - Dostarczenie pełnej struktury pytań i odpowiedzi - Przechowywanie definicji pytań i odpowiedzi - Walidacja zapisywanych odpowiedzi - Persystencja danych - Obsługa wielojęzyczności - Zapewnienie spójności danych ### Frontend - Przechowywanie i zarządzanie strukturą pytań i odpowiedzi - Nawigacja między pytaniami według nextQuestionId - Filtrowanie dostępnych odpowiedzi dla pytań - Zarządzanie stanem odpowiedzi użytkownika - Obsługa zmiany odpowiedzi i czyszczenie kolejnych - Prezentacja pytań i odpowiedzi - Obsługa interakcji z użytkownikiem ## 5. Rozszerzalność System zaprojektowany w ten sposób pozwala na: - Łatwe dodawanie nowych pytań i odpowiedzi - Modyfikację ścieżek przez zmianę nextQuestionId - Wsparcie wielu języków - Dodawanie nowych grup pytań - Rozbudowę o nowe typy rekomendacji Kluczowe zalety: - Frontend otrzymuje wszystkie dane na start - Minimalna liczba wywołań API - Szybka reakcja na odpowiedzi użytkownika - Proste zarządzanie zmianami odpowiedzi - Łatwa implementacja różnych wariantów UI --- ### 3.3 Flow przywracania sesji 1. **Inicjalizacja komponentu**: - Frontend sprawdza localStorage pod kątem zapisanego stanu APK - Jeśli stan istnieje: - Przywraca pełny stan z localStorage do store - Wyświetla ostatnie aktywne pytanie - Pokazuje wszystkie dotychczasowe odpowiedzi - Jeśli nie ma stanu: - Inicjuje nowy proces przez pobranie pytań z API 2. **Kontynuacja procesu**: - Użytkownik może: - Kontynuować od ostatniego pytania - Wrócić do wcześniejszych odpowiedzi i je zmienić - Zobaczyć rekomendację jeśli proces był zakończony 3. **Czyszczenie sesji**: - Po zapisie kompletu odpowiedzi: - Frontend czyści stan z localStorage - Inicjuje nowy proces dla kolejnej grupy pytań jeśli potrzebna --- // apk.types.ts export interface Question { id: number; content: string; answersIds: Array<{ id: number; }>; } export interface Answer { id: number; content: string; nextQuestionId?: number; recommendation?: string; } export interface QuestionnaireState { questionnaireId: string; questions: Question[]; answers: Answer[]; initQuestionId: number; userAnswers: Array<{ questionId: number; answerId: number; }>; currentQuestionId: number; recommendation?: string; } // apk.store.ts @Injectable({ providedIn: 'root' }) export class ApkStore { private state = signal<QuestionnaireState | null>(null); setState(state: QuestionnaireState) { this.state.set(state); // Zapisz w localStorage dla możliwości przywrócenia localStorage.setItem('apkState', JSON.stringify(state)); } updateUserAnswer(questionId: number, answerId: number) { this.state.update(state => { if (!state) return null; // Znajdź odpowiedź, aby sprawdzić następne pytanie lub rekomendację const selectedAnswer = state.answers.find(a => a.id === answerId); // Usuń wszystkie odpowiedzi po aktualnym pytaniu const filteredAnswers = state.userAnswers.filter( ua => state.questions.find( q => q.id === ua.questionId )?.answersIds.some( a => a.id === ua.answerId ) ); return { ...state, userAnswers: [...filteredAnswers, { questionId, answerId }], currentQuestionId: selectedAnswer?.nextQuestionId || -1, recommendation: selectedAnswer?.recommendation }; }); } restoreFromLocalStorage(): boolean { const savedState = localStorage.getItem('apkState'); if (savedState) { this.state.set(JSON.parse(savedState)); return true; } return false; } } // apk.service.ts @Injectable({ providedIn: 'root' }) export class ApkService { constructor( private http: HttpClient, private store: ApkStore ) {} getQuestionnaire(groupCode: string): Observable<void> { return this.http.get<QuestionnaireState>('/apk/questionnaires', { params: { groupCode } }).pipe( tap(response => { this.store.setState({ ...response, userAnswers: [], currentQuestionId: response.initQuestionId }); }), map(() => void 0) ); } saveAnswers(questionnaireId: string): Observable<void> { const state = this.store.getState(); if (!state) return EMPTY; return this.http.post<void>(`/apk/questionnaires/${questionnaireId}`, { initQuestionId: state.initQuestionId, answers: state.userAnswers, recommendation: state.recommendation }); } } // apk.component.ts @Component({ selector: 'app-apk', template: ` <ng-container *ngIf="currentQuestion$ | async as question"> <h2>{{ question.content }}</h2> <div class="answers"> <button *ngFor="let answer of getAnswersForQuestion(question)" (click)="selectAnswer(question.id, answer.id)" > {{ answer.content }} </button> </div> </ng-container> ` }) export class ApkComponent implements OnInit { constructor( private apkService: ApkService, private store: ApkStore ) {} ngOnInit() { // Próba przywrócenia stanu if (!this.store.restoreFromLocalStorage()) { // Jeśli nie ma zapisanego stanu, pobierz nowy this.apkService.getQuestionnaire('MAIN_RISKS').subscribe(); } } private getAnswersForQuestion(question: Question): Answer[] { const state = this.store.getState(); if (!state) return []; return question.answersIds .map(aid => state.answers.find(a => a.id === aid.id)) .filter((a): a is Answer => a !== undefined); } selectAnswer(questionId: number, answerId: number) { this.store.updateUserAnswer(questionId, answerId); // Jeśli mamy rekomendację, zapisz odpowiedzi const state = this.store.getState(); if (state?.recommendation) { this.apkService.saveAnswers(state.questionnaireId).subscribe(); } } } --- // models/apk.models.ts export type QuestionGroupCode = 'MAIN_RISKS' | 'ADDITIONAL_RISKS'; export interface QuestionnaireInit { groupCode: QuestionGroupCode; } export interface SaveAnswersRequest { initQuestionId: number; answers: Array<{ questionId: number; answerId: number; }>; recommendation: string; } // components/apk-summary.component.ts @Component({ selector: 'app-apk-summary', template: ` <div *ngIf="state$ | async as state"> <h2>Rekomendacja</h2> <p>{{ state.recommendation }}</p> <div class="answers-summary"> <h3>Twoje odpowiedzi:</h3> <div *ngFor="let answer of state.userAnswers"> <p> {{ getQuestionContent(answer.questionId) }}: {{ getAnswerContent(answer.answerId) }} </p> </div> </div> </div> ` }) export class ApkSummaryComponent { state$ = computed(() => this.store.state()); constructor(private store: ApkStore) {} getQuestionContent(questionId: number): string { return this.store.state()?.questions .find(q => q.id === questionId)?.content ?? ''; } getAnswerContent(answerId: number): string { return this.store.state()?.answers .find(a => a.id === answerId)?.content ?? ''; } } // stores/apk.store.ts export class ApkStore { private state = signal<QuestionnaireState | null>(null); // Computed signals dla lepszej reaktywności readonly currentQuestion = computed(() => { const state = this.state(); if (!state) return null; return state.questions.find(q => q.id === state.currentQuestionId); }); readonly currentAnswers = computed(() => { const state = this.state(); const question = this.currentQuestion(); if (!state || !question) return []; return question.answersIds .map(aid => state.answers.find(a => a.id === aid.id)) .filter((a): a is Answer => a !== undefined); }); readonly showSummary = computed(() => { return !!this.state()?.recommendation; }); } @Component({ selector: 'app-apk', template: ` <ng-container *ngIf="!showSummary(); else summary"> <ng-container *ngIf="currentQuestion() as question"> <h2>{{ question.content }}</h2> <div class="answers"> <button *ngFor="let answer of currentAnswers()" (click)="selectAnswer(question.id, answer.id)" [disabled]="isAnswerDisabled(answer.id)" > {{ answer.content }} </button> </div> </ng-container> </ng-container> <ng-template #summary> <app-apk-summary></app-apk-summary> </ng-template> ` }) export class ApkComponent implements OnInit { currentQuestion = this.store.currentQuestion; currentAnswers = this.store.currentAnswers; showSummary = this.store.showSummary; constructor( private apkService: ApkService, private store: ApkStore ) {} ngOnInit() { if (!this.store.restoreFromLocalStorage()) { this.apkService.getQuestionnaire('MAIN_RISKS').subscribe(); } } selectAnswer(questionId: number, answerId: number) { this.store.updateUserAnswer(questionId, answerId); const state = this.store.state(); if (state?.recommendation) { this.apkService.saveAnswers(state.questionnaireId).subscribe(); } } isAnswerDisabled(answerId: number): boolean { const state = this.store.state(); return state?.userAnswers.some(ua => ua.answerId === answerId) ?? false; } } @NgModule({ declarations: [ ApkComponent, ApkSummaryComponent ], imports: [ CommonModule, HttpClientModule ], providers: [ ApkService, ApkStore ] }) export class ApkModule { }
Leave a Comment