diff --git a/pom.xml b/pom.xml
index 81d76b6a..40e2e727 100644
--- a/pom.xml
+++ b/pom.xml
@@ -273,6 +273,19 @@
2.16.0
test
+
+
+ com.squareup.okhttp3
+ okhttp
+ 4.0.1
+ test
+
+
+ com.squareup.okhttp3
+ mockwebserver
+ 4.0.1
+ test
+
diff --git a/src/main/java/ch/admin/bag/covidcertificate/api/Constants.java b/src/main/java/ch/admin/bag/covidcertificate/api/Constants.java
index 4f4c3dcf..3708cbee 100644
--- a/src/main/java/ch/admin/bag/covidcertificate/api/Constants.java
+++ b/src/main/java/ch/admin/bag/covidcertificate/api/Constants.java
@@ -13,13 +13,13 @@
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class Constants {
- public static final String VERSION = "1.2.1";
+ public static final String VERSION = "1.3.0";
public static final String DEFAULT_DISEASE_OR_AGENT_TARGETED = "840539006";
public static final String DEFAULT_DISEASE_OR_AGENT_SYSTEM = "2.16.840.1.113883.6.96";
public static final String ISSUER = "Bundesamt für Gesundheit (BAG)";
public static final String PCR_TYPE_CODE = "LP6464-4";
public static final String NONE_PCR_TYPE_CODE = "LP217198-3";
- public static final int MAX_STRING_LENGTH = 50;
+ public static final int MAX_STRING_LENGTH = 80;
public static final int DAYS_UNTIL_RECOVERY_VALID = 10;
public static final int RECOVERY_CERTIFICATE_VALIDITY_IN_DAYS = 179;
@@ -27,6 +27,7 @@ public class Constants {
public static final String KPI_TYPE_VACCINATION = "v";
public static final String KPI_TYPE_TEST = "t";
public static final String KPI_TYPE_RECOVERY = "r";
+ public static final String KPI_TYPE_INAPP_DELIVERY = "ad";
public static final String USER_EXT_ID_CLAIM_KEY = "userExtId";
public static final String KPI_UUID_KEY = "uuid";
public static final String KPI_TIMESTAMP_KEY = "ts";
@@ -49,8 +50,8 @@ public class Constants {
public static final CreateCertificateError INVALID_DOSES = new CreateCertificateError(455, "Invalid number of doses", HttpStatus.BAD_REQUEST);
public static final CreateCertificateError INVALID_VACCINATION_DATE = new CreateCertificateError(456, "Invalid vaccination date! Date cannot be in the future", HttpStatus.BAD_REQUEST);
public static final CreateCertificateError INVALID_COUNTRY_OF_VACCINATION = new CreateCertificateError(457, "Invalid country of vaccination", HttpStatus.BAD_REQUEST);
- public static final CreateCertificateError INVALID_GIVEN_NAME = new CreateCertificateError(458, "Invalid given name! Must not exceed 50 chars", HttpStatus.BAD_REQUEST);
- public static final CreateCertificateError INVALID_FAMILY_NAME = new CreateCertificateError(459, "Invalid family name! Must not exceed 50 chars", HttpStatus.BAD_REQUEST);
+ public static final CreateCertificateError INVALID_GIVEN_NAME = new CreateCertificateError(458, "Invalid given name! Must not exceed 80 chars", HttpStatus.BAD_REQUEST);
+ public static final CreateCertificateError INVALID_FAMILY_NAME = new CreateCertificateError(459, "Invalid family name! Must not exceed 80 chars", HttpStatus.BAD_REQUEST);
public static final CreateCertificateError NO_TEST_DATA = new CreateCertificateError(460, "No test data was specified", HttpStatus.BAD_REQUEST);
public static final CreateCertificateError INVALID_MEMBER_STATE_OF_TEST = new CreateCertificateError(461, "Invalid member state of test", HttpStatus.BAD_REQUEST);
public static final CreateCertificateError INVALID_TYP_OF_TEST = new CreateCertificateError(462, "Invalid type of test and manufacturer code combination! Must either be a PCR Test type and no manufacturer code or give a manufacturer code and the antigen test type code.", HttpStatus.BAD_REQUEST);
@@ -62,6 +63,10 @@ public class Constants {
public static final CreateCertificateError INVALID_LANGUAGE = new CreateCertificateError(469, "The given language does not match any of the supported languages: de, it, fr, rm!", HttpStatus.BAD_REQUEST);
public static final RevocationError INVALID_UVCI = new RevocationError(470, "Invalid UVCI format.", HttpStatus.BAD_REQUEST);
public static final CreateCertificateError INVALID_ADDRESS = new CreateCertificateError(474, "Paper-based delivery requires a valid address.", HttpStatus.BAD_REQUEST);
+ public static final CreateCertificateError DUPLICATE_DELIVERY_METHOD = new CreateCertificateError(475, "Delivery method can either be InApp or print, but not both.", HttpStatus.BAD_REQUEST);
+ public static final CreateCertificateError INVALID_IN_APP_CODE = new CreateCertificateError(476, "InAppDelivery-Code is invalid.", HttpStatus.NOT_FOUND);
+ public static final CreateCertificateError INVALID_STANDARDISED_GIVEN_NAME = new CreateCertificateError(477, "Invalid given name! The standardised given name exceeds 80 chars", HttpStatus.BAD_REQUEST);
+ public static final CreateCertificateError INVALID_STANDARDISED_FAMILY_NAME = new CreateCertificateError(478, "Invalid family name! The standardised family name exceeds 80 chars", HttpStatus.BAD_REQUEST);
public static final RevocationError DUPLICATE_UVCI = new RevocationError(480, "Duplicate UVCI.", HttpStatus.CONFLICT);
@@ -79,4 +84,5 @@ public class Constants {
public static final CreateCertificateError CREATE_BARCODE_FAILED = new CreateCertificateError(555, "Creating barcode failed.", HttpStatus.INTERNAL_SERVER_ERROR);
public static final CreateCertificateError PRINTING_FAILED = new CreateCertificateError(556, "Printing failed.", HttpStatus.INTERNAL_SERVER_ERROR);
public static final CreateCertificateError WRITING_RETURN_CSV_FAILED = new CreateCertificateError(557, "Write CSV failed", HttpStatus.INTERNAL_SERVER_ERROR);
+ public static final CreateCertificateError INAPP_DELIVERY_FAILED = new CreateCertificateError(558, "InApp delivery failed.", HttpStatus.INTERNAL_SERVER_ERROR);
}
diff --git a/src/main/java/ch/admin/bag/covidcertificate/api/mapper/CovidCertificatePersonMapper.java b/src/main/java/ch/admin/bag/covidcertificate/api/mapper/CovidCertificatePersonMapper.java
index 4bbb16b4..30242fb3 100644
--- a/src/main/java/ch/admin/bag/covidcertificate/api/mapper/CovidCertificatePersonMapper.java
+++ b/src/main/java/ch/admin/bag/covidcertificate/api/mapper/CovidCertificatePersonMapper.java
@@ -1,5 +1,6 @@
package ch.admin.bag.covidcertificate.api.mapper;
+import ch.admin.bag.covidcertificate.api.exception.CreateCertificateException;
import ch.admin.bag.covidcertificate.api.request.CovidCertificatePersonDto;
import ch.admin.bag.covidcertificate.api.request.CovidCertificatePersonNameDto;
import ch.admin.bag.covidcertificate.service.domain.CovidCertificatePerson;
@@ -8,6 +9,8 @@
import lombok.NoArgsConstructor;
import se.digg.dgc.transliteration.MrzEncoder;
+import static ch.admin.bag.covidcertificate.api.Constants.*;
+
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class CovidCertificatePersonMapper {
@@ -19,7 +22,15 @@ public static CovidCertificatePerson toCovidCertificatePerson(CovidCertificatePe
}
private static CovidCertificatePersonName toCovidCertificatePersonName(CovidCertificatePersonNameDto name) {
- return new CovidCertificatePersonName(name.getFamilyName(), standardiseName(name.getFamilyName()), name.getGivenName(), standardiseName(name.getGivenName()));
+ String standardisedFamilyName = standardiseName(name.getFamilyName());
+ if (standardisedFamilyName.length() > MAX_STRING_LENGTH) {
+ throw new CreateCertificateException(INVALID_STANDARDISED_FAMILY_NAME);
+ }
+ String standardisedGivenName = standardiseName(name.getGivenName());
+ if (standardisedGivenName.length() > MAX_STRING_LENGTH) {
+ throw new CreateCertificateException(INVALID_STANDARDISED_GIVEN_NAME);
+ }
+ return new CovidCertificatePersonName(name.getFamilyName(), standardisedFamilyName, name.getGivenName(), standardisedGivenName);
}
private static String standardiseName(String name) {
diff --git a/src/main/java/ch/admin/bag/covidcertificate/api/mapper/TestCertificateQrCodeMapper.java b/src/main/java/ch/admin/bag/covidcertificate/api/mapper/TestCertificateQrCodeMapper.java
index 65c4a0cd..04576653 100644
--- a/src/main/java/ch/admin/bag/covidcertificate/api/mapper/TestCertificateQrCodeMapper.java
+++ b/src/main/java/ch/admin/bag/covidcertificate/api/mapper/TestCertificateQrCodeMapper.java
@@ -15,8 +15,7 @@
import java.util.List;
import java.util.stream.Collectors;
-import static ch.admin.bag.covidcertificate.api.Constants.ISSUER;
-import static ch.admin.bag.covidcertificate.api.Constants.VERSION;
+import static ch.admin.bag.covidcertificate.api.Constants.*;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class TestCertificateQrCodeMapper {
@@ -50,7 +49,7 @@ private static TestCertificateData toTestCertificateData(
return new TestCertificateData(
CovidCertificateDiseaseOrAgentTargeted.getStandardInstance().getCode(),
testValueSet.getTypeCode(),
- testValueSet.getName(),
+ getTrimmedTestName(testValueSet.getName()),
testValueSet.getManufacturerCodeEu(),
testCertificateDataDto.getSampleDateTime().truncatedTo(ChronoUnit.SECONDS),
NegativeTestResult.CODE,
@@ -60,4 +59,8 @@ private static TestCertificateData toTestCertificateData(
UVCI.generateUVCI(testCertificateDataDto.toString())
);
}
+
+ private static String getTrimmedTestName(String testName) {
+ return testName.substring(0, Math.min(testName.length(), MAX_STRING_LENGTH));
+ }
}
diff --git a/src/main/java/ch/admin/bag/covidcertificate/api/request/CertificateCreateDto.java b/src/main/java/ch/admin/bag/covidcertificate/api/request/CertificateCreateDto.java
index 8ad6c816..3d74a31d 100644
--- a/src/main/java/ch/admin/bag/covidcertificate/api/request/CertificateCreateDto.java
+++ b/src/main/java/ch/admin/bag/covidcertificate/api/request/CertificateCreateDto.java
@@ -4,19 +4,34 @@
import ch.admin.bag.covidcertificate.api.valueset.AcceptedLanguages;
import com.fasterxml.jackson.annotation.JsonUnwrapped;
import lombok.*;
+import org.springframework.util.StringUtils;
-import static ch.admin.bag.covidcertificate.api.Constants.INVALID_LANGUAGE;
-import static ch.admin.bag.covidcertificate.api.Constants.NO_PERSON_DATA;
+import static ch.admin.bag.covidcertificate.api.Constants.*;
@Getter
@ToString
@NoArgsConstructor(access = AccessLevel.PROTECTED)
-@AllArgsConstructor
public abstract class CertificateCreateDto {
@JsonUnwrapped
private CovidCertificatePersonDto personData;
private String language;
private CovidCertificateAddressDto address;
+ private String appCode;
+
+ public CertificateCreateDto(CovidCertificatePersonDto personData, String language, CovidCertificateAddressDto address, String appCode) {
+ this.personData = personData;
+ this.language = language;
+ this.address = address;
+ this.appCode = appCode != null ? appCode.toUpperCase() : null;
+ }
+
+ public boolean sendToPrint() {
+ return this.address != null;
+ }
+
+ public boolean sendToApp() {
+ return this.appCode != null;
+ }
public void validate() {
if (personData == null) {
@@ -27,8 +42,23 @@ public void validate() {
if (!AcceptedLanguages.isAcceptedLanguage(language)) {
throw new CreateCertificateException(INVALID_LANGUAGE);
}
- if (address != null) {
- address.validate();
+ this.validateDeliveryMethod();
+ }
+
+ private void validateDeliveryMethod() {
+ if (this.address != null && StringUtils.hasText(this.appCode)) {
+ throw new CreateCertificateException(DUPLICATE_DELIVERY_METHOD);
+ } else {
+ if (this.address != null) {
+ this.address.validate();
+ }
+ if (StringUtils.hasText(this.appCode)) {
+ var isAlphaNumeric = org.apache.commons.lang3.StringUtils.isAlphanumeric(this.appCode);
+ var isNineCharsLong = this.appCode.length() == 9;
+ if (!isAlphaNumeric || !isNineCharsLong) {
+ throw new CreateCertificateException(INVALID_IN_APP_CODE);
+ }
+ }
}
}
}
diff --git a/src/main/java/ch/admin/bag/covidcertificate/api/request/CertificateCsvBean.java b/src/main/java/ch/admin/bag/covidcertificate/api/request/CertificateCsvBean.java
index ed0e1fb2..18fde728 100644
--- a/src/main/java/ch/admin/bag/covidcertificate/api/request/CertificateCsvBean.java
+++ b/src/main/java/ch/admin/bag/covidcertificate/api/request/CertificateCsvBean.java
@@ -38,6 +38,8 @@ public abstract class CertificateCsvBean {
private String cantonCodeSender;
@CsvBindByName(column = "error")
private String error;
+ @CsvBindByName(column = "inAppDeliveryCode")
+ private String inAppDeliveryCode;
public abstract CertificateCreateDto mapToCreateDto();
@@ -50,7 +52,8 @@ protected VaccinationCertificateCreateDto mapToCreateDto(VaccinationCertificateD
mapToPersonDto(),
List.of(dataDto),
getLanguage(),
- mapToAddressDto()
+ mapToAddressDto(),
+ getInAppDeliveryCode()
);
}
@@ -59,7 +62,8 @@ protected TestCertificateCreateDto mapToCreateDto(TestCertificateDataDto dataDto
mapToPersonDto(),
List.of(dataDto),
getLanguage(),
- mapToAddressDto()
+ mapToAddressDto(),
+ getInAppDeliveryCode()
);
}
@@ -68,7 +72,8 @@ protected RecoveryCertificateCreateDto mapToCreateDto(RecoveryCertificateDataDto
mapToPersonDto(),
List.of(dataDto),
getLanguage(),
- mapToAddressDto()
+ mapToAddressDto(),
+ getInAppDeliveryCode()
);
}
diff --git a/src/main/java/ch/admin/bag/covidcertificate/api/request/RecoveryCertificateCreateDto.java b/src/main/java/ch/admin/bag/covidcertificate/api/request/RecoveryCertificateCreateDto.java
index 983152ce..8836b376 100644
--- a/src/main/java/ch/admin/bag/covidcertificate/api/request/RecoveryCertificateCreateDto.java
+++ b/src/main/java/ch/admin/bag/covidcertificate/api/request/RecoveryCertificateCreateDto.java
@@ -21,9 +21,10 @@ public RecoveryCertificateCreateDto(
CovidCertificatePersonDto personData,
List recoveryInfo,
String language,
- CovidCertificateAddressDto address
+ CovidCertificateAddressDto address,
+ String inAppDeliveryCode
) {
- super(personData, language, address);
+ super(personData, language, address, inAppDeliveryCode);
this.recoveryInfo = recoveryInfo;
}
diff --git a/src/main/java/ch/admin/bag/covidcertificate/api/request/TestCertificateCreateDto.java b/src/main/java/ch/admin/bag/covidcertificate/api/request/TestCertificateCreateDto.java
index f3d1cc18..25303943 100644
--- a/src/main/java/ch/admin/bag/covidcertificate/api/request/TestCertificateCreateDto.java
+++ b/src/main/java/ch/admin/bag/covidcertificate/api/request/TestCertificateCreateDto.java
@@ -21,9 +21,10 @@ public TestCertificateCreateDto(
CovidCertificatePersonDto personData,
List testInfo,
String language,
- CovidCertificateAddressDto address
+ CovidCertificateAddressDto address,
+ String inAppDeliveryCode
) {
- super(personData, language, address);
+ super(personData, language, address, inAppDeliveryCode);
this.testInfo = testInfo;
}
diff --git a/src/main/java/ch/admin/bag/covidcertificate/api/request/VaccinationCertificateCreateDto.java b/src/main/java/ch/admin/bag/covidcertificate/api/request/VaccinationCertificateCreateDto.java
index 96c73052..92a389b9 100644
--- a/src/main/java/ch/admin/bag/covidcertificate/api/request/VaccinationCertificateCreateDto.java
+++ b/src/main/java/ch/admin/bag/covidcertificate/api/request/VaccinationCertificateCreateDto.java
@@ -21,9 +21,10 @@ public VaccinationCertificateCreateDto(
CovidCertificatePersonDto personData,
List vaccinationInfo,
String language,
- CovidCertificateAddressDto address
+ CovidCertificateAddressDto address,
+ String inAppDeliveryCode
) {
- super(personData, language, address);
+ super(personData, language, address, inAppDeliveryCode);
this.vaccinationInfo = vaccinationInfo;
}
diff --git a/src/main/java/ch/admin/bag/covidcertificate/api/request/VaccinationCertificateDataDto.java b/src/main/java/ch/admin/bag/covidcertificate/api/request/VaccinationCertificateDataDto.java
index 98326d7c..a0b7b9db 100644
--- a/src/main/java/ch/admin/bag/covidcertificate/api/request/VaccinationCertificateDataDto.java
+++ b/src/main/java/ch/admin/bag/covidcertificate/api/request/VaccinationCertificateDataDto.java
@@ -26,9 +26,7 @@ public class VaccinationCertificateDataDto {
public void validate() {
if (numberOfDoses == null ||
numberOfDoses < 1 ||
- numberOfDoses > 9 ||
totalNumberOfDoses == null ||
- totalNumberOfDoses > 9 ||
numberOfDoses > totalNumberOfDoses) {
throw new CreateCertificateException(INVALID_DOSES);
}
diff --git a/src/main/java/ch/admin/bag/covidcertificate/client/inapp_delivery/InAppDeliveryClient.java b/src/main/java/ch/admin/bag/covidcertificate/client/inapp_delivery/InAppDeliveryClient.java
new file mode 100644
index 00000000..483e2d85
--- /dev/null
+++ b/src/main/java/ch/admin/bag/covidcertificate/client/inapp_delivery/InAppDeliveryClient.java
@@ -0,0 +1,13 @@
+package ch.admin.bag.covidcertificate.client.inapp_delivery;
+
+import ch.admin.bag.covidcertificate.api.exception.CreateCertificateException;
+import ch.admin.bag.covidcertificate.client.inapp_delivery.domain.InAppDeliveryRequestDto;
+
+public interface InAppDeliveryClient {
+ /**
+ * Sends an InApp delivery request to the app backend. If the request fails a CreateCertificateException is thrown.
+ *
+ * @param requestDto - data to be sent to the app.
+ */
+ void deliverToApp(InAppDeliveryRequestDto requestDto) throws CreateCertificateException;
+}
diff --git a/src/main/java/ch/admin/bag/covidcertificate/client/inapp_delivery/domain/InAppDeliveryRequestDto.java b/src/main/java/ch/admin/bag/covidcertificate/client/inapp_delivery/domain/InAppDeliveryRequestDto.java
new file mode 100644
index 00000000..2a50c8dc
--- /dev/null
+++ b/src/main/java/ch/admin/bag/covidcertificate/client/inapp_delivery/domain/InAppDeliveryRequestDto.java
@@ -0,0 +1,23 @@
+package ch.admin.bag.covidcertificate.client.inapp_delivery.domain;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.ToString;
+
+@Getter
+@ToString
+@AllArgsConstructor
+public class InAppDeliveryRequestDto {
+ /**
+ * Code of the App the certificate should be sent to. Must be 9 characters, alphanumeric and upper case.
+ */
+ String code;
+ /**
+ * Payload of the QRCode. Starts with 'HC1:'
+ */
+ String hcert;
+ /**
+ * Base64 encoded String of the PDF.
+ */
+ String pdf;
+}
diff --git a/src/main/java/ch/admin/bag/covidcertificate/client/inapp_delivery/internal/DefaultInAppDeliveryClient.java b/src/main/java/ch/admin/bag/covidcertificate/client/inapp_delivery/internal/DefaultInAppDeliveryClient.java
new file mode 100644
index 00000000..43d32b05
--- /dev/null
+++ b/src/main/java/ch/admin/bag/covidcertificate/client/inapp_delivery/internal/DefaultInAppDeliveryClient.java
@@ -0,0 +1,89 @@
+package ch.admin.bag.covidcertificate.client.inapp_delivery.internal;
+
+import ch.admin.bag.covidcertificate.api.exception.CreateCertificateException;
+import ch.admin.bag.covidcertificate.client.inapp_delivery.InAppDeliveryClient;
+import ch.admin.bag.covidcertificate.client.inapp_delivery.domain.InAppDeliveryRequestDto;
+import ch.admin.bag.covidcertificate.config.ProfileRegistry;
+import ch.admin.bag.covidcertificate.config.security.authentication.ServletJeapAuthorization;
+import ch.admin.bag.covidcertificate.domain.KpiData;
+import ch.admin.bag.covidcertificate.service.KpiDataService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Profile;
+import org.springframework.http.HttpStatus;
+import org.springframework.stereotype.Service;
+import org.springframework.web.reactive.function.client.WebClient;
+import org.springframework.web.reactive.function.client.WebClientRequestException;
+import org.springframework.web.reactive.function.client.WebClientResponseException;
+import org.springframework.web.util.UriComponentsBuilder;
+import reactor.core.publisher.Mono;
+
+import java.time.LocalDateTime;
+
+import static ch.admin.bag.covidcertificate.api.Constants.*;
+import static net.logstash.logback.argument.StructuredArguments.kv;
+
+@Service
+@RequiredArgsConstructor
+@Slf4j
+@Profile("!" + ProfileRegistry.INAPP_DELIVERY_SERVICE_MOCK)
+public class DefaultInAppDeliveryClient implements InAppDeliveryClient {
+
+ @Value("${cc-inapp-delivery-service.url}")
+ private String serviceUri;
+
+ private final WebClient defaultWebClient;
+ private final ServletJeapAuthorization jeapAuthorization;
+ private final KpiDataService kpiLogService;
+
+ @Override
+ public void deliverToApp(InAppDeliveryRequestDto requestDto) {
+ var builder = UriComponentsBuilder.fromHttpUrl(serviceUri);
+
+ var uri = builder.toUriString();
+ log.debug("Call the InApp Delivery Backend with url {}", serviceUri);
+ try {
+ var response = defaultWebClient.post()
+ .uri(uri)
+ .body(Mono.just(requestDto), requestDto.getClass())
+ .retrieve()
+ .toBodilessEntity()
+ .block();
+ log.trace("InApp Delivery Backend Response: {}", response);
+ if (response != null && response.getStatusCode().is2xxSuccessful()) {
+ logKpi();
+ } else {
+ throw new CreateCertificateException(INAPP_DELIVERY_FAILED);
+ }
+ } catch (WebClientResponseException e) {
+ log.error("Received error message", e);
+ this.handleErrorResponse(e);
+ } catch (WebClientRequestException e) {
+ log.error("Request to {} failed", serviceUri, e);
+ throw new CreateCertificateException(INAPP_DELIVERY_FAILED);
+ }
+ }
+
+ private void handleErrorResponse(WebClientResponseException exception) {
+ if (exception != null) {
+ var statusCode = exception.getStatusCode();
+ if (statusCode == HttpStatus.BAD_REQUEST || statusCode == HttpStatus.NOT_FOUND) {
+ throw new CreateCertificateException(INVALID_IN_APP_CODE);
+ } else {
+ throw new CreateCertificateException(INAPP_DELIVERY_FAILED);
+ }
+ } else {
+ throw new CreateCertificateException(INAPP_DELIVERY_FAILED);
+ }
+ }
+
+ private void logKpi() {
+ String extId = jeapAuthorization.getExtIdInAuthentication();
+ if (extId != null) {
+ LocalDateTime kpiTimestamp = LocalDateTime.now();
+ log.info("kpi: {} {} {}", kv(KPI_TIMESTAMP_KEY, kpiTimestamp.format(LOG_FORMAT)), kv(KPI_TYPE_KEY, KPI_TYPE_INAPP_DELIVERY), kv(KPI_UUID_KEY, extId));
+ kpiLogService.log(new KpiData(kpiTimestamp, KPI_TYPE_INAPP_DELIVERY, extId));
+ }
+ }
+}
diff --git a/src/main/java/ch/admin/bag/covidcertificate/client/inapp_delivery/internal/MockInAppDeliveryClient.java b/src/main/java/ch/admin/bag/covidcertificate/client/inapp_delivery/internal/MockInAppDeliveryClient.java
new file mode 100644
index 00000000..cc2cf1bd
--- /dev/null
+++ b/src/main/java/ch/admin/bag/covidcertificate/client/inapp_delivery/internal/MockInAppDeliveryClient.java
@@ -0,0 +1,19 @@
+package ch.admin.bag.covidcertificate.client.inapp_delivery.internal;
+
+import ch.admin.bag.covidcertificate.api.exception.CreateCertificateException;
+import ch.admin.bag.covidcertificate.client.inapp_delivery.InAppDeliveryClient;
+import ch.admin.bag.covidcertificate.client.inapp_delivery.domain.InAppDeliveryRequestDto;
+import ch.admin.bag.covidcertificate.config.ProfileRegistry;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.annotation.Profile;
+import org.springframework.stereotype.Service;
+
+@Service
+@Slf4j
+@Profile(ProfileRegistry.INAPP_DELIVERY_SERVICE_MOCK)
+public class MockInAppDeliveryClient implements InAppDeliveryClient {
+ @Override
+ public void deliverToApp(InAppDeliveryRequestDto requestDto) throws CreateCertificateException {
+ log.info("Call the mock InApp delivery service");
+ }
+}
diff --git a/src/main/java/ch/admin/bag/covidcertificate/config/ProfileRegistry.java b/src/main/java/ch/admin/bag/covidcertificate/config/ProfileRegistry.java
index 40530d5c..34f2a808 100644
--- a/src/main/java/ch/admin/bag/covidcertificate/config/ProfileRegistry.java
+++ b/src/main/java/ch/admin/bag/covidcertificate/config/ProfileRegistry.java
@@ -7,4 +7,5 @@
public class ProfileRegistry {
public static final String SIGNING_SERVICE_MOCK = "mock-signing-service";
public static final String PRINTING_SERVICE_MOCK = "mock-printing-service";
+ public static final String INAPP_DELIVERY_SERVICE_MOCK = "mock-inapp-delivery-service";
}
diff --git a/src/main/java/ch/admin/bag/covidcertificate/config/security/authentication/ServletJeapAuthorization.java b/src/main/java/ch/admin/bag/covidcertificate/config/security/authentication/ServletJeapAuthorization.java
index 762dc0a8..38de0778 100644
--- a/src/main/java/ch/admin/bag/covidcertificate/config/security/authentication/ServletJeapAuthorization.java
+++ b/src/main/java/ch/admin/bag/covidcertificate/config/security/authentication/ServletJeapAuthorization.java
@@ -2,6 +2,8 @@
import org.springframework.security.core.context.SecurityContextHolder;
+import static ch.admin.bag.covidcertificate.api.Constants.USER_EXT_ID_CLAIM_KEY;
+
/**
* This class provides methods to support authorization needs based on the current security context for Spring WebMvc applications.
*/
@@ -16,4 +18,18 @@ public JeapAuthenticationToken getJeapAuthenticationToken() {
return (JeapAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
}
+ /**
+ * Fetch the JeapAuthenticationToken from the current security context.
+ *
+ * @return The JeapAuthenticationToken extracted from the current security context.
+ */
+ public String getExtIdInAuthentication() {
+ var authentication = (JeapAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
+ var jwt = authentication.getToken();
+ if (jwt != null) {
+ return jwt.getClaimAsString(USER_EXT_ID_CLAIM_KEY);
+ }
+ return null;
+ }
+
}
diff --git a/src/main/java/ch/admin/bag/covidcertificate/service/COSETime.java b/src/main/java/ch/admin/bag/covidcertificate/service/COSETime.java
index df4ab66f..f8a9656b 100644
--- a/src/main/java/ch/admin/bag/covidcertificate/service/COSETime.java
+++ b/src/main/java/ch/admin/bag/covidcertificate/service/COSETime.java
@@ -10,7 +10,7 @@
@Component
@RequiredArgsConstructor
public class COSETime {
- private static final Integer EXPIRATION_PERIOD = 12;
+ private static final Integer EXPIRATION_PERIOD = 24;
private final Clock clock;
diff --git a/src/main/java/ch/admin/bag/covidcertificate/service/CovidCertificateGenerationService.java b/src/main/java/ch/admin/bag/covidcertificate/service/CovidCertificateGenerationService.java
index 0951ef46..93fce408 100644
--- a/src/main/java/ch/admin/bag/covidcertificate/service/CovidCertificateGenerationService.java
+++ b/src/main/java/ch/admin/bag/covidcertificate/service/CovidCertificateGenerationService.java
@@ -6,6 +6,8 @@
import ch.admin.bag.covidcertificate.api.request.TestCertificateCreateDto;
import ch.admin.bag.covidcertificate.api.request.VaccinationCertificateCreateDto;
import ch.admin.bag.covidcertificate.api.response.CovidCertificateCreateResponseDto;
+import ch.admin.bag.covidcertificate.client.inapp_delivery.InAppDeliveryClient;
+import ch.admin.bag.covidcertificate.client.inapp_delivery.domain.InAppDeliveryRequestDto;
import ch.admin.bag.covidcertificate.client.printing.PrintQueueClient;
import ch.admin.bag.covidcertificate.service.document.CovidPdfCertificateGenerationService;
import ch.admin.bag.covidcertificate.service.domain.*;
@@ -16,12 +18,15 @@
import org.springframework.stereotype.Service;
import se.digg.dgc.encoding.Barcode;
+import java.util.Base64;
+
@Service
@Slf4j
@RequiredArgsConstructor
public class CovidCertificateGenerationService {
private final BarcodeService barcodeService;
private final PrintQueueClient printQueueClient;
+ private final InAppDeliveryClient inAppDeliveryClient;
private final ObjectMapper objectMapper;
private final CovidPdfCertificateGenerationService covidPdfCertificateGenerationService;
private final CovidCertificateDtoMapperService covidCertificateDtoMapperService;
@@ -52,8 +57,11 @@ private CovidCertificateCreateResponseDto generateCovidCertificate(AbstractCerti
byte[] pdf = covidPdfCertificateGenerationService.generateCovidCertificate(pdfData, code);
CovidCertificateCreateResponseDto responseDto = new CovidCertificateCreateResponseDto(pdf, code.getImage(), uvci);
- if (createDto.getAddress() != null) {
+ if (createDto.sendToPrint()) {
printQueueClient.sendPrintJob(CertificatePrintRequestDtoMapper.toCertificatePrintRequestDto(pdf, uvci, createDto));
+ } else if (createDto.sendToApp()) {
+ var inAppDeliveryDto = new InAppDeliveryRequestDto(createDto.getAppCode(), code.getPayload(), Base64.getEncoder().encodeToString(pdf));
+ this.inAppDeliveryClient.deliverToApp(inAppDeliveryDto);
}
return responseDto;
}
diff --git a/src/main/java/ch/admin/bag/covidcertificate/service/document/CovidPdfCertificateGenerationService.java b/src/main/java/ch/admin/bag/covidcertificate/service/document/CovidPdfCertificateGenerationService.java
index 46a4465f..03abe5b3 100644
--- a/src/main/java/ch/admin/bag/covidcertificate/service/document/CovidPdfCertificateGenerationService.java
+++ b/src/main/java/ch/admin/bag/covidcertificate/service/document/CovidPdfCertificateGenerationService.java
@@ -77,16 +77,16 @@ public class CovidPdfCertificateGenerationService {
public CovidPdfCertificateGenerationService(ConfigurableEnvironment env) throws URISyntaxException, IOException, DocumentException {
final BaseFont baseFont = BaseFont.createFont("arial.ttf", BaseFont.IDENTITY_H, BaseFont.EMBEDDED, true,
- Files.readAllBytes(Path.of(Objects.requireNonNull(this.getClass().getClassLoader().getResource("templates/fonts/arial.ttf")).toURI())),
- null);
+ Files.readAllBytes(Path.of(Objects.requireNonNull(this.getClass().getClassLoader().getResource("templates/fonts/arial.ttf")).toURI())),
+ null);
final BaseFont baseFontBold = BaseFont.createFont("arialbd.ttf", BaseFont.IDENTITY_H, BaseFont.EMBEDDED, true,
- Files.readAllBytes(Path.of(Objects.requireNonNull(this.getClass().getClassLoader().getResource("templates/fonts/arialbd.ttf")).toURI())),
- null);
+ Files.readAllBytes(Path.of(Objects.requireNonNull(this.getClass().getClassLoader().getResource("templates/fonts/arialbd.ttf")).toURI())),
+ null);
final BaseFont baseFontItalic = BaseFont.createFont("ariali.ttf", BaseFont.IDENTITY_H, BaseFont.EMBEDDED, true,
- Files.readAllBytes(Path.of(Objects.requireNonNull(this.getClass().getClassLoader().getResource("templates/fonts/ariali.ttf")).toURI())),
- null);
+ Files.readAllBytes(Path.of(Objects.requireNonNull(this.getClass().getClassLoader().getResource("templates/fonts/ariali.ttf")).toURI())),
+ null);
fontRow = new Font(baseFont, 10, Font.NORMAL, BaseColor.BLACK);
@@ -109,7 +109,6 @@ public CovidPdfCertificateGenerationService(ConfigurableEnvironment env) throws
logoApp = getLogo("appicon.png", 100);
addDraftWatermark = Arrays.stream(env.getActiveProfiles()).noneMatch("prod"::equals);
-
}
private ResourceBundleMessageSource messageSource() {
@@ -129,6 +128,8 @@ public byte[] generateCovidCertificate(AbstractCertificatePdf data, Barcode barc
final Locale locale = getLocale(data.getLanguage());
+ boolean isPartialVaccination = data instanceof VaccinationCertificatePdf && ((VaccinationCertificatePdf) data).isPartialVaccination();
+
PdfWriter writer = PdfWriter.getInstance(document, stream);
document.open();
@@ -137,13 +138,13 @@ public byte[] generateCovidCertificate(AbstractCertificatePdf data, Barcode barc
document.setMargins(MARGIN_LEFT, MARGIN_RIGHT, MARGIN_TOP, MARGIN_BOTTOM);
- document.add(headerTable(locale));
+ document.add(headerTable(locale, isPartialVaccination));
Image qrCode = renderQRCode(writer, barcode.getPayload());
- document.add(mainTable(locale, data, qrCode));
+ document.add(mainTable(locale, data, qrCode, isPartialVaccination));
- document.add(issuerTable(locale));
+ document.add(issuerTable(locale, isPartialVaccination));
document.add(infoTable(locale));
@@ -182,7 +183,7 @@ protected void addMetadata(Document document) {
document.addCreator(METADATA);
}
- private PdfPTable headerTable(Locale locale) {
+ private PdfPTable headerTable(Locale locale, boolean isPartialVaccination) {
PdfPTable table = new PdfPTable(2);
table.setWidthPercentage(100);
table.setPaddingTop(0);
@@ -197,27 +198,31 @@ private PdfPTable headerTable(Locale locale) {
cell.setVerticalAlignment(Rectangle.TOP);
table.addCell(cell);
- PdfPCell valueCell = new PdfPCell(new Phrase(messageSource.getMessage("document.title", null, locale), fontHeaderRed));
+ String headerKey = isPartialVaccination ? "evidence.document.title" : "document.title";
+
+ PdfPCell valueCell = new PdfPCell(new Phrase(messageSource.getMessage(headerKey, null, locale), fontHeaderRed));
valueCell.setBorder(Rectangle.NO_BORDER);
valueCell.setVerticalAlignment(Rectangle.TOP);
valueCell.setPaddingTop(0);
+ valueCell.setPaddingLeft(0);
table.addCell(valueCell);
- PdfPCell valueCell2 = new PdfPCell(new Phrase(messageSource.getMessage("document.title", null, Locale.ENGLISH), fontHeaderBlack));
+ PdfPCell valueCell2 = new PdfPCell(new Phrase(messageSource.getMessage(headerKey, null, Locale.ENGLISH), fontHeaderBlack));
valueCell2.setBorder(Rectangle.NO_BORDER);
valueCell2.setVerticalAlignment(Rectangle.TOP);
valueCell2.setPaddingTop(0);
+ valueCell2.setPaddingLeft(0);
table.addCell(valueCell2);
return table;
}
- private PdfPTable mainTable(Locale locale, AbstractCertificatePdf data, Image qrCode) {
+ private PdfPTable mainTable(Locale locale, AbstractCertificatePdf data, Image qrCode, boolean isPartialVaccination) {
float[] pointColumnWidths = {50F, 20F, 30F};
PdfPTable table = new PdfPTable(pointColumnWidths);
table.setWidthPercentage(100);
- PdfPCell cell = new PdfPCell(addLeftColumn(locale, qrCode, data));
+ PdfPCell cell = new PdfPCell(addLeftColumn(locale, qrCode, data, isPartialVaccination));
cell.setVerticalAlignment(Element.ALIGN_TOP);
cell.setBorder(Rectangle.NO_BORDER);
cell.setRowspan(30);
@@ -225,7 +230,7 @@ private PdfPTable mainTable(Locale locale, AbstractCertificatePdf data, Image qr
table.addCell(cell);
if (data instanceof VaccinationCertificatePdf) {
- addVaccineData(locale, (VaccinationCertificatePdf) data, table);
+ addVaccineData(locale, (VaccinationCertificatePdf) data, table, isPartialVaccination);
} else if (data instanceof RecoveryCertificatePdf) {
addRecoveryData(locale, (RecoveryCertificatePdf) data, table);
} else {
@@ -234,8 +239,10 @@ private PdfPTable mainTable(Locale locale, AbstractCertificatePdf data, Image qr
return table;
}
- private void addVaccineData(Locale locale, VaccinationCertificatePdf data, PdfPTable table) {
- addIssuerRow(table, locale, "vaccination.title", 0, true, 0);
+ private void addVaccineData(Locale locale, VaccinationCertificatePdf data, PdfPTable table, boolean isPartialVaccination) {
+ String titleKey = isPartialVaccination ? "evidence.title" : "vaccination.title";
+
+ addIssuerRow(table, locale, titleKey, 0, true, 0);
addRow(table, locale, VACCINATION_DISEASE_LABEL_KEY, messageSource.getMessage(VACCINATION_DISEASE_MESSAGE_CODE, null, locale));
addRow(table, locale, "vaccination.dosis.label", data.getNumberOfDoses() + "/" + data.getTotalNumberOfDoses());
addRow(table, locale, "vaccination.type.label", data.getVaccineProphylaxis());
@@ -268,7 +275,7 @@ private void addTestData(Locale locale, TestCertificatePdf data, PdfPTable table
addLocaleAndEnglishRow(table, locale, "test.country.label", data.getMemberStateOfTest(), data.getMemberStateOfTestEn());
}
- private PdfPTable addLeftColumn(Locale locale, Image qrCode, AbstractCertificatePdf data) {
+ private PdfPTable addLeftColumn(Locale locale, Image qrCode, AbstractCertificatePdf data, boolean isPartialVaccination) {
float[] pointColumnWidths = {50F, 50F};
PdfPTable table = new PdfPTable(pointColumnWidths);
@@ -289,7 +296,7 @@ private PdfPTable addLeftColumn(Locale locale, Image qrCode, AbstractCertificate
cell2.setPaddingTop(5);
table.addCell(cell2);
- addQrLabelCell(table, locale, LocalDateTime.now());
+ addQrLabelCell(table, locale, LocalDateTime.now(), isPartialVaccination);
addIssuerRow(table, locale, "personalData.title", 20, true, PADDING_LEFT);
addNameRow(table, locale, "personalData.name.label", data.getFamilyName() + " " + data.getGivenName(), PADDING_LEFT);
@@ -377,8 +384,9 @@ private void addLocaleAndEnglishRow(PdfPTable table, Locale locale, String key,
table.addCell(valueEnglishCell);
}
- private void addIssuerRow(PdfPTable table, Locale locale) {
- addIssuerRow(table, locale, "issuer.title", 15, false, (float) CovidPdfCertificateGenerationService.PADDING_LEFT);
+ private void addIssuerRow(PdfPTable table, Locale locale, boolean isPartialVaccination) {
+ String issuerLabel = isPartialVaccination ? "evidence.issuer" : "issuer.title";
+ addIssuerRow(table, locale, issuerLabel, 15, false, (float) CovidPdfCertificateGenerationService.PADDING_LEFT);
}
private void addIssuerRow(PdfPTable table, Locale locale, String key, int padding, boolean title, float paddingLeft) {
@@ -398,16 +406,17 @@ private void addIssuerRow(PdfPTable table, Locale locale, String key, int paddin
table.addCell(issuerCell);
}
- private void addQrLabelCell(PdfPTable table, Locale locale, LocalDateTime dateTime) {
+ private void addQrLabelCell(PdfPTable table, Locale locale, LocalDateTime dateTime, boolean isPartialVaccination) {
+ String labelKey = isPartialVaccination ? "evidence.qrCode.label" : "qrCode.label";
String date = dateTime.format(LOCAL_DATE_FORMAT);
String time = dateTime.format(DateTimeFormatter.ofPattern("HH:mm"));
- PdfPCell first = new PdfPCell(new Phrase(messageSource.getMessage("qrCode.label", new String[]{date, time}, locale), font8Row));
+ PdfPCell first = new PdfPCell(new Phrase(messageSource.getMessage(labelKey, new String[]{date, time}, locale), font8Row));
first.setBorder(Rectangle.NO_BORDER);
first.setColspan(2);
first.setPaddingLeft(PADDING_LEFT);
table.addCell(first);
- PdfPCell second = new PdfPCell(new Phrase(messageSource.getMessage("qrCode.label", new String[]{date, time}, Locale.ENGLISH), font8English));
+ PdfPCell second = new PdfPCell(new Phrase(messageSource.getMessage(labelKey, new String[]{date, time}, Locale.ENGLISH), font8English));
second.setBorder(Rectangle.NO_BORDER);
second.setPaddingTop(0);
second.setColspan(2);
@@ -437,11 +446,11 @@ private Chunk getLogo(String name, int scale) {
return new Chunk(logo, 0, 0);
}
- private PdfPTable issuerTable(Locale locale) {
+ private PdfPTable issuerTable(Locale locale, boolean isPartialVaccination) {
PdfPTable table = new PdfPTable(1);
table.setWidthPercentage(100);
- addIssuerRow(table, locale);
+ addIssuerRow(table, locale, isPartialVaccination);
addIssuerRow(table, locale, "issuer.issuer", 5, true, PADDING_LEFT);
return table;
diff --git a/src/main/java/ch/admin/bag/covidcertificate/service/domain/VaccinationCertificatePdf.java b/src/main/java/ch/admin/bag/covidcertificate/service/domain/VaccinationCertificatePdf.java
index 1def0a3e..b4bde537 100644
--- a/src/main/java/ch/admin/bag/covidcertificate/service/domain/VaccinationCertificatePdf.java
+++ b/src/main/java/ch/admin/bag/covidcertificate/service/domain/VaccinationCertificatePdf.java
@@ -54,4 +54,8 @@ public VaccinationCertificatePdf(
this.issuer = issuer;
}
+ public boolean isPartialVaccination() {
+ return this.numberOfDoses < this.totalNumberOfDoses;
+ }
+
}
diff --git a/src/main/java/ch/admin/bag/covidcertificate/web/controller/ResponseStatusExceptionHandler.java b/src/main/java/ch/admin/bag/covidcertificate/web/controller/ResponseStatusExceptionHandler.java
index 733df6f4..c912d2d7 100644
--- a/src/main/java/ch/admin/bag/covidcertificate/web/controller/ResponseStatusExceptionHandler.java
+++ b/src/main/java/ch/admin/bag/covidcertificate/web/controller/ResponseStatusExceptionHandler.java
@@ -18,7 +18,12 @@ public class ResponseStatusExceptionHandler {
protected ResponseEntity