diff --git a/hartmann-foto-documentation-app/pom.xml b/hartmann-foto-documentation-app/pom.xml index ab7b148..fccdd6b 100644 --- a/hartmann-foto-documentation-app/pom.xml +++ b/hartmann-foto-documentation-app/pom.xml @@ -37,7 +37,19 @@ + + + + org.apache.poi + poi + + + + org.apache.poi + poi-ooxml + + org.apache.pdfbox pdfbox @@ -356,6 +368,14 @@ org.jacoco.core test + + + org.eclipse.parsson + parsson + 1.1.5 + test + + org.slf4j slf4j-simple diff --git a/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/service/QuestionnaireCustomerService.java b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/service/QuestionnaireCustomerService.java index bde3e94..214b6dd 100644 --- a/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/service/QuestionnaireCustomerService.java +++ b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/service/QuestionnaireCustomerService.java @@ -100,10 +100,10 @@ public class QuestionnaireCustomerService extends AbstractService { return QuestionnaireCustomerValue.builder(customer); } - public byte[] getExport(Long id, Long questionnaireId) { + public Optional getExport(Long id, Long questionnaireId) { QuestionnaireCustomer customer = entityManager.find(QuestionnaireCustomer.class, id); if (customer == null) { - return new byte[0]; + return Optional.empty(); } List questionnaires = customer.getQuestionnaires().stream().sorted((x, y) -> x.getQuestionnaireDate().compareTo(y.getQuestionnaireDate())).toList(); diff --git a/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/service/QuestionnairePublishService.java b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/service/QuestionnairePublishService.java index 5acffcf..55dda78 100644 --- a/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/service/QuestionnairePublishService.java +++ b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/service/QuestionnairePublishService.java @@ -1,9 +1,16 @@ package marketing.heyday.hartmann.fotodocumentation.core.service; +import static marketing.heyday.hartmann.fotodocumentation.core.model.QuestionnaireCustomer.*; + +import java.util.Optional; + import jakarta.annotation.security.PermitAll; import jakarta.ejb.LocalBean; import jakarta.ejb.Stateless; -import marketing.heyday.hartmann.fotodocumentation.rest.vo.QuestionnairePublishValue; +import marketing.heyday.hartmann.fotodocumentation.core.model.Questionnaire; +import marketing.heyday.hartmann.fotodocumentation.core.model.QuestionnaireCustomer; +import marketing.heyday.hartmann.fotodocumentation.core.query.Param; +import marketing.heyday.hartmann.fotodocumentation.core.utils.QuestionnaireUploadJsonParser; /** * @@ -19,8 +26,32 @@ import marketing.heyday.hartmann.fotodocumentation.rest.vo.QuestionnairePublishV @PermitAll public class QuestionnairePublishService extends AbstractService { - public boolean publish(QuestionnairePublishValue value) { - // FIXME: implement me - return false; + public boolean publish(String body) { + var parserOpt = QuestionnaireUploadJsonParser.builder(body); + if (parserOpt.isEmpty()) { + return false; + } + var parser = parserOpt.get(); + + Optional customerOpt = queryService.callNamedQuerySingleResult(FIND_BY_NUMBER, new Param(PARAM_NUMBER, parser.customerNumber())); + var customer = customerOpt.orElseGet(() -> new QuestionnaireCustomer.Builder().customerNumber(parser.customerNumber()).name(parser.pharmacyName()) + .city(parser.city()).zip(parser.zip()) + .build()); + customer = entityManager.merge(customer); + + var questionnaire = new Questionnaire.Builder().customer(customer).username(parser.username()) + .category("")// FIXME: remove category + .comment(parser.comment()) + .customer(customer) + .questionnaireDate(parser.date()) + .questions(parser.questionnair()) + .username(parser.username()) + .build(); + customer.getQuestionnaires().add(questionnaire); + + entityManager.persist(questionnaire); + entityManager.flush(); + return true; } + } diff --git a/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/utils/ExcelUtils.java b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/utils/ExcelUtils.java index 435474d..41436e6 100644 --- a/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/utils/ExcelUtils.java +++ b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/utils/ExcelUtils.java @@ -1,12 +1,22 @@ package marketing.heyday.hartmann.fotodocumentation.core.utils; +import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.util.List; +import java.util.Optional; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.CellType; +import org.apache.poi.ss.usermodel.CreationHelper; +import org.apache.poi.xssf.usermodel.XSSFRow; +import org.apache.poi.xssf.usermodel.XSSFSheet; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; import marketing.heyday.hartmann.fotodocumentation.core.model.Questionnaire; import marketing.heyday.hartmann.fotodocumentation.core.model.QuestionnaireCustomer; +import marketing.heyday.hartmann.fotodocumentation.core.utils.QuestionnaireJsonParser.QuestionJsonObj; /** * @@ -21,9 +31,164 @@ import marketing.heyday.hartmann.fotodocumentation.core.model.QuestionnaireCusto public class ExcelUtils { private static final Log LOG = LogFactory.getLog(ExcelUtils.class); - public byte[] create(QuestionnaireCustomer customer, List questionnaires) { - // FIXME: implement excel export - return new byte[0]; + public Optional create(QuestionnaireCustomer customer, List questionnaires) { + LOG.debug("Create excel file for customer " + customer); + // TODO: implement excel export + + try (ByteArrayOutputStream bos = new ByteArrayOutputStream(); XSSFWorkbook workbook = new XSSFWorkbook()) { + + for (var questionnaire : questionnaires) { + //TODO: set sheet name + writeSheet(workbook, customer, questionnaire); + } + + workbook.write(bos); + return Optional.of(bos.toByteArray()); + } catch (IOException e) { + LOG.debug("Failed to export countries", e); + return Optional.empty(); + } } + private void writeSheet(XSSFWorkbook workbook, QuestionnaireCustomer customer, Questionnaire questionnaire) throws IOException { + XSSFSheet sheet = workbook.createSheet(); + int rowIndex = writeCustomerData(workbook, sheet, customer, questionnaire); + writeQuestionData(sheet, questionnaire, rowIndex); + for (int i = 0; i < 10; i++) { + sheet.autoSizeColumn(i); + } + } + + private int writeCustomerData(XSSFWorkbook workbook, XSSFSheet sheet, QuestionnaireCustomer customer, Questionnaire questionnaire) { + int rowIndex = 0; + XSSFRow row = sheet.createRow(rowIndex++); + row.createCell(0).setCellValue(customer.getName()); + + row = sheet.createRow(rowIndex++); + row.createCell(0).setCellValue(customer.getCustomerNumber()); + row = sheet.createRow(rowIndex++); + row.createCell(0).setCellValue(customer.getCity()); + row = sheet.createRow(rowIndex++); + row.createCell(0).setCellValue(customer.getZip()); + + row = sheet.createRow(rowIndex++); + + CreationHelper createHelper = workbook.getCreationHelper(); + short format = createHelper.createDataFormat().getFormat("dd.MM.yyyy h:mm"); + CellStyle cellStyle = workbook.createCellStyle(); + cellStyle.setDataFormat(format); + + var dateCell = row.createCell(0); + dateCell.setCellStyle(cellStyle); + + dateCell.setCellValue(questionnaire.getQuestionnaireDate()); + rowIndex++; + rowIndex++; + return rowIndex; + } + + private void writeQuestionData(XSSFSheet sheet, Questionnaire questionnaire, int rowIndex) throws IOException { + QuestionnaireJsonParser parser = new QuestionnaireJsonParser(); + var success = parser.parse(questionnaire.getQuestions()); + if (!success) { + throw new IOException("Failed to parse json"); + } + + var questions = parser.getQuestions(); + + for (var question : questions) { + XSSFRow row = sheet.createRow(rowIndex++); + row.createCell(0).setCellValue(question.title()); + + switch (question.type()) { + case "singleChoice": + writeSingle(row, question); + break; + case "multiplChoice": + int amount1 = writeMultiple(sheet, row, question); + rowIndex = rowIndex + amount1; + break; + case "number": + writeNumber(row, question); + break; + case "matrix": + int amount2 = writeMatrix(sheet, row, question); + rowIndex = rowIndex + amount2; + break; + case "freeText": + writeFreeText(row, question); + break; + default: + break; + } + + rowIndex++; + } + } + + private void writeSingle(XSSFRow row, QuestionJsonObj question) { + row.createCell(1).setCellValue(question.getSingleAnswer()); + } + + private int writeMultiple(XSSFSheet sheet, XSSFRow row, QuestionJsonObj question) { + var answers = question.getMultiAnswer(); + int count = 0; + for (var answer : answers) { + if (count == 0) { + row.createCell(1).setCellValue(answer); + } else { + sheet.createRow(row.getRowNum() + count).createCell(1).setCellValue(answer); + } + count++; + } + return count - 1; + } + + private void writeNumber(XSSFRow row, QuestionJsonObj question) { + var cell = row.createCell(1); + cell.setCellType(CellType.NUMERIC); + cell.setCellValue(question.getNumberAnswer()); + } + + private void writeFreeText(XSSFRow row, QuestionJsonObj question) { + row.createCell(1).setCellValue(question.getFreeText()); + } + + private int writeMatrix(XSSFSheet sheet, XSSFRow row, QuestionJsonObj question) { + var questions = question.getMatrixAnswer(); + int count = 2; + + var answerOpts = questions.getFirst().answers(); + answerOpts.sort((i, j) -> i.answer().compareToIgnoreCase(j.answer())); + + var headerRow = sheet.createRow(row.getRowNum() + count); + // create header + for (int i = 0; i < answerOpts.size(); i++) { + var answer = answerOpts.get(i); + headerRow.createCell(i + 1).setCellValue(answer.answer()); + } + count++; + + for (var subQuestion : questions) { + var questionRow = sheet.createRow(row.getRowNum() + count); + questionRow.createCell(0).setCellValue(subQuestion.title()); + + for (var answer : subQuestion.answers()) { + if (!answer.selected()) { + continue; + } + int index = 0; + for (int i = 0; i < answerOpts.size(); i++) { + if (answer.answer().equalsIgnoreCase(answerOpts.get(i).answer())) { + index = i; + } + } + var cell = questionRow.createCell(1 + index); + cell.setCellType(CellType.BOOLEAN); + cell.setCellValue(answer.selected()); + } + count++; + } + return count; + } } diff --git a/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/utils/JsonSchemaValidator.java b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/utils/JsonSchemaValidator.java new file mode 100644 index 0000000..7b13555 --- /dev/null +++ b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/utils/JsonSchemaValidator.java @@ -0,0 +1,57 @@ +package marketing.heyday.hartmann.fotodocumentation.core.utils; + +import java.io.IOException; +import java.io.InputStream; +import java.util.HashSet; +import java.util.Set; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.networknt.schema.JsonSchema; +import com.networknt.schema.JsonSchemaFactory; +import com.networknt.schema.ValidationMessage; + +/** + * + *

Copyright: Copyright (c) 2024

+ *

Company: heyday Marketing GmbH

+ * @author Patrick Verboom + * @version 1.0 + * + * created: 20 Feb 2026 + */ + +public class JsonSchemaValidator { + private static final Log LOG = LogFactory.getLog(JsonSchemaValidator.class); + + + public ValidationReply validate(String schemaPath, String jsonData) { + try { + JsonSchema schema = getJsonSchema(schemaPath); + JsonNode node = getJsonNode(jsonData); + Set errors = schema.validate(node); + if (!errors.isEmpty()) { + LOG.error("Failed to validate json to schema " + schemaPath); + errors.forEach(LOG::error); + } + return new ValidationReply(errors.isEmpty(), errors); + } catch (IOException e) { + LOG.error(e.getMessage(), e); + return new ValidationReply(false, new HashSet<>()); + } + } + + protected JsonSchema getJsonSchema(String name) throws IOException { + JsonSchemaFactory factory = new JsonSchemaFactory(); + try (InputStream input = Thread.currentThread().getContextClassLoader().getResourceAsStream(name);) { + return factory.getSchema(input); + } + } + + protected JsonNode getJsonNode(String content) throws IOException { + return new ObjectMapper().readTree(content); + } +} diff --git a/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/utils/QuestionnaireJsonParser.java b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/utils/QuestionnaireJsonParser.java new file mode 100644 index 0000000..cd5a4a8 --- /dev/null +++ b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/utils/QuestionnaireJsonParser.java @@ -0,0 +1,170 @@ +package marketing.heyday.hartmann.fotodocumentation.core.utils; + +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; +import java.util.function.Supplier; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import jakarta.json.*; +import jakarta.json.stream.JsonParsingException; + +/** + * + *

Copyright: Copyright (c) 2024

+ *

Company: heyday Marketing GmbH

+ * @author Patrick Verboom + * @version 1.0 + * + * created: 20 Feb 2026 + */ + +public class QuestionnaireJsonParser { + private static final Log LOG = LogFactory.getLog(QuestionnaireJsonParser.class); + + private final List questions = new ArrayList<>(); + + public List getQuestions() { + return questions; + } + + public boolean parse(String body) { + questions.clear(); + if (body == null) { + return false; + } + + try (var stringReader = new StringReader(body); JsonReader reader = Json.createReader(stringReader);) { + JsonArray root = reader.readArray(); + + for (var object : root) { + var question = object.asJsonObject(); + + String id = question.getString("id"); + String title = question.getString("title"); + int order = question.getInt("order"); + String type = question.getString("type"); + String data = toData(question.getJsonObject("data")); + + questions.add(new QuestionJsonObj(id, title, order, type, data)); + } + + return true; + } catch (IOException | JsonParsingException | NullPointerException ioe) { + LOG.warn("Failed to parse json " + ioe.getMessage(), ioe); + return false; + } + } + + private String toData(JsonObject obj) throws IOException { + try (StringWriter writer = new StringWriter(); JsonWriter jsonWriter = Json.createWriter(writer);) { + + jsonWriter.writeObject(obj); + jsonWriter.close(); + + return writer.toString(); + } + } + + public record QuestionJsonObj(String id, String title, int order, String type, String data) { + + public List getMatrixAnswer() { + return getValue("questions", () -> null, (questions) -> { + var retVal = new ArrayList(); + + for (var question : questions) { + var questionObj = question.asJsonObject(); + var id = questionObj.getString("id"); + var title = questionObj.getString("title"); + var order = questionObj.getInt("order"); + + var answerList = new ArrayList(); + + var answers = questionObj.getJsonArray("answers"); + for (var answer : answers) { + var answerObj = answer.asJsonObject(); + var answerId = answerObj.getString("id"); + var answerStr = answerObj.getString("answer"); + var selected = answerObj.getBoolean("selected"); + + answerList.add(new MatrixAnswer(answerId, answerStr, selected)); + } + retVal.add(new MatrixQuestion(id, title, order, answerList)); + } + return retVal; + }); + } + + public Integer getNumberAnswer() { + return getValue(() -> null, (answers) -> { + for (var answer : answers) { + var answerObj = answer.asJsonObject(); + if (answerObj.getBoolean("selected")) { + return answerObj.getInt("answer"); + } + } + return null; + }); + } + + public String getSingleAnswer() { + return getValue(() -> "", (answers) -> { + for (var answer : answers) { + var answerObj = answer.asJsonObject(); + if (answerObj.getBoolean("selected")) { + return answerObj.getString("answer"); + } + } + return ""; + }); + } + + public List getMultiAnswer() { + return getValue(() -> List.of(), (answers) -> { + List retVal = new ArrayList<>(); + for (var answer : answers) { + var answerObj = answer.asJsonObject(); + if (answerObj.getBoolean("selected")) { + retVal.add(answerObj.getString("answer")); + } + } + return retVal; + }); + } + + public String getFreeText() { + return getValue(() -> "", (answers) -> answers.getFirst().asJsonObject().getString("answer")); + } + + private T getValue(Supplier emptyValue, Function function) { + return getValue("answers", emptyValue, function); + } + + private T getValue(String element, Supplier emptyValue, Function function) { + try (var stringReader = new StringReader(data); JsonReader reader = Json.createReader(stringReader);) { + JsonObject root = reader.readObject(); + JsonArray answers = root.getJsonArray(element); + if (answers.isEmpty()) { + return emptyValue.get(); + } + return function.apply(answers); + } catch (JsonParsingException | NullPointerException ioe) { + LOG.warn("Failed to parse json " + ioe.getMessage(), ioe); + return emptyValue.get(); + } + } + } + + public record MatrixQuestion(String id, String title, int order, List answers) { + + } + + public record MatrixAnswer(String id, String answer, boolean selected) { + + } +} diff --git a/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/utils/QuestionnaireUploadJsonParser.java b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/utils/QuestionnaireUploadJsonParser.java new file mode 100644 index 0000000..3677832 --- /dev/null +++ b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/utils/QuestionnaireUploadJsonParser.java @@ -0,0 +1,74 @@ +package marketing.heyday.hartmann.fotodocumentation.core.utils; + +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Optional; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import jakarta.json.*; +import jakarta.json.stream.JsonParsingException; + +/** + * + *

Copyright: Copyright (c) 2024

+ *

Company: heyday Marketing GmbH

+ * @author Patrick Verboom + * @version 1.0 + * + * created: 20 Feb 2026 + */ + +public record QuestionnaireUploadJsonParser( + String username, + String pharmacyName, + String customerNumber, + Date date, + String comment, + String city, + String zip, + String questionnair) { + + private static final Log LOG = LogFactory.getLog(QuestionnaireUploadJsonParser.class); + + public static Optional builder(String body) { + if (body == null) { + return Optional.empty(); + } + try (var stringReader = new StringReader(body); JsonReader reader = Json.createReader(stringReader);) { + JsonObject root = reader.readObject(); + + String username = root.getString("username"); + String pharmacyName = root.getString("pharmacyName"); + String customerNumber = root.getString("customerNumber"); + String dateStr = root.getString("date"); + + var dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX"); + Date date = dateFormat.parse(dateStr); + + String comment = root.getString("comment"); + String city = root.getString("city"); + String zip = root.getString("zip"); + + JsonArray questionnaireArr = root.getJsonArray("questionnaire"); + + String questionnaire = ""; + try (StringWriter writer = new StringWriter(); JsonWriter jsonWriter = Json.createWriter(writer);) { + + jsonWriter.writeArray(questionnaireArr); + jsonWriter.close(); + + questionnaire = writer.toString(); + } + return Optional.of(new QuestionnaireUploadJsonParser(username, pharmacyName, customerNumber, date, comment, city, zip, questionnaire)); + } catch (IOException | ParseException | JsonParsingException | NullPointerException ioe) { + LOG.warn("Failed to parse json " + ioe.getMessage(), ioe ); + return Optional.empty(); + } + } +} diff --git a/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/rest/jackson/ValidationReply.java b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/utils/ValidationReply.java similarity index 81% rename from hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/rest/jackson/ValidationReply.java rename to hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/utils/ValidationReply.java index ad56a13..7251bf0 100644 --- a/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/rest/jackson/ValidationReply.java +++ b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/core/utils/ValidationReply.java @@ -1,4 +1,4 @@ -package marketing.heyday.hartmann.fotodocumentation.rest.jackson; +package marketing.heyday.hartmann.fotodocumentation.core.utils; import java.util.Set; diff --git a/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/rest/QuestionnaireCustomerResource.java b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/rest/QuestionnaireCustomerResource.java index 23911aa..32d409d 100644 --- a/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/rest/QuestionnaireCustomerResource.java +++ b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/rest/QuestionnaireCustomerResource.java @@ -3,6 +3,7 @@ package marketing.heyday.hartmann.fotodocumentation.rest; import static marketing.heyday.hartmann.fotodocumentation.rest.jackson.ApplicationConfigApi.JSON_OUT; import java.io.OutputStream; +import java.util.Optional; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -73,15 +74,15 @@ public class QuestionnaireCustomerResource { @ApiResponse(responseCode = "200", description = "Successfully retrieved export") public Response doExport(@PathParam("id") Long id, @QueryParam("questionnaire") Long questionnaireId) { LOG.debug("Create export for customer " + id + " with optional param " + questionnaireId); - byte[] pdf = questionnaireCustomerService.getExport(id, questionnaireId); + Optional pdfOpt = questionnaireCustomerService.getExport(id, questionnaireId); - if (pdf.length == 0) { + if (pdfOpt.isEmpty()) { return Response.status(Status.NOT_FOUND).build(); } StreamingOutput streamingOutput = (OutputStream output) -> { LOG.debug("Start writing content to OutputStream available bytes"); - output.write(pdf); + output.write(pdfOpt.get()); }; return Response.status(Status.OK).entity(streamingOutput).build(); } diff --git a/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/rest/QuestionnairePublishResource.java b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/rest/QuestionnairePublishResource.java index 021ef0c..4faeb66 100644 --- a/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/rest/QuestionnairePublishResource.java +++ b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/rest/QuestionnairePublishResource.java @@ -1,11 +1,19 @@ package marketing.heyday.hartmann.fotodocumentation.rest; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.stream.Collectors; + import org.jboss.resteasy.annotations.GZIP; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponse; import jakarta.ejb.EJB; import jakarta.enterprise.context.RequestScoped; +import jakarta.inject.Inject; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; @@ -13,8 +21,7 @@ import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response.Status; import marketing.heyday.hartmann.fotodocumentation.core.service.QuestionnairePublishService; -import marketing.heyday.hartmann.fotodocumentation.rest.jackson.JsonSchemaValidate; -import marketing.heyday.hartmann.fotodocumentation.rest.vo.QuestionnairePublishValue; +import marketing.heyday.hartmann.fotodocumentation.core.utils.JsonSchemaValidator; /** * @@ -33,14 +40,28 @@ public class QuestionnairePublishResource { @EJB private QuestionnairePublishService questionnairePublishService; + @Inject + private JsonSchemaValidator jsonSchemaValidator; + @GZIP @POST @Path("") @Consumes(MediaType.APPLICATION_JSON) @Operation(summary = "Add questionnaire to database") @ApiResponse(responseCode = "200", description = "Add successfull") - public Response doAddQuestionnaire(@JsonSchemaValidate("schema/questionnaire_publish.json") QuestionnairePublishValue value) { - boolean success = questionnairePublishService.publish(value); - return success ? Response.ok().build() : Response.status(Status.BAD_REQUEST).build(); + public Response doAddQuestionnaire(InputStream inputStream) { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) { + String body = reader.lines().collect(Collectors.joining("\n")); + + var reply = jsonSchemaValidator.validate("schema/questionnaire_publish.json", body); + if (!reply.success()) { + return Response.status(Status.BAD_REQUEST).entity(reply.errors()).build(); + } + + questionnairePublishService.publish(body); + return Response.ok().build(); + } catch (IOException e) { + return Response.status(Status.BAD_REQUEST).build(); + } } } \ No newline at end of file diff --git a/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/rest/jackson/ValidatedMessageBodyReader.java b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/rest/jackson/ValidatedMessageBodyReader.java index 8b7e53c..c962148 100644 --- a/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/rest/jackson/ValidatedMessageBodyReader.java +++ b/hartmann-foto-documentation-app/src/main/java/marketing/heyday/hartmann/fotodocumentation/rest/jackson/ValidatedMessageBodyReader.java @@ -5,19 +5,10 @@ import java.lang.annotation.Annotation; import java.lang.reflect.Type; import java.nio.charset.Charset; import java.util.Arrays; -import java.util.HashSet; import java.util.Optional; -import java.util.Set; import java.util.stream.Collectors; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import com.networknt.schema.JsonSchema; -import com.networknt.schema.JsonSchemaFactory; -import com.networknt.schema.ValidationMessage; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.WebApplicationException; @@ -27,6 +18,8 @@ import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response.Status; import jakarta.ws.rs.ext.MessageBodyReader; import jakarta.ws.rs.ext.Provider; +import marketing.heyday.hartmann.fotodocumentation.core.utils.JsonSchemaValidator; +import marketing.heyday.hartmann.fotodocumentation.core.utils.ValidationReply; /** * @@ -44,7 +37,6 @@ import jakarta.ws.rs.ext.Provider; MediaType.APPLICATION_JSON, "application/json; charset=utf-8", }) public class ValidatedMessageBodyReader implements MessageBodyReader { - private static final Log LOG = LogFactory.getLog(ValidatedMessageBodyReader.class); /* (non-Javadoc) * @see jakarta.ws.rs.ext.MessageBodyReader#isReadable(java.lang.Class, java.lang.reflect.Type, java.lang.annotation.Annotation[], jakarta.ws.rs.core.MediaType) @@ -85,30 +77,7 @@ public class ValidatedMessageBodyReader implements MessageBodyReader errors = schema.validate(node); - if (!errors.isEmpty()) { - LOG.error("Failed to validate json to schema " + schemaPath); - errors.forEach(LOG::error); - } - return new ValidationReply(errors.isEmpty(), errors); - } catch (IOException e) { - LOG.error(e.getMessage(), e); - return new ValidationReply(false, new HashSet<>()); - } - } - - protected JsonSchema getJsonSchema(String name) throws IOException { - JsonSchemaFactory factory = new JsonSchemaFactory(); - try (InputStream input = Thread.currentThread().getContextClassLoader().getResourceAsStream(name);) { - return factory.getSchema(input); - } - } - - protected JsonNode getJsonNode(String content) throws IOException { - return new ObjectMapper().readTree(content); + return new JsonSchemaValidator().validate(schemaPath, jsonData); } private String read(InputStream input) throws IOException { diff --git a/hartmann-foto-documentation-app/src/main/resources/schema/questionnaire_publish.json b/hartmann-foto-documentation-app/src/main/resources/schema/questionnaire_publish.json index fb08682..cc7aa44 100644 --- a/hartmann-foto-documentation-app/src/main/resources/schema/questionnaire_publish.json +++ b/hartmann-foto-documentation-app/src/main/resources/schema/questionnaire_publish.json @@ -1,40 +1,44 @@ { "$schema": "http://json-schema.org/draft-04/schema#", - "title": "Add Customer Picture", - "description": "Add a Customer Picture to the system", + "title": "Publish Questionnaire", + "description": "Publish a completed questionnaire to the system", "type": "object", "properties": { "username": { - "description": "The username from the user who uploads the picture", + "description": "The username from the user who submits the questionnaire", "type": "string" }, "pharmacyName": { - "description": "The Name from the pharmacy customer ", + "description": "The name of the pharmacy customer", "type": "string" }, "customerNumber": { - "description": "The unique number from the pharmacy customer ", + "description": "The unique number of the pharmacy customer", "type": "string" }, "date": { - "description": "The date when the picture is taken ", + "description": "The date when the questionnaire was filled in (ISO 8601)", "type": "string" }, "comment": { - "description": "A free text comment field ", - "type": "string" - }, - "zip": { - "description": "The zip from the customer", + "description": "A free text comment field", "type": "string" }, "city": { - "description": "The city from the customer", + "description": "The city of the customer", "type": "string" }, - "base64String": { - "description": "The Picture content as base64 ", + "zip": { + "description": "The zip code of the customer", "type": "string" + }, + "questionnaire": { + "description": "The list of questions and answers", + "type": "array", + "items": { + "$ref": "#/definitions/question" + }, + "minItems": 1 } }, "required": [ @@ -42,7 +46,118 @@ "pharmacyName", "customerNumber", "date", - "comment", - "base64String" - ] -} \ No newline at end of file + "questionnaire" + ], + "definitions": { + "answer": { + "type": "object", + "properties": { + "id": { + "description": "The unique identifier of the answer", + "type": "string" + }, + "answer": { + "description": "The answer text", + "type": "string" + }, + "selected": { + "description": "Whether this answer is selected", + "type": "boolean" + } + }, + "required": [ + "id", + "answer", + "selected" + ] + }, + "matrixSubQuestion": { + "type": "object", + "properties": { + "id": { + "description": "The unique identifier of the sub-question", + "type": "string" + }, + "title": { + "description": "The title of the sub-question", + "type": "string" + }, + "order": { + "description": "The display order of the sub-question", + "type": "integer" + }, + "answers": { + "description": "The list of answers for the sub-question", + "type": "array", + "items": { + "$ref": "#/definitions/answer" + }, + "minItems": 1 + } + }, + "required": [ + "id", + "title", + "order", + "answers" + ] + }, + "questionData": { + "type": "object", + "properties": { + "answers": { + "description": "The list of answers (for singleChoice, multiplChoice, number, freeText)", + "type": "array", + "items": { + "$ref": "#/definitions/answer" + } + }, + "questions": { + "description": "The list of sub-questions (for matrix type)", + "type": "array", + "items": { + "$ref": "#/definitions/matrixSubQuestion" + } + } + } + }, + "question": { + "type": "object", + "properties": { + "id": { + "description": "The unique identifier of the question", + "type": "string" + }, + "title": { + "description": "The title/text of the question", + "type": "string" + }, + "order": { + "description": "The display order of the question", + "type": "integer" + }, + "type": { + "description": "The type of question", + "type": "string", + "enum": [ + "singleChoice", + "multiplChoice", + "number", + "freeText", + "matrix" + ] + }, + "data": { + "$ref": "#/definitions/questionData" + } + }, + "required": [ + "id", + "title", + "order", + "type", + "data" + ] + } + } +} diff --git a/hartmann-foto-documentation-app/src/test/java/marketing/heyday/hartmann/fotodocumentation/core/utils/ExcelUtilsTest.java b/hartmann-foto-documentation-app/src/test/java/marketing/heyday/hartmann/fotodocumentation/core/utils/ExcelUtilsTest.java new file mode 100644 index 0000000..ae9d296 --- /dev/null +++ b/hartmann-foto-documentation-app/src/test/java/marketing/heyday/hartmann/fotodocumentation/core/utils/ExcelUtilsTest.java @@ -0,0 +1,270 @@ +package marketing.heyday.hartmann.fotodocumentation.core.utils; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Optional; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.poi.xssf.usermodel.XSSFRow; +import org.apache.poi.xssf.usermodel.XSSFSheet; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import marketing.heyday.hartmann.fotodocumentation.core.model.Questionnaire; +import marketing.heyday.hartmann.fotodocumentation.core.model.QuestionnaireCustomer; + +/** + * + *

Copyright: Copyright (c) 2024

+ *

Company: heyday Marketing GmbH

+ * @author Patrick Verboom + * @version 1.0 + * + * created: 19 Feb 2026 + */ +class ExcelUtilsTest { + private static final Log LOG = LogFactory.getLog(ExcelUtilsTest.class); + + private ExcelUtils excelUtils; + + @BeforeEach + void setUp() { + excelUtils = new ExcelUtils(); + } + + // --- create: basic output --- + + @Test + void create_singleQuestionnaire_returnsPresent() { + QuestionnaireCustomer customer = createCustomer("Müller GmbH", "C-001", "Berlin", "10115"); + Questionnaire questionnaire = createQuestionnaire(new Date(), "What is your rating?"); + + Optional result = excelUtils.create(customer, List.of(questionnaire)); + + assertTrue(result.isPresent()); + } + + @Test + void create_singleQuestionnaire_returnsValidXlsx() throws IOException { + QuestionnaireCustomer customer = createCustomer("Müller GmbH", "C-001", "Berlin", "10115"); + Questionnaire questionnaire = createQuestionnaire(new Date(), "What is your rating?"); + + byte[] bytes = excelUtils.create(customer, List.of(questionnaire)).orElseThrow(); + + try (XSSFWorkbook workbook = new XSSFWorkbook(new ByteArrayInputStream(bytes))) { + assertNotNull(workbook); + } + } + + @Test + void create_emptyQuestionnaires_returnsEmptyWorkbook() throws IOException { + QuestionnaireCustomer customer = createCustomer("Müller GmbH", "C-001", "Berlin", "10115"); + + byte[] bytes = excelUtils.create(customer, Collections.emptyList()).orElseThrow(); + + try (XSSFWorkbook workbook = new XSSFWorkbook(new ByteArrayInputStream(bytes))) { + assertEquals(0, workbook.getNumberOfSheets()); + } + } + + // --- create: sheet count --- + + @Test + void create_singleQuestionnaire_createsOneSheet() throws IOException { + QuestionnaireCustomer customer = createCustomer("Müller GmbH", "C-001", "Berlin", "10115"); + Questionnaire questionnaire = createQuestionnaire(new Date(), "Q1"); + + byte[] bytes = excelUtils.create(customer, List.of(questionnaire)).orElseThrow(); + + try (XSSFWorkbook workbook = new XSSFWorkbook(new ByteArrayInputStream(bytes))) { + assertEquals(1, workbook.getNumberOfSheets()); + } + } + + @Test + void create_multipleQuestionnaires_createsSheetPerQuestionnaire() throws IOException { + QuestionnaireCustomer customer = createCustomer("Müller GmbH", "C-001", "Berlin", "10115"); + Questionnaire q1 = createQuestionnaire(new Date(), "Q1"); + Questionnaire q2 = createQuestionnaire(new Date(), "Q2"); + Questionnaire q3 = createQuestionnaire(new Date(), "Q3"); + + byte[] bytes = excelUtils.create(customer, List.of(q1, q2, q3)).orElseThrow(); + + try (XSSFWorkbook workbook = new XSSFWorkbook(new ByteArrayInputStream(bytes))) { + assertEquals(3, workbook.getNumberOfSheets()); + } + + writeToFile(bytes, "create_multipleQuestionnaires_createsSheetPerQuestionnaire.xlsx"); + } + + // --- create: customer data (each field on its own row) --- + + @Test + void create_writesCustomerNameInRow0() throws IOException { + QuestionnaireCustomer customer = createCustomer("Hartmann AG", "C-100", "München", "80331"); + Questionnaire questionnaire = createQuestionnaire(new Date(), "Q1"); + + byte[] bytes = excelUtils.create(customer, List.of(questionnaire)).orElseThrow(); + + try (XSSFWorkbook workbook = new XSSFWorkbook(new ByteArrayInputStream(bytes))) { + assertEquals("Hartmann AG", workbook.getSheetAt(0).getRow(0).getCell(0).getStringCellValue()); + } + } + + @Test + void create_writesCustomerNumberInRow1() throws IOException { + QuestionnaireCustomer customer = createCustomer("Hartmann AG", "C-100", "München", "80331"); + Questionnaire questionnaire = createQuestionnaire(new Date(), "Q1"); + + byte[] bytes = excelUtils.create(customer, List.of(questionnaire)).orElseThrow(); + + try (XSSFWorkbook workbook = new XSSFWorkbook(new ByteArrayInputStream(bytes))) { + assertEquals("C-100", workbook.getSheetAt(0).getRow(1).getCell(0).getStringCellValue()); + } + } + + @Test + void create_writesCustomerCityInRow2() throws IOException { + QuestionnaireCustomer customer = createCustomer("Hartmann AG", "C-100", "München", "80331"); + Questionnaire questionnaire = createQuestionnaire(new Date(), "Q1"); + + byte[] bytes = excelUtils.create(customer, List.of(questionnaire)).orElseThrow(); + + try (XSSFWorkbook workbook = new XSSFWorkbook(new ByteArrayInputStream(bytes))) { + assertEquals("München", workbook.getSheetAt(0).getRow(2).getCell(0).getStringCellValue()); + } + } + + @Test + void create_writesCustomerZipInRow3() throws IOException { + QuestionnaireCustomer customer = createCustomer("Hartmann AG", "C-100", "München", "80331"); + Questionnaire questionnaire = createQuestionnaire(new Date(), "Q1"); + + byte[] bytes = excelUtils.create(customer, List.of(questionnaire)).orElseThrow(); + + try (XSSFWorkbook workbook = new XSSFWorkbook(new ByteArrayInputStream(bytes))) { + assertEquals("80331", workbook.getSheetAt(0).getRow(3).getCell(0).getStringCellValue()); + } + } + + // --- create: questionnaire date in row 4 --- + + @Test + void create_writesQuestionnaireDateInRow4() throws IOException { + QuestionnaireCustomer customer = createCustomer("Test", "C-001", "Berlin", "10115"); + Date date = new Date(); + Questionnaire questionnaire = createQuestionnaire(date, "Q1"); + + byte[] bytes = excelUtils.create(customer, List.of(questionnaire)).orElseThrow(); + + try (XSSFWorkbook workbook = new XSSFWorkbook(new ByteArrayInputStream(bytes))) { + XSSFRow row = workbook.getSheetAt(0).getRow(4); + assertEquals(date, row.getCell(0).getDateCellValue()); + } + } + + // --- create: question data in row 7 (after 5 customer rows + 2 blank rows) --- + + @Test + void create_writesFirstQuestionTitleInRow7() throws IOException { + QuestionnaireCustomer customer = createCustomer("Test", "C-001", "Berlin", "10115"); + Questionnaire questionnaire = createQuestionnaire(new Date(), null); + + byte[] bytes = excelUtils.create(customer, List.of(questionnaire)).orElseThrow(); + + try (XSSFWorkbook workbook = new XSSFWorkbook(new ByteArrayInputStream(bytes))) { + XSSFSheet sheet = workbook.getSheetAt(0); + // Row 7 should contain the title of the first question from testJson1 + XSSFRow row = sheet.getRow(7); + assertNotNull(row); + assertNotNull(row.getCell(0)); + } + } + + + // --- create: null handling --- + + @Test + void create_nullCustomerFields_writesNullsWithoutError() { + QuestionnaireCustomer customer = createCustomer(null, null, null, null); + Questionnaire questionnaire = createQuestionnaire(null, null); + + Optional result = excelUtils.create(customer, List.of(questionnaire)); + + assertTrue(result.isPresent()); + } + + // --- create: each sheet gets same customer data --- + + @Test + void create_multipleSheets_eachSheetHasCustomerData() throws IOException { + QuestionnaireCustomer customer = createCustomer("Hartmann AG", "C-100", "München", "80331"); + Questionnaire q1 = createQuestionnaire(new Date(), "Q1"); + Questionnaire q2 = createQuestionnaire(new Date(), "Q2"); + + byte[] bytes = excelUtils.create(customer, List.of(q1, q2)).orElseThrow(); + + try (XSSFWorkbook workbook = new XSSFWorkbook(new ByteArrayInputStream(bytes))) { + for (int i = 0; i < workbook.getNumberOfSheets(); i++) { + XSSFSheet sheet = workbook.getSheetAt(i); + assertEquals("Hartmann AG", sheet.getRow(0).getCell(0).getStringCellValue()); + assertEquals("C-100", sheet.getRow(1).getCell(0).getStringCellValue()); + } + } + } + + @Test + void create_multipleSheets_eachSheetHasQuestionData() throws IOException { + QuestionnaireCustomer customer = createCustomer("Test", "C-001", "Berlin", "10115"); + Questionnaire q1 = createQuestionnaire(new Date(), null); + Questionnaire q2 = createQuestionnaire(new Date(), null); + + byte[] bytes = excelUtils.create(customer, List.of(q1, q2)).orElseThrow(); + + try (XSSFWorkbook workbook = new XSSFWorkbook(new ByteArrayInputStream(bytes))) { + for (int i = 0; i < workbook.getNumberOfSheets(); i++) { + XSSFRow row = workbook.getSheetAt(i).getRow(7); + assertNotNull(row); + } + } + } + + // --- helpers --- + + private QuestionnaireCustomer createCustomer(String name, String number, String city, String zip) { + return new QuestionnaireCustomer.Builder() + .name(name) + .customerNumber(number) + .city(city) + .zip(zip) + .build(); + } + + private Questionnaire createQuestionnaire(Date date, String questions) { + return new Questionnaire.Builder() + .questionnaireDate(date) + .questions(QuestionnaireJsonParserTest.testJson1) + .build(); + } + + public void writeToFile(final byte[] content, final String fileName) { + File file = new File("target/test/output/"); + file.mkdirs(); + try (FileOutputStream out = new FileOutputStream(new File(file, fileName))) { + + IOUtils.write(content, out); + } catch (Exception e) { + LOG.error("Error saveing pdf file", e); + } + } +} diff --git a/hartmann-foto-documentation-app/src/test/java/marketing/heyday/hartmann/fotodocumentation/core/utils/ImageUtilTest.java b/hartmann-foto-documentation-app/src/test/java/marketing/heyday/hartmann/fotodocumentation/core/utils/ImageUtilTest.java index 952843c..c747967 100644 --- a/hartmann-foto-documentation-app/src/test/java/marketing/heyday/hartmann/fotodocumentation/core/utils/ImageUtilTest.java +++ b/hartmann-foto-documentation-app/src/test/java/marketing/heyday/hartmann/fotodocumentation/core/utils/ImageUtilTest.java @@ -332,7 +332,7 @@ class ImageUtilTest { @Test void getExifOrientation_invalidBytes_returns1() { - byte[] garbage = new byte[]{0x00, 0x01, 0x02, 0x03}; + byte[] garbage = new byte[] { 0x00, 0x01, 0x02, 0x03 }; int orientation = imageUtil.getExifOrientation(garbage); @@ -364,7 +364,7 @@ class ImageUtilTest { // --- EXIF orientation with resize --- @Test - void getImage_size2_rotatedImage_usesEffectiveWidthForResizeDecision() throws IOException { + void getImage_size2_rotatedImage_usesEffectiveWidthForResizeDecision() { // A tall image (600x800) with no EXIF won't be resized since width (600) < 1200 String base64 = createTestImageBase64(600, 800); byte[] original = Base64.getDecoder().decode(base64); diff --git a/hartmann-foto-documentation-app/src/test/java/marketing/heyday/hartmann/fotodocumentation/core/utils/QuestionnaireJsonParserTest.java b/hartmann-foto-documentation-app/src/test/java/marketing/heyday/hartmann/fotodocumentation/core/utils/QuestionnaireJsonParserTest.java new file mode 100644 index 0000000..8a7f5b6 --- /dev/null +++ b/hartmann-foto-documentation-app/src/test/java/marketing/heyday/hartmann/fotodocumentation/core/utils/QuestionnaireJsonParserTest.java @@ -0,0 +1,349 @@ +package marketing.heyday.hartmann.fotodocumentation.core.utils; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +/** + * + *

Copyright: Copyright (c) 2024

+ *

Company: heyday Marketing GmbH

+ * @author Patrick Verboom + * @version 1.0 + * + * created: 20 Feb 2026 + */ + +public class QuestionnaireJsonParserTest { + + + @Test + public void testJson1() { + var parser = new QuestionnaireJsonParser(); + boolean retVal = parser.parse(testJson1); + assertTrue(retVal); + var questions = parser.getQuestions(); + assertEquals(10, questions.size()); + } + + + public static final String testJson1 = """ +[ + { + "id": "question1", + "title": "", + "order": 1, + "type": "singleChoice", + "data": + { + "answers": + [ + { + "id": "answer1", + "answer": "Nicht-kaufender Kunde", + "selected": true + }, + { + "id": "answer2", + "answer": "Bestandskunde", + "selected": false + } + ] + } + }, + { + "id": "question2", + "title": "Haben Sie Rezeptpatienten(GKV) für Inko?", + "order": 2, + "type": "singleChoice", + "data": + { + "answers": + [ + { + "id": "answer1", + "answer": "Ja", + "selected": false + }, + { + "id": "answer1", + "answer": "Nein", + "selected": true + } + ] + } + }, + { + "id": "question3", + "title": "Warum nicht?", + "order": 3, + "type": "multiplChoice", + "data": + { + "answers": + [ + { + "id": "answer1", + "answer": "Lagergründe", + "selected": false + }, + { + "id": "answer2", + "answer": "Wirtschaftlichkeitsgründe", + "selected": true + }, + { + "id": "answer3", + "answer": "Administrativer Aufwand", + "selected": true + }, + { + "id": "answer4", + "answer": "Personeller Aufwand", + "selected": false + } + ] + } + }, + { + "id": "question4", + "title": "Haben Sie Privatrezeptpatienten für inko?", + "order": 4, + "type": "singleChoice", + "data": + { + "answers": + [ + { + "id": "answer1", + "answer": "Ja", + "selected": false + }, + { + "id": "answer2", + "answer": "Nein", + "selected": true + } + ] + } + }, + { + "id": "question5", + "title": "Wie viele Patienten versorgen Sie regelmäßig? (Privat) un GKV", + "order": 5, + "type": "number", + "data": + { + "answers": + [ + { + "id": "answer1", + "answer": 47, + "selected": true + } + ] + } + }, + { + "id": "question6", + "title": "Mit welchem Herstellern arbeiten Sie zusammen?", + "order": 6, + "type": "multiplChoice", + "data": + { + "answers": + [ + { + "id": "answer1", + "answer": "HARTMANN", + "selected": false + }, + { + "id": "answer2", + "answer": "TZMO", + "selected": true + }, + { + "id": "answer3", + "answer": "Essity", + "selected": true + }, + { + "id": "answer4", + "answer": "Ontex", + "selected": true + }, + { + "id": "answer5", + "answer": "Param", + "selected": false + }, + { + "id": "answer6", + "answer": "Andere", + "selected": false + } + ] + } + }, + { + "id": "question7", + "title": "Was sind Ihre Gründe für die Zusammenarbeit?", + "order": 7, + "type": "matrix", + "data": + { + "questions": + [ + { + "id": "subq1", + "title": "Preis", + "order": 1, + "answers": + [ + { + "id": "answer1", + "answer": "HARTMANN", + "selected": true + }, + { + "id": "answer2", + "answer": "Essity", + "selected": true + }, + { + "id": "answer3", + "answer": "Ontex", + "selected": false + } + ] + }, + { + "id": "subq2", + "title": "Einkaufskondition", + "order": 2, + "answers": + [ + { + "id": "answer1", + "answer": "HARTMANN", + "selected": false + }, + { + "id": "answer2", + "answer": "Essity", + "selected": true + }, + { + "id": "answer3", + "answer": "Ontex", + "selected": false + } + ] + }, + { + "id": "subq3", + "title": "Qualität", + "order": 3, + "answers": + [ + { + "id": "answer1", + "answer": "HARTMANN", + "selected": false + }, + { + "id": "answer2", + "answer": "Essity", + "selected": true + }, + { + "id": "answer3", + "answer": "Ontex", + "selected": true + } + ] + } + ] + } + }, + { + "id": "question8", + "title": "Beziehen Sie Produkte direkt oder über den Großhandel?", + "order": 8, + "type": "singleChoice", + "data": + { + "answers": + [ + { + "id": "answer1", + "answer": "primär direkt", + "selected": false + }, + { + "id": "answer2", + "answer": "primär Großhandel", + "selected": true + }, + { + "id": "answer3", + "answer": "ptimär teils. Großhandel", + "selected": true + }, + { + "id": "answer4", + "answer": "unterschiedlich", + "selected": false + } + ] + } + }, + { + "id": "question9", + "title": "Gründe für Bezug?", + "order": 10, + "type": "singleChoice", + "data": + { + "answers": + [ + { + "id": "answer1", + "answer": "Umsatzziel mit Händler", + "selected": false + }, + { + "id": "answer2", + "answer": "Warenverfügbarkeit/ Liefergeschwindigkeit und Frequent", + "selected": true + }, + { + "id": "answer3", + "answer": "Einkaufskondition", + "selected": true + } + ] + } + }, + { + "id": "question10", + "title": "Weiter/Kommentare Hinweise?", + "order": 11, + "type": "freeText", + "data": + { + "answers": + [ + { + "id": "answer1", + "answer": "Kommentar eintragen", + "selected": true + } + ] + } + } +] +"""; +} diff --git a/hartmann-foto-documentation-app/src/test/java/marketing/heyday/hartmann/fotodocumentation/core/utils/QuestionnaireUploadJsonParserTest.java b/hartmann-foto-documentation-app/src/test/java/marketing/heyday/hartmann/fotodocumentation/core/utils/QuestionnaireUploadJsonParserTest.java new file mode 100644 index 0000000..e093ced --- /dev/null +++ b/hartmann-foto-documentation-app/src/test/java/marketing/heyday/hartmann/fotodocumentation/core/utils/QuestionnaireUploadJsonParserTest.java @@ -0,0 +1,270 @@ +package marketing.heyday.hartmann.fotodocumentation.core.utils; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.Optional; + +import org.junit.jupiter.api.Test; + +/** + * + *

Copyright: Copyright (c) 2024

+ *

Company: heyday Marketing GmbH

+ * @author Patrick Verboom + * @version 1.0 + * + * created: 20 Feb 2026 + */ +class QuestionnaireUploadJsonParserTest { + + // --- builder: successful parsing --- + + @Test + void builder_validJson_returnsPresent() { + String json = createValidJson(); + + Optional result = QuestionnaireUploadJsonParser.builder(json); + + assertTrue(result.isPresent()); + } + + @Test + void builder_validJson_parsesUsername() { + String json = createValidJson(); + + QuestionnaireUploadJsonParser parser = QuestionnaireUploadJsonParser.builder(json).orElseThrow(); + + assertEquals("verboomp", parser.username()); + } + + @Test + void builder_validJson_parsesPharmacyName() { + String json = createValidJson(); + + QuestionnaireUploadJsonParser parser = QuestionnaireUploadJsonParser.builder(json).orElseThrow(); + + assertEquals("Müller Apotheke", parser.pharmacyName()); + } + + @Test + void builder_validJson_parsesCustomerNumber() { + String json = createValidJson(); + + QuestionnaireUploadJsonParser parser = QuestionnaireUploadJsonParser.builder(json).orElseThrow(); + + assertEquals("1234", parser.customerNumber()); + } + + @Test + void builder_validJson_parsesDate() { + String json = createValidJson(); + + QuestionnaireUploadJsonParser parser = QuestionnaireUploadJsonParser.builder(json).orElseThrow(); + + assertNotNull(parser.date()); + } + + @Test + void builder_validJson_parsesComment() { + String json = createValidJson(); + + QuestionnaireUploadJsonParser parser = QuestionnaireUploadJsonParser.builder(json).orElseThrow(); + + assertEquals("Some long text", parser.comment()); + } + + @Test + void builder_validJson_parsesCity() { + String json = createValidJson(); + + QuestionnaireUploadJsonParser parser = QuestionnaireUploadJsonParser.builder(json).orElseThrow(); + + assertEquals("Hannover", parser.city()); + } + + @Test + void builder_validJson_parsesZip() { + String json = createValidJson(); + + QuestionnaireUploadJsonParser parser = QuestionnaireUploadJsonParser.builder(json).orElseThrow(); + + assertEquals("12345", parser.zip()); + } + + @Test + void builder_validJson_parsesQuestionnaireAsJsonString() { + String json = createValidJson(); + + QuestionnaireUploadJsonParser parser = QuestionnaireUploadJsonParser.builder(json).orElseThrow(); + + assertNotNull(parser.questionnair()); + assertTrue(parser.questionnair().contains("question1")); + assertTrue(parser.questionnair().contains("singleChoice")); + } + + @Test + void builder_validJson_questionnaireContainsAnswers() { + String json = createValidJson(); + + QuestionnaireUploadJsonParser parser = QuestionnaireUploadJsonParser.builder(json).orElseThrow(); + + assertTrue(parser.questionnair().contains("answer1")); + assertTrue(parser.questionnair().contains("Ja")); + } + + // --- builder: multiple questions --- + + @Test + void builder_multipleQuestions_allQuestionsInOutput() { + String json = createJsonWithMultipleQuestions(); + + QuestionnaireUploadJsonParser parser = QuestionnaireUploadJsonParser.builder(json).orElseThrow(); + + assertTrue(parser.questionnair().contains("question1")); + assertTrue(parser.questionnair().contains("question2")); + } + + // --- builder: invalid input --- + + @Test + void builder_invalidJson_returnsEmpty() { + String json = "not valid json"; + + Optional result = QuestionnaireUploadJsonParser.builder(json); + + assertTrue(result.isEmpty()); + } + + @Test + void builder_emptyObject_returnsEmpty() { + String json = "{}"; + + Optional result = QuestionnaireUploadJsonParser.builder(json); + + assertTrue(result.isEmpty()); + } + + @Test + void builder_missingRequiredField_returnsEmpty() { + String json = """ + { + "username": "verboomp", + "pharmacyName": "Test" + }"""; + + Optional result = QuestionnaireUploadJsonParser.builder(json); + + assertTrue(result.isEmpty()); + } + + @Test + void builder_invalidDateFormat_returnsEmpty() { + String json = """ + { + "username": "verboomp", + "pharmacyName": "Test", + "customerNumber": "1234", + "date": "not-a-date", + "comment": "test", + "city": "Berlin", + "zip": "10115", + "questionnaire": [{"id": "q1", "title": "Q", "order": 1, "type": "freeText", "data": {"answers": []}}] + }"""; + + Optional result = QuestionnaireUploadJsonParser.builder(json); + + assertTrue(result.isEmpty()); + } + + // --- builder: special characters --- + + @Test + void builder_unicodeCharacters_parsedCorrectly() { + String json = createJsonWithValues("user1", "Löwen Apotheke", "5678", "Köln", "50667"); + + QuestionnaireUploadJsonParser parser = QuestionnaireUploadJsonParser.builder(json).orElseThrow(); + + assertEquals("Löwen Apotheke", parser.pharmacyName()); + assertEquals("Köln", parser.city()); + } + + // --- helpers --- + + private String createValidJson() { + return """ + { + "username": "verboomp", + "pharmacyName": "Müller Apotheke", + "customerNumber": "1234", + "date": "2026-01-20T11:06:00+01:00", + "comment": "Some long text", + "city": "Hannover", + "zip": "12345", + "questionnaire": [ + { + "id": "question1", + "title": "Kundentyp", + "order": 1, + "type": "singleChoice", + "data": { + "answers": [ + {"id": "answer1", "answer": "Ja", "selected": true}, + {"id": "answer2", "answer": "Nein", "selected": false} + ] + } + } + ] + }"""; + } + + private String createJsonWithMultipleQuestions() { + return """ + { + "username": "verboomp", + "pharmacyName": "Test Apotheke", + "customerNumber": "9999", + "date": "2026-01-20T11:06:00+01:00", + "comment": "comment", + "city": "Berlin", + "zip": "10115", + "questionnaire": [ + { + "id": "question1", + "title": "First", + "order": 1, + "type": "singleChoice", + "data": {"answers": [{"id": "a1", "answer": "Yes", "selected": true}]} + }, + { + "id": "question2", + "title": "Second", + "order": 2, + "type": "freeText", + "data": {"answers": [{"id": "a1", "answer": "Some text", "selected": true}]} + } + ] + }"""; + } + + private String createJsonWithValues(String username, String pharmacyName, String customerNumber, String city, String zip) { + return """ + { + "username": "%s", + "pharmacyName": "%s", + "customerNumber": "%s", + "date": "2026-01-20T11:06:00+01:00", + "comment": "test", + "city": "%s", + "zip": "%s", + "questionnaire": [ + { + "id": "q1", + "title": "Q", + "order": 1, + "type": "freeText", + "data": {"answers": [{"id": "a1", "answer": "text", "selected": true}]} + } + ] + }""".formatted(username, pharmacyName, customerNumber, city, zip); + } +} diff --git a/hartmann-foto-documentation-docker/src/test/java/marketing/heyday/hartmann/fotodocumentation/rest/QuestionnairePublishResourceTest.java b/hartmann-foto-documentation-docker/src/test/java/marketing/heyday/hartmann/fotodocumentation/rest/QuestionnairePublishResourceTest.java index 8bfef83..e0021ad 100644 --- a/hartmann-foto-documentation-docker/src/test/java/marketing/heyday/hartmann/fotodocumentation/rest/QuestionnairePublishResourceTest.java +++ b/hartmann-foto-documentation-docker/src/test/java/marketing/heyday/hartmann/fotodocumentation/rest/QuestionnairePublishResourceTest.java @@ -35,7 +35,6 @@ public class QuestionnairePublishResourceTest extends AbstractQuestionnaireTest @Test @Order(2) - @Disabled // FIXME: enable when implemented public void doAddCustomerdoAddQuestionniare() throws IOException { LOG.info("doAddCustomerdoAddQuestionniare"); @@ -43,8 +42,8 @@ public class QuestionnairePublishResourceTest extends AbstractQuestionnaireTest assertEquals(5, questionnaireCount()); String path = deploymentURL + PATH; - Request request = Request.Post(path).addHeader("Accept", "application/json; charset=utf-8") - .bodyFile(new File(BASE_UPLOAD + "add.json"), ContentType.APPLICATION_JSON); + Request request = Request.Post(path)//.addHeader("Accept", "application/json; charset=utf-8") + .bodyFile(new File(BASE_UPLOAD + "questionnaire_add.json"), ContentType.APPLICATION_JSON); HttpResponse httpResponse = executeRequest(request); int code = httpResponse.getStatusLine().getStatusCode(); @@ -56,7 +55,6 @@ public class QuestionnairePublishResourceTest extends AbstractQuestionnaireTest @Test @Order(3) - @Disabled // FIXME: enable when implemented public void doAddCustomerWithQuestionnaire() throws IOException { LOG.info("doAddCustomerWithQuestionnaire"); @@ -65,7 +63,7 @@ public class QuestionnairePublishResourceTest extends AbstractQuestionnaireTest String path = deploymentURL + PATH; Request request = Request.Post(path).addHeader("Accept", "application/json; charset=utf-8") - .bodyFile(new File(BASE_UPLOAD + "addNewCustomer.json"), ContentType.APPLICATION_JSON); + .bodyFile(new File(BASE_UPLOAD + "questionnaire_addNewCustomer.json"), ContentType.APPLICATION_JSON); HttpResponse httpResponse = executeRequest(request); int code = httpResponse.getStatusLine().getStatusCode(); @@ -77,13 +75,12 @@ public class QuestionnairePublishResourceTest extends AbstractQuestionnaireTest @Test @Order(1) - @Disabled // FIXME: enable when implemented public void doAddCustomerWithQuestionnaireWrongJson() throws IOException { LOG.info("doAddCustomerWithQuestionnaireWrongJson"); String path = deploymentURL + PATH; Request request = Request.Post(path).addHeader("Accept", "application/json; charset=utf-8") - .bodyFile(new File(BASE_UPLOAD + "addWrong.json"), ContentType.APPLICATION_JSON); + .bodyFile(new File(BASE_UPLOAD + "questionnaire_addWrong.json"), ContentType.APPLICATION_JSON); HttpResponse httpResponse = executeRequest(request); int code = httpResponse.getStatusLine().getStatusCode(); diff --git a/hartmann-foto-documentation-docker/src/test/resources/upload/questionnaire_add.json b/hartmann-foto-documentation-docker/src/test/resources/upload/questionnaire_add.json new file mode 100644 index 0000000..d1384a3 --- /dev/null +++ b/hartmann-foto-documentation-docker/src/test/resources/upload/questionnaire_add.json @@ -0,0 +1,302 @@ +{ + "username": "verboomp", + "pharmacyName": "Müller Apotheke", + "customerNumber": "1234", + "date": "2026-01-20T11:06:00+01:00", + "comment": "Some long text for Müller Pharmacy", + "city": "Hannover", + "zip": "12345", + "questionnaire": [ + { + "id": "question1", + "title": "", + "order": 1, + "type": "singleChoice", + "data": { + "answers": [ + { + "id": "answer1", + "answer": "Nicht-kaufender Kunde", + "selected": true + }, + { + "id": "answer2", + "answer": "Bestandskunde", + "selected": false + } + ] + } + }, + { + "id": "question2", + "title": "Haben Sie Rezeptpatienten(GKV) für Inko?", + "order": 2, + "type": "singleChoice", + "data": { + "answers": [ + { + "id": "answer1", + "answer": "Ja", + "selected": false + }, + { + "id": "answer1", + "answer": "Nein", + "selected": true + } + ] + } + }, + { + "id": "question3", + "title": "Warum nicht?", + "order": 3, + "type": "multiplChoice", + "data": { + "answers": [ + { + "id": "answer1", + "answer": "Lagergründe", + "selected": false + }, + { + "id": "answer2", + "answer": "Wirtschaftlichkeitsgründe", + "selected": true + }, + { + "id": "answer3", + "answer": "Administrativer Aufwand", + "selected": true + }, + { + "id": "answer4", + "answer": "Personeller Aufwand", + "selected": false + } + ] + } + }, + { + "id": "question4", + "title": "Haben Sie Privatrezeptpatienten für inko?", + "order": 4, + "type": "singleChoice", + "data": { + "answers": [ + { + "id": "answer1", + "answer": "Ja", + "selected": false + }, + { + "id": "answer2", + "answer": "Nein", + "selected": true + } + ] + } + }, + { + "id": "question5", + "title": "Wie viele Patienten versorgen Sie regelmäßig?\n(Privat) un GKV", + "order": 5, + "type": "number", + "data": { + "answers": [ + { + "id": "answer1", + "answer": "47", + "selected": true + } + ] + } + }, + { + "id": "question6", + "title": "Mit welchem Herstellern arbeiten Sie zusammen?", + "order": 6, + "type": "multiplChoice", + "data": { + "answers": [ + { + "id": "answer1", + "answer": "HARTMANN", + "selected": false + }, + { + "id": "answer2", + "answer": "TZMO", + "selected": true + }, + { + "id": "answer3", + "answer": "Essity", + "selected": true + }, + { + "id": "answer4", + "answer": "Ontex", + "selected": false + }, + { + "id": "answer5", + "answer": "Param", + "selected": false + }, + { + "id": "answer6", + "answer": "Andere", + "selected": false + } + ] + } + }, + { + "id": "question7", + "title": "Was sind Ihre Gründe für die Zusammenarbeit?", + "order": 7, + "type": "matrix", + "data": { + "questions": [ + { + "id": "subq1", + "title": "Preis", + "order": 1, + "answers": [ + { + "id": "answer1", + "answer": "HARTMANN", + "selected": false + }, + { + "id": "answer2", + "answer": "Essity", + "selected": true + }, + { + "id": "answer3", + "answer": "Ontex", + "selected": false + } + ] + }, + { + "id": "subq2", + "title": "Einkaufskondition", + "order": 2, + "answers": [ + { + "id": "answer1", + "answer": "HARTMANN", + "selected": false + }, + { + "id": "answer2", + "answer": "Essity", + "selected": true + }, + { + "id": "answer3", + "answer": "Ontex", + "selected": false + } + ] + }, + { + "id": "subq3", + "title": "Qualität", + "order": 3, + "answers": [ + { + "id": "answer1", + "answer": "HARTMANN", + "selected": false + }, + { + "id": "answer2", + "answer": "Essity", + "selected": true + }, + { + "id": "answer3", + "answer": "Ontex", + "selected": false + } + ] + } + ] + } + }, + { + "id": "question8", + "title": "Beziehen Sie Produkte direkt oder über den Großhandel?", + "order": 8, + "type": "singleChoice", + "data": { + "answers": [ + { + "id": "answer1", + "answer": "primär direkt", + "selected": false + }, + { + "id": "answer2", + "answer": "primär Großhandel", + "selected": true + }, + { + "id": "answer3", + "answer": "ptimär teils. Großhandel", + "selected": true + }, + { + "id": "answer4", + "answer": "unterschiedlich", + "selected": false + } + ] + } + }, + { + "id": "question9", + "title": "Gründe für Bezug?", + "order": 10, + "type": "singleChoice", + "data": { + "answers": [ + { + "id": "answer1", + "answer": "Umsatzziel mit Händler", + "selected": false + }, + { + "id": "answer2", + "answer": "Warenverfügbarkeit/ Liefergeschwindigkeit und Frequent", + "selected": true + }, + { + "id": "answer3", + "answer": "Einkaufskondition", + "selected": true + } + ] + } + }, + { + "id": "question10", + "title": "Weiter/Kommentare Hinweise?", + "order": 11, + "type": "freeText", + "data": { + "answers": [ + { + "id": "answer1", + "answer": "Kommentar eintragen", + "selected": true + } + ] + } + } + ] +} \ No newline at end of file diff --git a/hartmann-foto-documentation-docker/src/test/resources/upload/questionnaire_addNewCustomer.json b/hartmann-foto-documentation-docker/src/test/resources/upload/questionnaire_addNewCustomer.json new file mode 100644 index 0000000..033004a --- /dev/null +++ b/hartmann-foto-documentation-docker/src/test/resources/upload/questionnaire_addNewCustomer.json @@ -0,0 +1,302 @@ +{ + "username": "verboomp", + "pharmacyName": "New Apotheke", + "customerNumber": "new_number", + "date": "2026-01-20T11:06:00+01:00", + "comment": "Some long text for New Pharmacy", + "city": "Hannover", + "zip": "12345", + "questionnaire": [ + { + "id": "question1", + "title": "", + "order": 1, + "type": "singleChoice", + "data": { + "answers": [ + { + "id": "answer1", + "answer": "Nicht-kaufender Kunde", + "selected": true + }, + { + "id": "answer2", + "answer": "Bestandskunde", + "selected": false + } + ] + } + }, + { + "id": "question2", + "title": "Haben Sie Rezeptpatienten(GKV) für Inko?", + "order": 2, + "type": "singleChoice", + "data": { + "answers": [ + { + "id": "answer1", + "answer": "Ja", + "selected": false + }, + { + "id": "answer1", + "answer": "Nein", + "selected": true + } + ] + } + }, + { + "id": "question3", + "title": "Warum nicht?", + "order": 3, + "type": "multiplChoice", + "data": { + "answers": [ + { + "id": "answer1", + "answer": "Lagergründe", + "selected": false + }, + { + "id": "answer2", + "answer": "Wirtschaftlichkeitsgründe", + "selected": true + }, + { + "id": "answer3", + "answer": "Administrativer Aufwand", + "selected": true + }, + { + "id": "answer4", + "answer": "Personeller Aufwand", + "selected": false + } + ] + } + }, + { + "id": "question4", + "title": "Haben Sie Privatrezeptpatienten für inko?", + "order": 4, + "type": "singleChoice", + "data": { + "answers": [ + { + "id": "answer1", + "answer": "Ja", + "selected": false + }, + { + "id": "answer2", + "answer": "Nein", + "selected": true + } + ] + } + }, + { + "id": "question5", + "title": "Wie viele Patienten versorgen Sie regelmäßig?\n(Privat) un GKV", + "order": 5, + "type": "number", + "data": { + "answers": [ + { + "id": "answer1", + "answer": "47", + "selected": true + } + ] + } + }, + { + "id": "question6", + "title": "Mit welchem Herstellern arbeiten Sie zusammen?", + "order": 6, + "type": "multiplChoice", + "data": { + "answers": [ + { + "id": "answer1", + "answer": "HARTMANN", + "selected": false + }, + { + "id": "answer2", + "answer": "TZMO", + "selected": true + }, + { + "id": "answer3", + "answer": "Essity", + "selected": true + }, + { + "id": "answer4", + "answer": "Ontex", + "selected": false + }, + { + "id": "answer5", + "answer": "Param", + "selected": false + }, + { + "id": "answer6", + "answer": "Andere", + "selected": false + } + ] + } + }, + { + "id": "question7", + "title": "Was sind Ihre Gründe für die Zusammenarbeit?", + "order": 7, + "type": "matrix", + "data": { + "questions": [ + { + "id": "subq1", + "title": "Preis", + "order": 1, + "answers": [ + { + "id": "answer1", + "answer": "HARTMANN", + "selected": false + }, + { + "id": "answer2", + "answer": "Essity", + "selected": true + }, + { + "id": "answer3", + "answer": "Ontex", + "selected": false + } + ] + }, + { + "id": "subq2", + "title": "Einkaufskondition", + "order": 2, + "answers": [ + { + "id": "answer1", + "answer": "HARTMANN", + "selected": false + }, + { + "id": "answer2", + "answer": "Essity", + "selected": true + }, + { + "id": "answer3", + "answer": "Ontex", + "selected": false + } + ] + }, + { + "id": "subq3", + "title": "Qualität", + "order": 3, + "answers": [ + { + "id": "answer1", + "answer": "HARTMANN", + "selected": false + }, + { + "id": "answer2", + "answer": "Essity", + "selected": true + }, + { + "id": "answer3", + "answer": "Ontex", + "selected": false + } + ] + } + ] + } + }, + { + "id": "question8", + "title": "Beziehen Sie Produkte direkt oder über den Großhandel?", + "order": 8, + "type": "singleChoice", + "data": { + "answers": [ + { + "id": "answer1", + "answer": "primär direkt", + "selected": false + }, + { + "id": "answer2", + "answer": "primär Großhandel", + "selected": true + }, + { + "id": "answer3", + "answer": "ptimär teils. Großhandel", + "selected": true + }, + { + "id": "answer4", + "answer": "unterschiedlich", + "selected": false + } + ] + } + }, + { + "id": "question9", + "title": "Gründe für Bezug?", + "order": 10, + "type": "singleChoice", + "data": { + "answers": [ + { + "id": "answer1", + "answer": "Umsatzziel mit Händler", + "selected": false + }, + { + "id": "answer2", + "answer": "Warenverfügbarkeit/ Liefergeschwindigkeit und Frequent", + "selected": true + }, + { + "id": "answer3", + "answer": "Einkaufskondition", + "selected": true + } + ] + } + }, + { + "id": "question10", + "title": "Weiter/Kommentare Hinweise?", + "order": 11, + "type": "freeText", + "data": { + "answers": [ + { + "id": "answer1", + "answer": "Kommentar eintragen", + "selected": true + } + ] + } + } + ] +} \ No newline at end of file diff --git a/hartmann-foto-documentation-docker/src/test/resources/upload/questionnaire_addWrong.json b/hartmann-foto-documentation-docker/src/test/resources/upload/questionnaire_addWrong.json new file mode 100644 index 0000000..160658f --- /dev/null +++ b/hartmann-foto-documentation-docker/src/test/resources/upload/questionnaire_addWrong.json @@ -0,0 +1,298 @@ +{ + "username": "verboomp", + "date": "2026-01-20T11:06:00+01:00", + "comment": "Some long text for Müller Pharmacy", + "questionnaire": [ + { + "id": "question1", + "title": "", + "order": 1, + "type": "singleChoice", + "data": { + "answers": [ + { + "id": "answer1", + "answer": "Nicht-kaufender Kunde", + "selected": true + }, + { + "id": "answer2", + "answer": "Bestandskunde", + "selected": false + } + ] + } + }, + { + "id": "question2", + "title": "Haben Sie Rezeptpatienten(GKV) für Inko?", + "order": 2, + "type": "singleChoice", + "data": { + "answers": [ + { + "id": "answer1", + "answer": "Ja", + "selected": false + }, + { + "id": "answer1", + "answer": "Nein", + "selected": true + } + ] + } + }, + { + "id": "question3", + "title": "Warum nicht?", + "order": 3, + "type": "multiplChoice", + "data": { + "answers": [ + { + "id": "answer1", + "answer": "Lagergründe", + "selected": false + }, + { + "id": "answer2", + "answer": "Wirtschaftlichkeitsgründe", + "selected": true + }, + { + "id": "answer3", + "answer": "Administrativer Aufwand", + "selected": true + }, + { + "id": "answer4", + "answer": "Personeller Aufwand", + "selected": false + } + ] + } + }, + { + "id": "question4", + "title": "Haben Sie Privatrezeptpatienten für inko?", + "order": 4, + "type": "singleChoice", + "data": { + "answers": [ + { + "id": "answer1", + "answer": "Ja", + "selected": false + }, + { + "id": "answer2", + "answer": "Nein", + "selected": true + } + ] + } + }, + { + "id": "question5", + "title": "Wie viele Patienten versorgen Sie regelmäßig?\n(Privat) un GKV", + "order": 5, + "type": "number", + "data": { + "answers": [ + { + "id": "answer1", + "answer": "47", + "selected": true + } + ] + } + }, + { + "id": "question6", + "title": "Mit welchem Herstellern arbeiten Sie zusammen?", + "order": 6, + "type": "multiplChoice", + "data": { + "answers": [ + { + "id": "answer1", + "answer": "HARTMANN", + "selected": false + }, + { + "id": "answer2", + "answer": "TZMO", + "selected": true + }, + { + "id": "answer3", + "answer": "Essity", + "selected": true + }, + { + "id": "answer4", + "answer": "Ontex", + "selected": false + }, + { + "id": "answer5", + "answer": "Param", + "selected": false + }, + { + "id": "answer6", + "answer": "Andere", + "selected": false + } + ] + } + }, + { + "id": "question7", + "title": "Was sind Ihre Gründe für die Zusammenarbeit?", + "order": 7, + "type": "matrix", + "data": { + "questions": [ + { + "id": "subq1", + "title": "Preis", + "order": 1, + "answers": [ + { + "id": "answer1", + "answer": "HARTMANN", + "selected": false + }, + { + "id": "answer2", + "answer": "Essity", + "selected": true + }, + { + "id": "answer3", + "answer": "Ontex", + "selected": false + } + ] + }, + { + "id": "subq2", + "title": "Einkaufskondition", + "order": 2, + "answers": [ + { + "id": "answer1", + "answer": "HARTMANN", + "selected": false + }, + { + "id": "answer2", + "answer": "Essity", + "selected": true + }, + { + "id": "answer3", + "answer": "Ontex", + "selected": false + } + ] + }, + { + "id": "subq3", + "title": "Qualität", + "order": 3, + "answers": [ + { + "id": "answer1", + "answer": "HARTMANN", + "selected": false + }, + { + "id": "answer2", + "answer": "Essity", + "selected": true + }, + { + "id": "answer3", + "answer": "Ontex", + "selected": false + } + ] + } + ] + } + }, + { + "id": "question8", + "title": "Beziehen Sie Produkte direkt oder über den Großhandel?", + "order": 8, + "type": "singleChoice", + "data": { + "answers": [ + { + "id": "answer1", + "answer": "primär direkt", + "selected": false + }, + { + "id": "answer2", + "answer": "primär Großhandel", + "selected": true + }, + { + "id": "answer3", + "answer": "ptimär teils. Großhandel", + "selected": true + }, + { + "id": "answer4", + "answer": "unterschiedlich", + "selected": false + } + ] + } + }, + { + "id": "question9", + "title": "Gründe für Bezug?", + "order": 10, + "type": "singleChoice", + "data": { + "answers": [ + { + "id": "answer1", + "answer": "Umsatzziel mit Händler", + "selected": false + }, + { + "id": "answer2", + "answer": "Warenverfügbarkeit/ Liefergeschwindigkeit und Frequent", + "selected": true + }, + { + "id": "answer3", + "answer": "Einkaufskondition", + "selected": true + } + ] + } + }, + { + "id": "question10", + "title": "Weiter/Kommentare Hinweise?", + "order": 11, + "type": "freeText", + "data": { + "answers": [ + { + "id": "answer1", + "answer": "Kommentar eintragen", + "selected": true + } + ] + } + } + ] +} \ No newline at end of file diff --git a/hartmann-foto-documentation/pom.xml b/hartmann-foto-documentation/pom.xml index cc8983e..c8af823 100644 --- a/hartmann-foto-documentation/pom.xml +++ b/hartmann-foto-documentation/pom.xml @@ -290,12 +290,13 @@ org.apache.poi poi - 5.1.0 + 5.5.1 + org.apache.poi poi-ooxml - 5.1.0 + 5.5.1 dom4j diff --git a/question_answer.json b/question_answer.json new file mode 100644 index 0000000..ddd3ea8 --- /dev/null +++ b/question_answer.json @@ -0,0 +1,316 @@ +[ + { + "id": "question1", + "title": "", + "order": 1, + "type": "singleChoice", + "data": + { + "answers": + [ + { + "id": "answer1", + "answer": "Nicht-kaufender Kunde", + "selected": true + }, + { + "id": "answer2", + "answer": "Bestandskunde", + "selected": false + } + ] + } + }, + { + "id": "question2", + "title": "Haben Sie Rezeptpatienten(GKV) für Inko?", + "order": 2, + "type": "singleChoice", + "data": + { + "answers": + [ + { + "id": "answer1", + "answer": "Ja", + "selected": false + }, + { + "id": "answer1", + "answer": "Nein", + "selected": true + } + ] + } + }, + { + "id": "question3", + "title": "Warum nicht?", + "order": 3, + "type": "multiplChoice", + "data": + { + "answers": + [ + { + "id": "answer1", + "answer": "Lagergründe", + "selected": false + }, + { + "id": "answer2", + "answer": "Wirtschaftlichkeitsgründe", + "selected": true + }, + { + "id": "answer3", + "answer": "Administrativer Aufwand", + "selected": true + }, + { + "id": "answer4", + "answer": "Personeller Aufwand", + "selected": false + } + ] + } + }, + { + "id": "question4", + "title": "Haben Sie Privatrezeptpatienten für inko?", + "order": 4, + "type": "singleChoice", + "data": + { + "answers": + [ + { + "id": "answer1", + "answer": "Ja", + "selected": false + }, + { + "id": "answer2", + "answer": "Nein", + "selected": true + } + ] + } + }, + { + "id": "question5", + "title": "Wie viele Patienten versorgen Sie regelmäßig?\n(Privat) un GKV", + "order": 5, + "type": "number", + "data": + { + "answers": + [ + { + "id": "answer1", + "answer": "47", + "selected": true + } + ] + } + }, + { + "id": "question6", + "title": "Mit welchem Herstellern arbeiten Sie zusammen?", + "order": 6, + "type": "multiplChoice", + "data": + { + "answers": + [ + { + "id": "answer1", + "answer": "HARTMANN", + "selected": false + }, + { + "id": "answer2", + "answer": "TZMO", + "selected": true + }, + { + "id": "answer3", + "answer": "Essity", + "selected": true + }, + { + "id": "answer4", + "answer": "Ontex", + "selected": false + }, + { + "id": "answer5", + "answer": "Param", + "selected": false + }, + { + "id": "answer6", + "answer": "Andere", + "selected": false + } + ] + } + }, + { + "id": "question7", + "title": "Was sind Ihre Gründe für die Zusammenarbeit?", + "order": 7, + "type": "matrix", + "data": + { + "questions": + [ + { + "id": "subq1", + "title": "Preis", + "order": 1, + "answers": + [ + { + "id": "answer1", + "answer": "HARTMANN", + "selected": false + }, + { + "id": "answer2", + "answer": "Essity", + "selected": true + }, + { + "id": "answer3", + "answer": "Ontex", + "selected": false + } + ] + }, + { + "id": "subq2", + "title": "Einkaufskondition", + "order": 2, + "answers": + [ + { + "id": "answer1", + "answer": "HARTMANN", + "selected": false + }, + { + "id": "answer2", + "answer": "Essity", + "selected": true + }, + { + "id": "answer3", + "answer": "Ontex", + "selected": false + } + ] + }, + { + "id": "subq3", + "title": "Qualität", + "order": 3, + "answers": + [ + { + "id": "answer1", + "answer": "HARTMANN", + "selected": false + }, + { + "id": "answer2", + "answer": "Essity", + "selected": true + }, + { + "id": "answer3", + "answer": "Ontex", + "selected": false + } + ] + } + ] + } + }, + { + "id": "question8", + "title": "Beziehen Sie Produkte direkt oder über den Großhandel?", + "order": 8, + "type": "singleChoice", + "data": + { + "answers": + [ + { + "id": "answer1", + "answer": "primär direkt", + "selected": false + }, + { + "id": "answer2", + "answer": "primär Großhandel", + "selected": true + }, + { + "id": "answer3", + "answer": "ptimär teils. Großhandel", + "selected": true + }, + { + "id": "answer4", + "answer": "unterschiedlich", + "selected": false + } + ] + } + }, + { + "id": "question9", + "title": "Gründe für Bezug?", + "order": 10, + "type": "singleChoice", + "data": + { + "answers": + [ + { + "id": "answer1", + "answer": "Umsatzziel mit Händler", + "selected": false + }, + { + "id": "answer2", + "answer": "Warenverfügbarkeit/ Liefergeschwindigkeit und Frequent", + "selected": true + }, + { + "id": "answer3", + "answer": "Einkaufskondition", + "selected": true + } + ] + } + }, + { + "id": "question10", + "title": "Weiter/Kommentare Hinweise?", + "order": 11, + "type": "freeText", + "data": + { + "answers": + [ + { + "id": "answer1", + "answer": "Kommentar eintragen", + "selected": true + } + ] + } + } +] \ No newline at end of file