package io.konik.validation;
import com.google.common.base.Predicate;
import com.google.common.collect.Collections2;
import com.google.common.collect.Iterables;
import io.konik.validator.annotation.Basic;
import io.konik.zugferd.Invoice;
import io.konik.zugferd.entity.GrossPrice;
import io.konik.zugferd.entity.PaymentMeans;
import io.konik.zugferd.entity.trade.MonetarySummation;
import io.konik.zugferd.entity.trade.Settlement;
import io.konik.zugferd.entity.trade.Trade;
import io.konik.zugferd.entity.trade.item.Item;
import io.konik.zugferd.entity.trade.item.SpecifiedMonetarySummation;
import io.konik.zugferd.entity.trade.item.SpecifiedSettlement;
import io.konik.zugferd.profile.ConformanceLevel;
import io.konik.zugferd.unqualified.Amount;
import org.apache.bval.jsr.util.PathImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
import javax.validation.*;
import javax.validation.metadata.ConstraintDescriptor;
import java.lang.annotation.Annotation;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.*;
/**
* Validates {@link Invoice}'s {@link MonetarySummation} by comparing values after recalculating MonetarySummation
* for the invoice and all line position items.
*/
public class MonetarySummationValidator {
private static Logger log = LoggerFactory.getLogger(MonetarySummationValidator.class);
private final MessageInterpolator messageInterpolator;
/**
* @param messageInterpolator
*/
public MonetarySummationValidator(MessageInterpolator messageInterpolator) {
this.messageInterpolator = messageInterpolator;
}
/**
* Checks if given method belongs to the validation groups profile.
*
* @param clazz
* @param methodName
* @param validationGroups
* @return true if the method belongs to this validation group
*
*/
public static boolean belongsToProfile(final Class<?> clazz, final String methodName, final List<Class<?>> validationGroups) {
try {
Annotation[] annotations = clazz.getMethod(methodName).getAnnotations();
List<Annotation> profileAnnotationsOnly = new LinkedList<Annotation>(Collections2.filter(Arrays.asList(annotations), new Predicate<Annotation>() {
@Override
public boolean apply(Annotation annotation) {
return ConformanceLevel.getAnnotations().contains(annotation.annotationType());
}
}));
if (profileAnnotationsOnly.isEmpty()) {
return true;
}
if (profileAnnotationsOnly.size() == 1 && profileAnnotationsOnly.get(0).annotationType().equals(Basic.class)) {
return true;
}
return Iterables.any(profileAnnotationsOnly, new Predicate<Annotation>() {
@Override
public boolean apply(@Nullable Annotation annotation) {
return validationGroups.contains(annotation.annotationType());
}
});
} catch (Exception e) {
log.warn("{} caught while checking if method {} from class {} belongs to validation groups: {}", e.getClass().getSimpleName(), methodName, clazz, e.getMessage());
}
return false;
}
public Set<ConstraintViolation<Invoice>> validate(final Invoice invoice, final Class<?>[] validationGroups) {
if (invoice == null) {
throw new IllegalArgumentException("Invoice cannot be null");
}
Set<ConstraintViolation<Invoice>> violations = new HashSet<ConstraintViolation<Invoice>>();
Trade trade = invoice.getTrade();
if (trade != null) {
Settlement settlement = trade.getSettlement();
List<Class<?>> validationGroupsList = Arrays.asList(validationGroups);
if (settlement.getMonetarySummation() != null) {
log.debug("Validating invoice monetary summation...");
MonetarySummation monetarySummation = settlement.getMonetarySummation();
MonetarySummation calculatedMonetarySummation = AmountCalculator.recalculate(invoice).getMonetarySummation();
Class<?> clazz = MonetarySummation.class;
if (belongsToProfile(clazz, "getGrandTotal", validationGroupsList) &&
!areEqual(monetarySummation.getGrandTotal(), calculatedMonetarySummation.getGrandTotal())) {
String message = message(monetarySummation.getGrandTotal(), calculatedMonetarySummation.getGrandTotal());
violations.add(new Violation(invoice, message, "monetarySummation.grandTotal.error", "trade.settlement.monetarySummation.grandTotal", monetarySummation.getGrandTotal() != null ? monetarySummation.getGrandTotal().getValue() : null));
}
if (belongsToProfile(clazz, "getTaxBasisTotal", validationGroupsList) &&
!areEqual(monetarySummation.getTaxBasisTotal(), calculatedMonetarySummation.getTaxBasisTotal())) {
String message = message(monetarySummation.getTaxBasisTotal(), calculatedMonetarySummation.getTaxBasisTotal());
violations.add(new Violation(invoice, message, "monetarySummation.taxBasisTotal.error", "trade.settlement.monetarySummation.taxBasisTotal", monetarySummation.getTaxBasisTotal() != null ? monetarySummation.getTaxBasisTotal().getValue() : null));
}
if (belongsToProfile(clazz, "getChargeTotal", validationGroupsList) &&
!areEqual(monetarySummation.getChargeTotal(), calculatedMonetarySummation.getChargeTotal())) {
String message = message(monetarySummation.getChargeTotal(), calculatedMonetarySummation.getChargeTotal());
violations.add(new Violation(invoice, message, "monetarySummation.chargeTotal.error", "trade.settlement.monetarySummation.chargeTotal", monetarySummation.getChargeTotal() != null ? monetarySummation.getChargeTotal().getValue() : null));
}
if (belongsToProfile(clazz, "getAllowanceTotal", validationGroupsList) &&
!areEqual(monetarySummation.getAllowanceTotal(), calculatedMonetarySummation.getAllowanceTotal())) {
String message = message(monetarySummation.getAllowanceTotal(), calculatedMonetarySummation.getAllowanceTotal());
violations.add(new Violation(invoice, message, "monetarySummation.allowanceTotal.error", "trade.settlement.monetarySummation.allowanceTotal", monetarySummation.getAllowanceTotal() != null ? monetarySummation.getAllowanceTotal().getValue() : null));
}
boolean expectDuePayable = monetarySummation.getTotalPrepaid() != null && !isEqualZero(monetarySummation.getTotalPrepaid());
if (settlement.getPaymentMeans() != null) {
for (PaymentMeans paymentMeans : settlement.getPaymentMeans()) {
expectDuePayable = expectDuePayable || paymentMeans.getCode() != null;
}
}
if (belongsToProfile(clazz, "getDuePayable", validationGroupsList) &&
expectDuePayable &&
!areEqual(monetarySummation.getDuePayable(), calculatedMonetarySummation.getDuePayable())) {
String message = message(monetarySummation.getDuePayable(), calculatedMonetarySummation.getDuePayable());
violations.add(new Violation(invoice, message, "monetarySummation.duePayable.error", "trade.settlement.monetarySummation.duePayable", monetarySummation.getDuePayable() != null ? monetarySummation.getDuePayable().getValue() : null));
}
if (belongsToProfile(clazz, "getLineTotal", validationGroupsList) &&
!areEqual(monetarySummation.getLineTotal(), calculatedMonetarySummation.getLineTotal())) {
String message = message(monetarySummation.getLineTotal(), calculatedMonetarySummation.getLineTotal());
violations.add(new Violation(invoice, message, "monetarySummation.lineTotal.error", "trade.settlement.monetarySummation.lineTotal", monetarySummation.getLineTotal() != null ? monetarySummation.getLineTotal().getValue() : null));
}
if (belongsToProfile(clazz, "getTaxTotal", validationGroupsList) &&
!areEqual(monetarySummation.getTaxTotal(), calculatedMonetarySummation.getTaxTotal())) {
String message = message(monetarySummation.getTaxTotal(), calculatedMonetarySummation.getTaxTotal());
violations.add(new Violation(invoice, message, "monetarySummation.taxTotal.error", "trade.settlement.monetarySummation.taxTotal", monetarySummation.getTaxTotal() != null ? monetarySummation.getTaxTotal().getValue() : null));
}
if (belongsToProfile(clazz, "getTotalPrepaid", validationGroupsList) &&
!areEqual(monetarySummation.getTotalPrepaid(), calculatedMonetarySummation.getTotalPrepaid())) {
String message = message(monetarySummation.getTotalPrepaid(), calculatedMonetarySummation.getTotalPrepaid());
violations.add(new Violation(invoice, message, "monetarySummation.totalPrepaid.error", "trade.settlement.monetarySummation.totalPrepaid", monetarySummation.getTotalPrepaid() != null ? monetarySummation.getTotalPrepaid().getValue() : null));
}
}
log.debug("Validating item's specified monetary summations...");
if (trade.getItems() != null) {
for (int i = 0; i < trade.getItems().size(); i++) {
Item item = trade.getItems().get(i);
if (item.getSettlement() != null) {
SpecifiedSettlement specifiedSettlement = item.getSettlement();
if (specifiedSettlement.getMonetarySummation() != null) {
SpecifiedMonetarySummation monetarySummation = specifiedSettlement.getMonetarySummation();
SpecifiedMonetarySummation calculatedMonetarySummation = AmountCalculator.calculateSpecifiedMonetarySummation(item);
if (belongsToProfile(SpecifiedMonetarySummation.class, "getLineTotal", validationGroupsList) &&
!areEqual(monetarySummation.getLineTotal(), calculatedMonetarySummation.getLineTotal())) {
String message = message(monetarySummation.getLineTotal(), calculatedMonetarySummation.getLineTotal());
violations.add(new Violation(invoice, message, "item.monetarySummation.lineTotal.error", "trade.items["+i+"].settlement.monetarySummation.lineTotal", monetarySummation.getLineTotal() != null ? monetarySummation.getLineTotal().getValue() : null));
}
if (belongsToProfile(SpecifiedMonetarySummation.class, "getTotalAllowanceCharge", validationGroupsList) &&
((grossPriceIncludesCharges(item) && monetarySummation.getTotalAllowanceCharge() == null) || !areEqual(monetarySummation.getTotalAllowanceCharge(), calculatedMonetarySummation.getTotalAllowanceCharge()))) {
String message = message(monetarySummation.getTotalAllowanceCharge(), calculatedMonetarySummation.getTotalAllowanceCharge());
violations.add(new Violation(invoice, message, "item.monetarySummation.totalAllowanceCharge.error", "trade.items["+i+"].settlement.monetarySummation.totalAllowanceCharge", monetarySummation.getTotalAllowanceCharge() != null ? monetarySummation.getTotalAllowanceCharge().getValue() : null));
}
}
}
}
}
}
return violations;
}
private static boolean grossPriceIncludesCharges(final Item item) {
boolean result = false;
if (item != null && item.getAgreement() != null && item.getAgreement().getGrossPrice() != null) {
GrossPrice grossPrice = item.getAgreement().getGrossPrice();
if (grossPrice.getAllowanceCharges() != null) {
return !grossPrice.getAllowanceCharges().isEmpty();
}
}
return result;
}
private static boolean isEqualZero(final Amount amount) {
if (amount == null || amount.getValue() == null) {
return false;
}
return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP).equals(amount.getValue().setScale(2, RoundingMode.HALF_UP));
}
private String message(final Amount current, final Amount expected) {
Object currentValue = current != null ? current.getValue() : "null";
Object expectedValue = expected != null ? expected.getValue() : "null";
return messageInterpolator.interpolate("{io.konik.validation.amount.calculation.error}", new Violation.Context(currentValue, expectedValue));
}
private static boolean areEqual(final Amount first, final Amount second) {
if (first == null && second == null) {
return true;
}
if (zeroEqualsNull(first, second) || zeroEqualsNull(second, first)) {
return true;
}
if (first == null || second == null) {
return false;
}
if (first.getCurrency() != null && second.getCurrency() != null) {
if (!first.getCurrency().getCurrency().equals(second.getCurrency().getCurrency())) {
return false;
}
}
if (first.getValue() != null && second.getValue() != null) {
return first.getValue()
.setScale(2, RoundingMode.HALF_UP)
.equals(second.getValue()
.setScale(2, RoundingMode.HALF_UP)
);
}
return false;
}
private static boolean zeroEqualsNull(final Amount first, final Amount second) {
return first == null &&
second != null &&
second.getValue() != null &&
second.getValue().setScale(2, RoundingMode.HALF_UP).equals(BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP));
}
private static class Violation implements ConstraintViolation<Invoice> {
private final Invoice invoice;
private final String message;
private final String messageTemplate;
private final String propertyPath;
private final Object invalidValue;
public Violation(Invoice invoice, String message, String messageTemplate, String propertyPath, Object invalidValue) {
this.invoice = invoice;
this.message = message;
this.messageTemplate = messageTemplate;
this.propertyPath = propertyPath;
this.invalidValue = invalidValue;
}
@Override
public String getMessage() {
return message;
}
@Override
public String getMessageTemplate() {
return messageTemplate;
}
@Override
public Invoice getRootBean() {
return invoice;
}
@Override
public Class<Invoice> getRootBeanClass() {
return Invoice.class;
}
@Override
public Object getLeafBean() {
return null;
}
@Override
public Object[] getExecutableParameters() {
return new Object[0];
}
@Override
public Object getExecutableReturnValue() {
return null;
}
@Override
public Path getPropertyPath() {
return PathImpl.createPathFromString(propertyPath);
}
@Override
public Object getInvalidValue() {
return invalidValue;
}
@Override
public ConstraintDescriptor<?> getConstraintDescriptor() {
return null;
}
@Override
public <U> U unwrap(Class<U> type) {
return null;
}
static class Context implements MessageInterpolator.Context {
private final Object currentValue;
private final Object expectedValue;
public Context(Object currentValue, Object expectedValue) {
this.currentValue = currentValue;
this.expectedValue = expectedValue;
}
@Override
public ConstraintDescriptor<?> getConstraintDescriptor() {
return new ConstraintDescriptor<Annotation>() {
@Override
public Annotation getAnnotation() {
return null;
}
@Override
public String getMessageTemplate() {
return "{io.konik.validation.amount.calculation.error}";
}
@Override
public Set<Class<?>> getGroups() {
return null;
}
@Override
public Set<Class<? extends Payload>> getPayload() {
return null;
}
@Override
public ConstraintTarget getValidationAppliesTo() {
return null;
}
@Override
public List<Class<? extends ConstraintValidator<Annotation, ?>>> getConstraintValidatorClasses() {
return new LinkedList<Class<? extends ConstraintValidator<Annotation, ?>>>();
}
@Override
public Map<String, Object> getAttributes() {
Map<String,Object> map = new HashMap<String, Object>();
map.put("currentValue", currentValue);
map.put("expectedValue", expectedValue);
return map;
}
@Override
public Set<ConstraintDescriptor<?>> getComposingConstraints() {
return new HashSet<ConstraintDescriptor<?>>();
}
@Override
public boolean isReportAsSingleViolation() {
return false;
}
};
}
@Override
public Object getValidatedValue() {
return currentValue;
}
@Override
public <T> T unwrap(Class<T> type) {
return null;
}
}
}
}