Proposal Selection

How to follow this chapter

Inputs: The chosen business line and investment category, then responses entered by the client.

Steps and expected result: Resolve lookup IDs, collect response IDs, calculate risk, and let the client choose a proposal. Carry these values into Plan creation.

Alternative or later actions: The recommended proposal is the main path. Choosing another proposal can require deviation reasons.

DTO source: BusinessLineOutputModel, InvestmentCategoryOutputModel, ProposalOutputModel, QuestionnaireOutputModel, ReasonToChangeProposalOutputModel, ReasonToChangeProposalResponseOutputModel, RiskCategorizationOutputModel

Variables assigned in an earlier block are reused by later blocks. Client-entered values come from your UI; keep identifiers and lookup results in the current client context. Do not create a fresh context between steps.

The snippets use shared helper classes. See Shared helper classes: complete C# and Java source before copying an example.
Before you start: A configured HTTP client. Reference calls can use a GUEST token; keep the client or administrative context for later writes.
Keep for the next step: The business line, investment category, response IDs, calculated risk and proposalId chosen by the client.
When selecting the investment proposal, the client must specify the desired investment category. Typical business lines and investment categories are listed below. Retrieve the enabled reference data for the target deployment before offering a product:
Vorsorge (pension):
  • 3a pillar (3a)
  • Vested benefits (FZ)
Finance (wealth management):
  • Invest (3b)

Note

Investment types can be activated or deactivated depending on the needs of the company.

Querying business lines

Inputs: Business-line and investment-category codes chosen for the product.
Action: Resolve their reference entries.
Keep / expected result: Keep businessLineId and investmentCategoryId.
Assuming that the client is interested in a 3A plan, we retrieve the ID of the Vorsorge business line and the ID of the 3A investment category ID. These The business-line ID selects the active questionnaire and risk calculation. The investment-category ID is used with the calculated risk ID to retrieve proposals:
								
var businessLines = await httpClient.GetFromJsonAsync<IList<BusinessLineOutputModel>>("api/v1/business-lines");
var businessLineVorsorge = businessLines.First(x => x.Code == "VORSORGE");
var investmentCategories = await httpClient.GetFromJsonAsync<IList<InvestmentCategoryOutputModel>>("api/v1/investment-categories/business-line-id/" + businessLineVorsorge.Id);
var investmentCategory3A = investmentCategories.First(x => x.Code == "3A");

								
HttpRequest businessLineRequest = HttpRequest.newBuilder()
    .timeout(Duration.ofMinutes(1))
    .uri(Settings.wmsUri("business-lines/code/VORSORGE"))
    .header("Accept-Language", "de-DE")
    .header("Authorization", "Bearer " + Settings.GUEST_TOKEN)
    .header("Content-Type", "application/json")
    .build();

var response = httpClient.send(businessLineRequest, BodyHandlers.ofString());
Api.requireSuccess(response);
BusinessLineOutputModel businessLineOutputModelObj = new Gson().fromJson(response.body(), BusinessLineOutputModel.class);
var businessLineId = businessLineOutputModelObj.Id;

HttpRequest investmentCategoryRequest = HttpRequest.newBuilder()
    .timeout(Duration.ofMinutes(1))
    .uri(Settings.wmsUri("investment-categories/code/3A"))
    .header("Accept-Language", "de-DE")
    .header("Authorization", "Bearer " + Settings.GUEST_TOKEN)
    .header("Content-Type", "application/json")
    .build();

response = httpClient.send(investmentCategoryRequest, BodyHandlers.ofString());
Api.requireSuccess(response);
InvestmentCategoryOutputModel investmentCategoryObj = new Gson().fromJson(response.body(), InvestmentCategoryOutputModel.class);
var investmentCategoryId = investmentCategoryObj.Id;

Questionnaire responses

Inputs: businessLineId and the questionnaire answers selected by the client.
Action: Fetch the questionnaire, resolve response IDs and calculate risk.
Keep / expected result: Keep responseIds and the calculated risk entry.
Given a business line, we retrieve the respective questionnaire, with possible responses.

Display the active questionnaire and collect selectedResponseCodes from the client answers across all sections. This example uses discrete choices. If the questionnaire requires numeric response values, use the applicable schema and include ResponseValue when saving those answers. Resolve those codes to IDs in that questionnaire. Required answers and suitability rules are validated by the API; do not assume a fixed version or question order:
								
var activeQuestionnaire = await httpClient.GetFromJsonAsync<QuestionnaireOutputModel>($"api/v1/questionnaires/business-line-id/{businessLineVorsorge.Id}/active");
var availableResponses = activeQuestionnaire.Sections.SelectMany(s => s.Questions).SelectMany(q => q.Responses).ToList();
// selectedResponseCodes comes from the client answers to the displayed questionnaire.
var responseIds = selectedResponseCodes.Select(code => availableResponses.Single(r => r.Code == code).Id).ToArray();
var responsesCsv = string.Join(",", responseIds);
var risk = await httpClient.GetFromJsonAsync<RiskCategorizationOutputModel>($"api/v1/risk-categorizations/business-line-id/{businessLineVorsorge.Id}/calculate-risk?csv-response-ids={Uri.EscapeDataString(responsesCsv)}");

								
HttpRequest questionnaireRequest = HttpRequest.newBuilder()
    .uri(Settings.wmsUri("questionnaires/business-line-id/" + businessLineId + "/active"))
    .timeout(Duration.ofMinutes(1))
    .header("Authorization", "Bearer " + Settings.GUEST_TOKEN).GET().build();
var questionnaireResponse = httpClient.send(questionnaireRequest, BodyHandlers.ofString());
Api.requireSuccess(questionnaireResponse);
QuestionnaireOutputModel activeQuestionnaire = new Gson().fromJson(questionnaireResponse.body(), QuestionnaireOutputModel.class);
var availableResponses = activeQuestionnaire.Sections.stream()
    .flatMap(s -> s.Questions.stream()).flatMap(q -> q.Responses.stream()).toList();
// selectedResponseCodes comes from the client answers to the displayed questionnaire.
var responseIds = selectedResponseCodes.stream().map(code -> availableResponses.stream()
    .filter(r -> code.equals(r.Code)).findFirst()
    .orElseThrow(() -> new IllegalArgumentException("Response not in the active questionnaire")).Id).toList();
var responsesCsv = responseIds.stream().map(String::valueOf).collect(java.util.stream.Collectors.joining(","));
HttpRequest riskRequest = HttpRequest.newBuilder()
    .uri(Settings.wmsUri("risk-categorizations/business-line-id/" + businessLineId
    + "/calculate-risk?csv-response-ids=" + java.net.URLEncoder.encode(responsesCsv, StandardCharsets.UTF_8)))
    .timeout(Duration.ofMinutes(1))
    .header("Authorization", "Bearer " + Settings.GUEST_TOKEN).GET().build();
var riskResponse = httpClient.send(riskRequest, BodyHandlers.ofString());
Api.requireSuccess(riskResponse);
RiskCategorizationOutputModel riskCategorizationObj = new Gson().fromJson(riskResponse.body(), RiskCategorizationOutputModel.class);

Obtaining investment proposals

Inputs: investmentCategoryId and the calculated risk.
Action: Retrieve the proposals and collect the client selection.
Keep / expected result: Keep the chosen proposalId and any required deviation-reason IDs.
Given the calculated risk, retrieve the proposed investment models and let the client choose proposalId from the displayed results. If no matching proposals or reference codes are returned, handle that as unavailable product/reference data rather than selecting a default:
								
var proposals = await httpClient.GetFromJsonAsync<List<ProposalOutputModel>>($"api/v1/proposals/investment-category-id/{investmentCategory3A.Id}/risk-id/{risk.Id}");
// The client chooses proposalId from the displayed proposals.
var proposalSelectedByUser = proposals.Single(x => x.Id == proposalId);

								
HttpRequest proposalRequest = HttpRequest.newBuilder()
    .timeout(Duration.ofMinutes(1))
    .uri(Settings.wmsUri("proposals/investment-category-id/" + investmentCategoryId + "/risk-id/" + riskCategorizationObj.Id))
    .header("Accept-Language", "de-DE")
    .header("Authorization", "Bearer " + Settings.GUEST_TOKEN)
    .header("Content-Type", "application/json")
    .build();

response = httpClient.send(proposalRequest, BodyHandlers.ofString());
Api.requireSuccess(response);

Type listType = new TypeToken<ArrayList<ProposalOutputModel>>(){}.getType();
List<ProposalOutputModel> proposalsListObj = new Gson().fromJson(response.body(), listType);

// proposalId is selected by the client from the displayed proposals.
var proposalSelectedByUser = proposalsListObj.stream().filter(x -> x.Id == proposalId)
    .findFirst().orElseThrow(() -> new IllegalArgumentException("Select a displayed proposal"));

The result of the proposal can be displayed as follows:
Example investment proposal
Also, it is possible that the client chooses an investment model that does not correspond to the one proposed. In this case it is necessary to specify the reasons to change proposal. This is a simple list of answers with the following structure:
									
public class ReasonToChangeProposalResponseOutputModel
{
    public long Id { get; set; }
    public string Code { get; set; }
    public string Description { get; set; }
    public int Order { get; set; }
}

public class ReasonToChangeProposalOutputModel
{
    public long Id { get; set; }
    public string Code { get; set; }
    public string Name { get; set; }
    public bool IsDeleted { get; set; }
    public IList<ReasonToChangeProposalResponseOutputModel> Responses { get; set; }
}

	
									
import java.util.List;
import java.util.Map;

import com.google.gson.annotations.SerializedName;

public class ReasonToChangeProposalResponseOutputModel {
    @SerializedName("id")
    public long Id;

    @SerializedName("code")
    public String Code;

    @SerializedName("description")
    public String Description;

    @SerializedName("order")
    public int Order;
}

public class ReasonToChangeProposalOutputModel {
    @SerializedName("id")
    public long Id;

    @SerializedName("code")
    public String Code;

    @SerializedName("name")
    public String Name;

    @SerializedName("isDeleted")
    public boolean IsDeleted;

    @SerializedName("responses")
    public List<ReasonToChangeProposalResponseOutputModel> Responses;
}

This list can be retrieved as follows:
									
var reasonsToChangeProposal = await httpClient.GetFromJsonAsync<ReasonToChangeProposalOutputModel>($"api/v1/reason-to-change-proposed-proposal/investment-category-id/{investmentCategory3A.Id}/active");

	
									
HttpRequest reasonToChangeRequest = HttpRequest.newBuilder()
    .timeout(Duration.ofMinutes(1))
    .uri(Settings.wmsUri("reason-to-change-proposed-proposal/investment-category-id/" + investmentCategoryId + "/active"))
    .header("Accept-Language", "de-DE")
    .header("Authorization", "Bearer " + Settings.GUEST_TOKEN)
    .header("Content-Type", "application/json")
    .build();

response = httpClient.send(reasonToChangeRequest, BodyHandlers.ofString());
Api.requireSuccess(response);
ReasonToChangeProposalOutputModel reasonToChangeProposalObj = new Gson().fromJson(response.body(), ReasonToChangeProposalOutputModel.class);

If the client departs from the recommended proposal, collect the relevant response IDs in selectedReasonIds. Use an empty list when no deviation reasons are required; do not choose reasons automatically. Pass these IDs when submitting the plan creation order. We will see this in the next chapter.