Untitled
unknown
plain_text
9 months ago
26 kB
9
Indexable
# 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 { }
Editor is loading...
Leave a Comment