001package io.konik.validation; 002 003import com.google.common.base.Predicate; 004import com.google.common.collect.Collections2; 005import com.google.common.collect.Iterables; 006import io.konik.validator.annotation.Basic; 007import io.konik.zugferd.Invoice; 008import io.konik.zugferd.entity.GrossPrice; 009import io.konik.zugferd.entity.PaymentMeans; 010import io.konik.zugferd.entity.trade.MonetarySummation; 011import io.konik.zugferd.entity.trade.Settlement; 012import io.konik.zugferd.entity.trade.Trade; 013import io.konik.zugferd.entity.trade.item.Item; 014import io.konik.zugferd.entity.trade.item.SpecifiedMonetarySummation; 015import io.konik.zugferd.entity.trade.item.SpecifiedSettlement; 016import io.konik.zugferd.profile.ConformanceLevel; 017import io.konik.zugferd.unqualified.Amount; 018import org.apache.bval.jsr.util.PathImpl; 019import org.slf4j.Logger; 020import org.slf4j.LoggerFactory; 021 022import javax.annotation.Nullable; 023import javax.validation.*; 024import javax.validation.metadata.ConstraintDescriptor; 025import java.lang.annotation.Annotation; 026import java.math.BigDecimal; 027import java.math.RoundingMode; 028import java.util.*; 029 030/** 031 * Validates {@link Invoice}'s {@link MonetarySummation} by comparing values after recalculating MonetarySummation 032 * for the invoice and all line position items. 033 */ 034public class MonetarySummationValidator { 035 036 private static Logger log = LoggerFactory.getLogger(MonetarySummationValidator.class); 037 038 private final MessageInterpolator messageInterpolator; 039 040 /** 041 * @param messageInterpolator 042 */ 043 public MonetarySummationValidator(MessageInterpolator messageInterpolator) { 044 this.messageInterpolator = messageInterpolator; 045 } 046 047 /** 048 * Checks if given method belongs to the validation groups profile. 049 * 050 * @param clazz 051 * @param methodName 052 * @param validationGroups 053 * @return true if the method belongs to this validation group 054 * 055 */ 056 public static boolean belongsToProfile(final Class<?> clazz, final String methodName, final List<Class<?>> validationGroups) { 057 try { 058 Annotation[] annotations = clazz.getMethod(methodName).getAnnotations(); 059 List<Annotation> profileAnnotationsOnly = new LinkedList<Annotation>(Collections2.filter(Arrays.asList(annotations), new Predicate<Annotation>() { 060 @Override 061 public boolean apply(Annotation annotation) { 062 return ConformanceLevel.getAnnotations().contains(annotation.annotationType()); 063 } 064 })); 065 066 if (profileAnnotationsOnly.isEmpty()) { 067 return true; 068 } 069 070 if (profileAnnotationsOnly.size() == 1 && profileAnnotationsOnly.get(0).annotationType().equals(Basic.class)) { 071 return true; 072 } 073 074 return Iterables.any(profileAnnotationsOnly, new Predicate<Annotation>() { 075 @Override 076 public boolean apply(@Nullable Annotation annotation) { 077 return validationGroups.contains(annotation.annotationType()); 078 } 079 }); 080 081 } catch (Exception e) { 082 log.warn("{} caught while checking if method {} from class {} belongs to validation groups: {}", e.getClass().getSimpleName(), methodName, clazz, e.getMessage()); 083 } 084 085 return false; 086 } 087 088 public Set<ConstraintViolation<Invoice>> validate(final Invoice invoice, final Class<?>[] validationGroups) { 089 if (invoice == null) { 090 throw new IllegalArgumentException("Invoice cannot be null"); 091 } 092 093 Set<ConstraintViolation<Invoice>> violations = new HashSet<ConstraintViolation<Invoice>>(); 094 Trade trade = invoice.getTrade(); 095 096 if (trade != null) { 097 Settlement settlement = trade.getSettlement(); 098 099 List<Class<?>> validationGroupsList = Arrays.asList(validationGroups); 100 101 if (settlement.getMonetarySummation() != null) { 102 log.debug("Validating invoice monetary summation..."); 103 104 MonetarySummation monetarySummation = settlement.getMonetarySummation(); 105 MonetarySummation calculatedMonetarySummation = AmountCalculator.recalculate(invoice).getMonetarySummation(); 106 107 Class<?> clazz = MonetarySummation.class; 108 109 if (belongsToProfile(clazz, "getGrandTotal", validationGroupsList) && 110 !areEqual(monetarySummation.getGrandTotal(), calculatedMonetarySummation.getGrandTotal())) { 111 String message = message(monetarySummation.getGrandTotal(), calculatedMonetarySummation.getGrandTotal()); 112 violations.add(new Violation(invoice, message, "monetarySummation.grandTotal.error", "trade.settlement.monetarySummation.grandTotal", monetarySummation.getGrandTotal() != null ? monetarySummation.getGrandTotal().getValue() : null)); 113 } 114 115 if (belongsToProfile(clazz, "getTaxBasisTotal", validationGroupsList) && 116 !areEqual(monetarySummation.getTaxBasisTotal(), calculatedMonetarySummation.getTaxBasisTotal())) { 117 String message = message(monetarySummation.getTaxBasisTotal(), calculatedMonetarySummation.getTaxBasisTotal()); 118 violations.add(new Violation(invoice, message, "monetarySummation.taxBasisTotal.error", "trade.settlement.monetarySummation.taxBasisTotal", monetarySummation.getTaxBasisTotal() != null ? monetarySummation.getTaxBasisTotal().getValue() : null)); 119 } 120 121 if (belongsToProfile(clazz, "getChargeTotal", validationGroupsList) && 122 !areEqual(monetarySummation.getChargeTotal(), calculatedMonetarySummation.getChargeTotal())) { 123 String message = message(monetarySummation.getChargeTotal(), calculatedMonetarySummation.getChargeTotal()); 124 violations.add(new Violation(invoice, message, "monetarySummation.chargeTotal.error", "trade.settlement.monetarySummation.chargeTotal", monetarySummation.getChargeTotal() != null ? monetarySummation.getChargeTotal().getValue() : null)); 125 } 126 127 if (belongsToProfile(clazz, "getAllowanceTotal", validationGroupsList) && 128 !areEqual(monetarySummation.getAllowanceTotal(), calculatedMonetarySummation.getAllowanceTotal())) { 129 String message = message(monetarySummation.getAllowanceTotal(), calculatedMonetarySummation.getAllowanceTotal()); 130 violations.add(new Violation(invoice, message, "monetarySummation.allowanceTotal.error", "trade.settlement.monetarySummation.allowanceTotal", monetarySummation.getAllowanceTotal() != null ? monetarySummation.getAllowanceTotal().getValue() : null)); 131 } 132 133 boolean expectDuePayable = monetarySummation.getTotalPrepaid() != null && !isEqualZero(monetarySummation.getTotalPrepaid()); 134 if (settlement.getPaymentMeans() != null) { 135 for (PaymentMeans paymentMeans : settlement.getPaymentMeans()) { 136 expectDuePayable = expectDuePayable || paymentMeans.getCode() != null; 137 } 138 } 139 140 if (belongsToProfile(clazz, "getDuePayable", validationGroupsList) && 141 expectDuePayable && 142 !areEqual(monetarySummation.getDuePayable(), calculatedMonetarySummation.getDuePayable())) { 143 String message = message(monetarySummation.getDuePayable(), calculatedMonetarySummation.getDuePayable()); 144 violations.add(new Violation(invoice, message, "monetarySummation.duePayable.error", "trade.settlement.monetarySummation.duePayable", monetarySummation.getDuePayable() != null ? monetarySummation.getDuePayable().getValue() : null)); 145 } 146 147 if (belongsToProfile(clazz, "getLineTotal", validationGroupsList) && 148 !areEqual(monetarySummation.getLineTotal(), calculatedMonetarySummation.getLineTotal())) { 149 String message = message(monetarySummation.getLineTotal(), calculatedMonetarySummation.getLineTotal()); 150 violations.add(new Violation(invoice, message, "monetarySummation.lineTotal.error", "trade.settlement.monetarySummation.lineTotal", monetarySummation.getLineTotal() != null ? monetarySummation.getLineTotal().getValue() : null)); 151 } 152 153 if (belongsToProfile(clazz, "getTaxTotal", validationGroupsList) && 154 !areEqual(monetarySummation.getTaxTotal(), calculatedMonetarySummation.getTaxTotal())) { 155 String message = message(monetarySummation.getTaxTotal(), calculatedMonetarySummation.getTaxTotal()); 156 violations.add(new Violation(invoice, message, "monetarySummation.taxTotal.error", "trade.settlement.monetarySummation.taxTotal", monetarySummation.getTaxTotal() != null ? monetarySummation.getTaxTotal().getValue() : null)); 157 } 158 159 if (belongsToProfile(clazz, "getTotalPrepaid", validationGroupsList) && 160 !areEqual(monetarySummation.getTotalPrepaid(), calculatedMonetarySummation.getTotalPrepaid())) { 161 String message = message(monetarySummation.getTotalPrepaid(), calculatedMonetarySummation.getTotalPrepaid()); 162 violations.add(new Violation(invoice, message, "monetarySummation.totalPrepaid.error", "trade.settlement.monetarySummation.totalPrepaid", monetarySummation.getTotalPrepaid() != null ? monetarySummation.getTotalPrepaid().getValue() : null)); 163 } 164 } 165 166 log.debug("Validating item's specified monetary summations..."); 167 168 if (trade.getItems() != null) { 169 for (int i = 0; i < trade.getItems().size(); i++) { 170 Item item = trade.getItems().get(i); 171 172 if (item.getSettlement() != null) { 173 SpecifiedSettlement specifiedSettlement = item.getSettlement(); 174 175 if (specifiedSettlement.getMonetarySummation() != null) { 176 SpecifiedMonetarySummation monetarySummation = specifiedSettlement.getMonetarySummation(); 177 SpecifiedMonetarySummation calculatedMonetarySummation = AmountCalculator.calculateSpecifiedMonetarySummation(item); 178 179 if (belongsToProfile(SpecifiedMonetarySummation.class, "getLineTotal", validationGroupsList) && 180 !areEqual(monetarySummation.getLineTotal(), calculatedMonetarySummation.getLineTotal())) { 181 String message = message(monetarySummation.getLineTotal(), calculatedMonetarySummation.getLineTotal()); 182 violations.add(new Violation(invoice, message, "item.monetarySummation.lineTotal.error", "trade.items["+i+"].settlement.monetarySummation.lineTotal", monetarySummation.getLineTotal() != null ? monetarySummation.getLineTotal().getValue() : null)); 183 } 184 if (belongsToProfile(SpecifiedMonetarySummation.class, "getTotalAllowanceCharge", validationGroupsList) && 185 ((grossPriceIncludesCharges(item) && monetarySummation.getTotalAllowanceCharge() == null) || !areEqual(monetarySummation.getTotalAllowanceCharge(), calculatedMonetarySummation.getTotalAllowanceCharge()))) { 186 String message = message(monetarySummation.getTotalAllowanceCharge(), calculatedMonetarySummation.getTotalAllowanceCharge()); 187 violations.add(new Violation(invoice, message, "item.monetarySummation.totalAllowanceCharge.error", "trade.items["+i+"].settlement.monetarySummation.totalAllowanceCharge", monetarySummation.getTotalAllowanceCharge() != null ? monetarySummation.getTotalAllowanceCharge().getValue() : null)); 188 } 189 } 190 } 191 } 192 } 193 } 194 195 return violations; 196 } 197 198 private static boolean grossPriceIncludesCharges(final Item item) { 199 boolean result = false; 200 201 if (item != null && item.getAgreement() != null && item.getAgreement().getGrossPrice() != null) { 202 GrossPrice grossPrice = item.getAgreement().getGrossPrice(); 203 204 if (grossPrice.getAllowanceCharges() != null) { 205 return !grossPrice.getAllowanceCharges().isEmpty(); 206 } 207 } 208 209 return result; 210 } 211 212 private static boolean isEqualZero(final Amount amount) { 213 if (amount == null || amount.getValue() == null) { 214 return false; 215 } 216 217 return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP).equals(amount.getValue().setScale(2, RoundingMode.HALF_UP)); 218 } 219 220 private String message(final Amount current, final Amount expected) { 221 Object currentValue = current != null ? current.getValue() : "null"; 222 Object expectedValue = expected != null ? expected.getValue() : "null"; 223 224 return messageInterpolator.interpolate("{io.konik.validation.amount.calculation.error}", new Violation.Context(currentValue, expectedValue)); 225 } 226 227 private static boolean areEqual(final Amount first, final Amount second) { 228 if (first == null && second == null) { 229 return true; 230 } 231 232 if (zeroEqualsNull(first, second) || zeroEqualsNull(second, first)) { 233 return true; 234 } 235 236 if (first == null || second == null) { 237 return false; 238 } 239 240 if (first.getCurrency() != null && second.getCurrency() != null) { 241 if (!first.getCurrency().getCurrency().equals(second.getCurrency().getCurrency())) { 242 return false; 243 } 244 } 245 246 if (first.getValue() != null && second.getValue() != null) { 247 return first.getValue() 248 .setScale(2, RoundingMode.HALF_UP) 249 .equals(second.getValue() 250 .setScale(2, RoundingMode.HALF_UP) 251 ); 252 } 253 254 return false; 255 } 256 257 private static boolean zeroEqualsNull(final Amount first, final Amount second) { 258 return first == null && 259 second != null && 260 second.getValue() != null && 261 second.getValue().setScale(2, RoundingMode.HALF_UP).equals(BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP)); 262 } 263 264 private static class Violation implements ConstraintViolation<Invoice> { 265 266 private final Invoice invoice; 267 private final String message; 268 private final String messageTemplate; 269 private final String propertyPath; 270 private final Object invalidValue; 271 272 public Violation(Invoice invoice, String message, String messageTemplate, String propertyPath, Object invalidValue) { 273 this.invoice = invoice; 274 this.message = message; 275 this.messageTemplate = messageTemplate; 276 this.propertyPath = propertyPath; 277 this.invalidValue = invalidValue; 278 } 279 280 @Override 281 public String getMessage() { 282 return message; 283 } 284 285 @Override 286 public String getMessageTemplate() { 287 return messageTemplate; 288 } 289 290 @Override 291 public Invoice getRootBean() { 292 return invoice; 293 } 294 295 @Override 296 public Class<Invoice> getRootBeanClass() { 297 return Invoice.class; 298 } 299 300 @Override 301 public Object getLeafBean() { 302 return null; 303 } 304 305 @Override 306 public Object[] getExecutableParameters() { 307 return new Object[0]; 308 } 309 310 @Override 311 public Object getExecutableReturnValue() { 312 return null; 313 } 314 315 @Override 316 public Path getPropertyPath() { 317 return PathImpl.createPathFromString(propertyPath); 318 } 319 320 @Override 321 public Object getInvalidValue() { 322 return invalidValue; 323 } 324 325 @Override 326 public ConstraintDescriptor<?> getConstraintDescriptor() { 327 return null; 328 } 329 330 @Override 331 public <U> U unwrap(Class<U> type) { 332 return null; 333 } 334 335 static class Context implements MessageInterpolator.Context { 336 337 private final Object currentValue; 338 private final Object expectedValue; 339 340 public Context(Object currentValue, Object expectedValue) { 341 this.currentValue = currentValue; 342 this.expectedValue = expectedValue; 343 } 344 345 @Override 346 public ConstraintDescriptor<?> getConstraintDescriptor() { 347 return new ConstraintDescriptor<Annotation>() { 348 @Override 349 public Annotation getAnnotation() { 350 return null; 351 } 352 353 @Override 354 public String getMessageTemplate() { 355 return "{io.konik.validation.amount.calculation.error}"; 356 } 357 358 @Override 359 public Set<Class<?>> getGroups() { 360 return null; 361 } 362 363 @Override 364 public Set<Class<? extends Payload>> getPayload() { 365 return null; 366 } 367 368 @Override 369 public ConstraintTarget getValidationAppliesTo() { 370 return null; 371 } 372 373 @Override 374 public List<Class<? extends ConstraintValidator<Annotation, ?>>> getConstraintValidatorClasses() { 375 return new LinkedList<Class<? extends ConstraintValidator<Annotation, ?>>>(); 376 } 377 378 @Override 379 public Map<String, Object> getAttributes() { 380 Map<String,Object> map = new HashMap<String, Object>(); 381 map.put("currentValue", currentValue); 382 map.put("expectedValue", expectedValue); 383 return map; 384 } 385 386 @Override 387 public Set<ConstraintDescriptor<?>> getComposingConstraints() { 388 return new HashSet<ConstraintDescriptor<?>>(); 389 } 390 391 @Override 392 public boolean isReportAsSingleViolation() { 393 return false; 394 } 395 }; 396 } 397 398 @Override 399 public Object getValidatedValue() { 400 return currentValue; 401 } 402 403 @Override 404 public <T> T unwrap(Class<T> type) { 405 return null; 406 } 407 } 408 } 409}