NotificationMessageGenerator.java

  1. package no.nav.data.team.notify;

  2. import com.github.benmanes.caffeine.cache.Caffeine;
  3. import com.github.benmanes.caffeine.cache.LoadingCache;
  4. import lombok.Data;
  5. import lombok.extern.slf4j.Slf4j;
  6. import no.nav.data.common.auditing.domain.AuditVersion;
  7. import no.nav.data.common.auditing.domain.AuditVersionRepository;
  8. import no.nav.data.common.exceptions.NotFoundException;
  9. import no.nav.data.common.storage.StorageService;
  10. import no.nav.data.team.contact.domain.ContactMessage;
  11. import no.nav.data.team.notify.domain.NotificationTask;
  12. import no.nav.data.team.notify.domain.NotificationTask.AuditTarget;
  13. import no.nav.data.team.notify.dto.MailModels;
  14. import no.nav.data.team.notify.dto.MailModels.InactiveModel;
  15. import no.nav.data.team.notify.dto.MailModels.NudgeModel;
  16. import no.nav.data.team.notify.dto.MailModels.Resource;
  17. import no.nav.data.team.notify.dto.MailModels.TypedItem;
  18. import no.nav.data.team.notify.dto.MailModels.UpdateItem;
  19. import no.nav.data.team.notify.dto.MailModels.UpdateModel;
  20. import no.nav.data.team.notify.dto.MailModels.UpdateModel.TargetType;
  21. import no.nav.data.team.po.domain.ProductArea;
  22. import no.nav.data.team.resource.NomClient;
  23. import no.nav.data.team.shared.Lang;
  24. import no.nav.data.team.shared.domain.Member;
  25. import no.nav.data.team.shared.domain.Membered;
  26. import no.nav.data.team.team.domain.Team;
  27. import org.apache.commons.lang3.StringUtils;
  28. import org.springframework.stereotype.Service;

  29. import java.time.Duration;
  30. import java.util.ArrayList;
  31. import java.util.List;
  32. import java.util.Objects;
  33. import java.util.Optional;
  34. import java.util.UUID;
  35. import java.util.stream.Collectors;

  36. import static no.nav.data.common.utils.StreamUtils.convert;
  37. import static no.nav.data.common.utils.StreamUtils.filterCommonElements;
  38. import static no.nav.data.team.contact.domain.ContactMessage.Paragraph.VarselUrl.url;

  39. @Slf4j
  40. @Service
  41. public class NotificationMessageGenerator {

  42.     private final AuditVersionRepository auditVersionRepository;
  43.     private final LoadingCache<UUID, AuditVersion> auditCache;
  44.     private final LoadingCache<UUID, ProductArea> paCache;
  45.     private final UrlGenerator urlGenerator;
  46.     private final NomClient nomClient;

  47.     public NotificationMessageGenerator(AuditVersionRepository auditVersionRepository,
  48.             StorageService storageService, UrlGenerator urlGenerator, NomClient nomClient) {
  49.         this.auditVersionRepository = auditVersionRepository;
  50.         this.auditCache = Caffeine.newBuilder().recordStats()
  51.                 .expireAfterWrite(Duration.ofMinutes(5))
  52.                 .maximumSize(1000).build(id -> auditVersionRepository.findById(id).orElseThrow());
  53.         this.paCache = Caffeine.newBuilder().recordStats()
  54.                 .expireAfterWrite(Duration.ofMinutes(1))
  55.                 .maximumSize(1000).build(id -> storageService.get(id, ProductArea.class));

  56.         this.urlGenerator = urlGenerator;
  57.         this.nomClient = nomClient;
  58.     }

  59.     public NotificationMessage<UpdateModel> updateSummary(NotificationTask task) {
  60.         var model = new UpdateModel();
  61.         model.setBaseUrl(urlGenerator.getBaseUrl());
  62.         model.setTime(task.getTime());
  63.         task.getTargets().forEach(this::fetchAuditVersions);

  64.         task.getTargets().forEach(t -> {
  65.             if (t.isSilent()) {
  66.                 // target is here only for calculating other diffs, ie. teams in/out of product area
  67.                 return;
  68.             }
  69.             if (t.isCreate()) {
  70.                 AuditVersion auditVersion = t.getCurrAuditVersion();
  71.                 model.getCreated().add(auditToTypedItem(auditVersion, false));
  72.             } else if (t.isDelete()) {
  73.                 AuditVersion auditVersion = t.getPrevAuditVersion();
  74.                 model.getDeleted().add(auditToTypedItem(auditVersion, true));
  75.             } else if (t.isEdit()) { // is edit check -> temp fix due to scheduler bug
  76.                 AuditVersion prevVersion = t.getPrevAuditVersion();
  77.                 AuditVersion currVersion = t.getCurrAuditVersion();
  78.                 UpdateItem diff = diffItem(prevVersion, currVersion, task);
  79.                 if (diff.hasChanged()) {
  80.                     model.getUpdated().add(diff);
  81.                 }
  82.             }
  83.         });

  84.         boolean isEmpty = model.getCreated().isEmpty() && model.getDeleted().isEmpty() && model.getUpdated().isEmpty();
  85.         return new NotificationMessage<>("Teamkatalog oppdatering", model, urlGenerator.isDev(), isEmpty);
  86.     }

  87.     private void fetchAuditVersions(AuditTarget auditTarget) {
  88.         Optional.ofNullable(auditTarget.getPrevAuditId()).ifPresent(id -> auditTarget.setPrevAuditVersion(auditCache.get(id)));
  89.         Optional.ofNullable(auditTarget.getCurrAuditId()).ifPresent(id -> auditTarget.setCurrAuditVersion(auditCache.get(id)));
  90.     }

  91.     private UpdateItem diffItem(AuditVersion prevVersion, AuditVersion currVersion, NotificationTask task) {
  92.         var item = UpdateItem.builder();

  93.         var toName = nameFor(currVersion);
  94.         item.fromName(nameFor(prevVersion));
  95.         item.toName(toName);
  96.         item.item(new TypedItem(typeForAudit(currVersion), currVersion.getTableId(), urlGenerator.urlFor(currVersion), toName));

  97.         if (prevVersion.isTeam()) {
  98.             Team prevData = prevVersion.getTeamData();
  99.             Team currData = currVersion.getTeamData();
  100.             item.fromOwnershipType(Lang.teamOwnershipType(prevData.getTeamOwnershipType()));
  101.             item.toOwnershipType(Lang.teamOwnershipType(currData.getTeamOwnershipType()));
  102.             item.fromTeamType(Lang.teamType(prevData.getTeamType()));
  103.             item.toTeamType(Lang.teamType(currData.getTeamType()));

  104.             if (!Objects.equals(prevData.getProductAreaId(), currData.getProductAreaId())) {
  105.                 Optional.ofNullable(prevData.getProductAreaId()).map(this::getPa).ifPresent(item::oldProductArea);
  106.                 Optional.ofNullable(currData.getProductAreaId()).map(this::getPa).ifPresent(item::newProductArea);
  107.             }
  108.         }
  109.         if (prevVersion.isProductArea()) {
  110.             var newTeams = new ArrayList<AuditTarget>();
  111.             var removedTeams = new ArrayList<AuditTarget>();
  112.             var paId = UUID.fromString(prevVersion.getTableId());
  113.             // Task with productArea targets will contain all it's teams' targets
  114.             var teamTargets = task.getTargets().stream()
  115.                     .filter(AuditTarget::isTeam)
  116.                     .filter(t -> paId.equals(paIdForTeamAudit(t.getPrevAuditVersion())) || paId.equals(paIdForTeamAudit(t.getCurrAuditVersion())))
  117.                     .collect(Collectors.toList());

  118.             log.info("Looking into teams for pa {} {} of {} total targets", paId, teamTargets.size(), task.getTargets().size());

  119.             teamTargets.forEach(t -> {
  120.                 if (t.isCreate() || !paId.equals(paIdForTeamAudit(t.getPrevAuditVersion()))) {
  121.                     log.info("Team added {}", t.getTargetId());
  122.                     newTeams.add(t);
  123.                 } else if (t.isDelete() || !paId.equals(paIdForTeamAudit(t.getCurrAuditVersion()))) {
  124.                     log.info("Team removed {}", t.getTargetId());
  125.                     removedTeams.add(t);
  126.                 }
  127.             });
  128.             item.newTeams(convert(newTeams, this::teamTargetToTypedItem));
  129.             item.removedTeams(convert(removedTeams, this::teamTargetToTypedItem));

  130.             ProductArea prevData = prevVersion.getProductAreaData();
  131.             ProductArea currData = currVersion.getProductAreaData();
  132.             item.fromAreaType(Lang.areaType(prevData.getAreaType()));
  133.             item.toAreaType(Lang.areaType(currData.getAreaType()));
  134.         }
  135.         var fromMembers = members(prevVersion);
  136.         var toMembers = members(currVersion);

  137.         item.removedMembers(convertMember(filterCommonElements(fromMembers, toMembers, Member::getNavIdent)));
  138.         item.newMembers(convertMember(filterCommonElements(toMembers, fromMembers, Member::getNavIdent)));

  139.         return item.build();
  140.     }

  141.     private TypedItem getPa(UUID id) {
  142.         ProductArea pa = null;
  143.         try {
  144.             pa = paCache.get(id);
  145.         } catch (NotFoundException e) {
  146.             log.trace("Product area has been deleted {}", id);
  147.         }
  148.         if (pa == null) {
  149.             pa = auditVersionRepository.findByTableIdOrderByTimeDescLimitOne(id.toString()).getProductAreaData();
  150.             return paToItem(pa, true);
  151.         }
  152.         return paToItem(pa, false);
  153.     }

  154.     private String teamNameFor(AuditTarget teamTarget) {
  155.         return teamTarget.isCreate() || teamTarget.isUpdate() ? teamTarget.getCurrAuditVersion().getTeamData().getName()
  156.                 : teamTarget.getPrevAuditVersion().getTeamData().getName();
  157.     }

  158.     private UUID paIdForTeamAudit(AuditVersion auditVersion) {
  159.         return auditVersion == null ? null : auditVersion.getTeamData().getProductAreaId();
  160.     }

  161.     private List<Resource> convertMember(List<? extends Member> list) {
  162.         return convertIdents(convert(list, Member::getNavIdent));
  163.     }

  164.     private List<Resource> convertIdents(List<String> list) {
  165.         return convert(list, ident -> new Resource(urlGenerator.resourceUrl(ident), nomClient.getNameForIdent(ident).orElse(ident), ident));
  166.     }

  167.     private List<Member> members(AuditVersion version) {
  168.         if (version.isTeam()) {
  169.             return List.copyOf(version.getTeamData().getMembers());
  170.         } else if (version.isProductArea()) {
  171.             return List.copyOf(version.getProductAreaData().getMembers());
  172.         }
  173.         return List.of();
  174.     }

  175.     public ContactMessage nudgeTime(Membered membered, String role) {
  176.         NudgeModel model = NudgeModel.builder()
  177.                 .targetUrl(urlGenerator.urlFor(membered.getClass(), membered.getId()))
  178.                 .targetName(membered.getName())
  179.                 .targetType(Lang.objectType(membered.getClass()))
  180.                 .recipientRole(role.toLowerCase())
  181.                 .cutoffTime(NotificationConstants.NUDGE_TIME_CUTOFF_DESCRIPTION)
  182.                 .build();

  183.         var subject = "Teamkatalog påminnelse for %s %s".formatted(model.getTargetType(), model.getTargetName());

  184.         return new ContactMessage(subject, "nudge")
  185.                 .paragraph("Hei, det har nå gått over %s siden %%s ble sist oppdatert.".formatted(model.getCutoffTime()),
  186.                         url(model.getTargetUrl(), "%s %s".formatted(model.getTargetType(), model.getTargetName())))
  187.                 .paragraph("Som %s mottar du derfor en påminnelse for å sikre at innholdet er korrekt.".formatted(model.getRecipientRole()))
  188.                 .footer(model.getTargetUrl());
  189.     }

  190.     public ContactMessage inactive(Membered membered, String role, List<String> identsInactive) {
  191.         InactiveModel model = InactiveModel.builder()
  192.                 .targetUrl(urlGenerator.urlFor(membered.getClass(), membered.getId()))
  193.                 .targetName(membered.getName())
  194.                 .targetType(Lang.objectType(membered.getClass()))
  195.                 .recipientRole(role.toLowerCase())
  196.                 .members(convertIdents(identsInactive))
  197.                 .build();

  198.         String subject = "Medlemmer av %s %s har blitt inaktive".formatted(model.getTargetType(), model.getTargetName());
  199.         var message = new ContactMessage(subject, "inactive")
  200.                 .paragraph("Hei, %s har nå fått inaktive medlem(mer)", url(model.getTargetUrl(), "%s %s".formatted(model.getTargetType(), model.getTargetName())))
  201.                 .paragraph("Som %s mottar du derfor en påminnelse for å sikre at innholdet er korrekt.".formatted(model.getRecipientRole()))
  202.                 .spacing()
  203.                 .paragraph("Nye inaktive medlemmer:");

  204.         for (MailModels.Resource member : model.getMembers()) {
  205.             message.paragraph(" - %s", url(member.getUrl(), member.getName()));
  206.         }
  207.         message.footer(model.getTargetUrl());

  208.         return message;
  209.     }

  210.     private TargetType typeForAudit(AuditVersion auditVersion) {
  211.         if (auditVersion.isProductArea()) {
  212.             return TargetType.AREA;
  213.         } else if (auditVersion.isTeam()) {
  214.             return TargetType.TEAM;
  215.         }
  216.         return null;
  217.     }

  218.     private String nameFor(AuditVersion auditVersion) {
  219.         if (auditVersion.isTeam()) {
  220.             return auditVersion.getTeamData().getName();
  221.         } else if (auditVersion.isProductArea()) {
  222.             return auditVersion.getProductAreaData().getName();
  223.         }
  224.         return StringUtils.EMPTY;
  225.     }

  226.     private TypedItem auditToTypedItem(AuditVersion auditVersion, boolean deleted) {
  227.         return new TypedItem(typeForAudit(auditVersion), auditVersion.getTableId(), urlGenerator.urlFor(auditVersion), nameFor(auditVersion), deleted);
  228.     }

  229.     private TypedItem teamTargetToTypedItem(AuditTarget target) {
  230.         return new TypedItem(TargetType.TEAM, target.getTargetId().toString(), urlGenerator.urlFor(Team.class, target.getTargetId()), teamNameFor(target), target.isDelete());
  231.     }

  232.     private TypedItem paToItem(ProductArea pa, boolean deleted) {
  233.         return new TypedItem(TargetType.AREA, pa.getId().toString(), urlGenerator.urlFor(pa.getClass(), pa.getId()), pa.getName(), deleted);
  234.     }

  235.     @Data
  236.     public static class NotificationMessage<T> {

  237.         private final String subject;
  238.         private final T model;
  239.         private final boolean empty;
  240.         private final boolean dev;

  241.         NotificationMessage(String subject, T model, boolean dev, boolean isEmpty) {
  242.             this.subject = subject + (dev ? " [DEV]" : "");
  243.             this.model = model;
  244.             this.empty = isEmpty;
  245.             this.dev = dev;
  246.         }

  247.         NotificationMessage(String subject, T model, boolean dev) {
  248.             this(subject, model, dev, false);
  249.         }
  250.     }

  251. }