From 6d9e33b18d6837c9899b2ffc5dc96d9e50bbffc1 Mon Sep 17 00:00:00 2001 From: Carsten Otto Date: Sat, 25 Dec 2021 20:03:36 +0100 Subject: [PATCH] report online percentage in online report --- .../service/OnlinePeersService.java | 45 +++++++++- .../service/OnlinePeersServiceTest.java | 90 ++++++++++++++++++- .../cotto/lndmanagej/model/OnlineReport.java | 6 +- .../lndmanagej/model/OnlineReportTest.java | 7 +- .../model/OnlineReportFixtures.java | 4 +- .../persistence/OnlinePeersRepositoryIT.java | 18 ++++ .../onlinepeers/OnlinePeersDao.java | 3 + .../persistence/OnlinePeerJpaDto.java | 4 + .../persistence/OnlinePeersDaoImpl.java | 8 ++ .../persistence/OnlinePeersRepository.java | 3 + .../persistence/OnlinePeersDaoImplTest.java | 9 ++ .../controller/NodeControllerIT.java | 1 + .../controller/dto/OnlineReportDto.java | 4 +- .../controller/dto/OnlineReportDtoTest.java | 6 +- 14 files changed, 194 insertions(+), 14 deletions(-) diff --git a/backend/src/main/java/de/cotto/lndmanagej/service/OnlinePeersService.java b/backend/src/main/java/de/cotto/lndmanagej/service/OnlinePeersService.java index ea1268c4..81437fb6 100644 --- a/backend/src/main/java/de/cotto/lndmanagej/service/OnlinePeersService.java +++ b/backend/src/main/java/de/cotto/lndmanagej/service/OnlinePeersService.java @@ -3,15 +3,19 @@ package de.cotto.lndmanagej.service; import de.cotto.lndmanagej.model.Node; import de.cotto.lndmanagej.model.OnlineReport; import de.cotto.lndmanagej.model.OnlineStatus; +import de.cotto.lndmanagej.model.Pubkey; import de.cotto.lndmanagej.onlinepeers.OnlinePeersDao; import org.springframework.stereotype.Component; +import java.time.Duration; import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; +import java.util.List; @Component public class OnlinePeersService { + private static final int DAYS_FOR_OFFLINE_PERCENTAGE = 7; private final OnlinePeersDao dao; public OnlinePeersService(OnlinePeersDao dao) { @@ -20,11 +24,44 @@ public class OnlinePeersService { public OnlineReport getOnlineReport(Node node) { boolean online = node.online(); - OnlineStatus mostRecentOnlineStatus = dao.getMostRecentOnlineStatus(node.pubkey()).orElse(null); - if (mostRecentOnlineStatus != null && mostRecentOnlineStatus.online() == online) { - return OnlineReport.createFromStatus(mostRecentOnlineStatus); + OnlineStatus mostRecentOnlineStatus = dao.getMostRecentOnlineStatus(node.pubkey()) + .orElse(new OnlineStatus(online, now())); + int onlinePercentageLastWeek = getOnlinePercentageLastWeek(node.pubkey()); + if (mostRecentOnlineStatus.online() == online) { + return OnlineReport.createFromStatus(mostRecentOnlineStatus, onlinePercentageLastWeek); } - return new OnlineReport(online, now()); + return new OnlineReport(online, now(), onlinePercentageLastWeek); + } + + public int getOnlinePercentageLastWeek(Pubkey pubkey) { + Duration total = Duration.ZERO; + Duration online = Duration.ZERO; + ZonedDateTime intervalStart = ZonedDateTime.now(ZoneOffset.UTC); + ZonedDateTime cutoff = intervalStart.minusDays(DAYS_FOR_OFFLINE_PERCENTAGE); + boolean shouldContinue = true; + List allForPeer = dao.getAllForPeer(pubkey); + for (int i = 0; i < allForPeer.size() && shouldContinue; i++) { + OnlineStatus onlineStatus = allForPeer.get(i); + ZonedDateTime intervalEnd = onlineStatus.since(); + if (intervalEnd.isBefore(cutoff)) { + intervalEnd = cutoff; + shouldContinue = false; + } + Duration difference = Duration.between(intervalEnd, intervalStart); + total = total.plus(difference); + if (onlineStatus.online()) { + online = online.plus(difference); + } + intervalStart = intervalEnd; + } + if (total.isZero()) { + return 0; + } + return getRoundedPercentage(total, online); + } + + private int getRoundedPercentage(Duration total, Duration offline) { + return (int) (offline.getSeconds() * 100.0 / total.getSeconds()); } private ZonedDateTime now() { diff --git a/backend/src/test/java/de/cotto/lndmanagej/service/OnlinePeersServiceTest.java b/backend/src/test/java/de/cotto/lndmanagej/service/OnlinePeersServiceTest.java index c7adef82..6780ca9d 100644 --- a/backend/src/test/java/de/cotto/lndmanagej/service/OnlinePeersServiceTest.java +++ b/backend/src/test/java/de/cotto/lndmanagej/service/OnlinePeersServiceTest.java @@ -1,6 +1,7 @@ package de.cotto.lndmanagej.service; import de.cotto.lndmanagej.model.OnlineReport; +import de.cotto.lndmanagej.model.OnlineStatus; import de.cotto.lndmanagej.onlinepeers.OnlinePeersDao; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -10,6 +11,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import java.time.ZoneOffset; import java.time.ZonedDateTime; +import java.util.List; import java.util.Optional; import static de.cotto.lndmanagej.model.NodeFixtures.NODE; @@ -23,6 +25,8 @@ import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) class OnlinePeersServiceTest { + private static final ZonedDateTime NOW = ZonedDateTime.now(ZoneOffset.UTC); + @InjectMocks private OnlinePeersService onlinePeersService; @@ -32,6 +36,7 @@ class OnlinePeersServiceTest { @Test void with_time_if_given_status_matches_last_known_status() { when(dao.getMostRecentOnlineStatus(PUBKEY)).thenReturn(Optional.of(ONLINE_STATUS)); + mockFor23PercentOffline(); assertThat(onlinePeersService.getOnlineReport(NODE_PEER)).isEqualTo(ONLINE_REPORT); } @@ -57,9 +62,92 @@ class OnlinePeersServiceTest { assertVeryRecentSince(report); } + @Test + void getOnlinePercentageLastWeek_no_data() { + assertThat(onlinePeersService.getOnlinePercentageLastWeek(PUBKEY)).isZero(); + } + + @Test + void getOnlinePercentageLastWeek_always_online() { + ZonedDateTime early = ZonedDateTime.of(2018, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC); + when(dao.getAllForPeer(PUBKEY)).thenReturn(List.of(new OnlineStatus(true, early))); + assertThat(onlinePeersService.getOnlinePercentageLastWeek(PUBKEY)).isEqualTo(100); + } + + @Test + void getOnlinePercentageLastWeek_always_offline() { + ZonedDateTime early = ZonedDateTime.of(2018, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC); + when(dao.getAllForPeer(PUBKEY)).thenReturn(List.of(new OnlineStatus(false, early))); + assertThat(onlinePeersService.getOnlinePercentageLastWeek(PUBKEY)).isZero(); + } + + @Test + void getOnlinePercentageLastWeek_limited_data_offline() { + ZonedDateTime oneHourAgo = NOW.minusHours(1); + when(dao.getAllForPeer(PUBKEY)).thenReturn(List.of(new OnlineStatus(false, oneHourAgo))); + assertThat(onlinePeersService.getOnlinePercentageLastWeek(PUBKEY)).isZero(); + } + + @Test + void getOnlinePercentageLastWeek_limited_data_online() { + ZonedDateTime oneHourAgo = NOW.minusHours(1); + when(dao.getAllForPeer(PUBKEY)).thenReturn(List.of(new OnlineStatus(true, oneHourAgo))); + assertThat(onlinePeersService.getOnlinePercentageLastWeek(PUBKEY)).isEqualTo(100); + } + + @Test + void getOnlinePercentageLastWeek_limited_data_online_then_offline() { + ZonedDateTime twoHoursAgo = NOW.minusHours(2); + ZonedDateTime oneHourAgo = NOW.minusHours(1); + when(dao.getAllForPeer(PUBKEY)).thenReturn(List.of( + new OnlineStatus(true, oneHourAgo), + new OnlineStatus(false, twoHoursAgo) + )); + assertThat(onlinePeersService.getOnlinePercentageLastWeek(PUBKEY)).isEqualTo(50); + } + + @Test + void getOnlinePercentageLastWeek_limited_data_offline_then_online() { + ZonedDateTime twoHoursAgo = NOW.minusHours(2); + ZonedDateTime oneHourAgo = NOW.minusHours(1); + when(dao.getAllForPeer(PUBKEY)).thenReturn(List.of( + new OnlineStatus(false, oneHourAgo), + new OnlineStatus(true, twoHoursAgo) + )); + assertThat(onlinePeersService.getOnlinePercentageLastWeek(PUBKEY)).isEqualTo(49); + } + + @Test + void getOnlinePercentageLastWeek_cuts_off_old_data() { + ZonedDateTime twoYearsAgo = NOW.minusYears(2); + ZonedDateTime oneYearAgo = NOW.minusYears(1); + ZonedDateTime thirteenDaysAgo = NOW.minusDays(6); + when(dao.getAllForPeer(PUBKEY)).thenReturn(List.of( + new OnlineStatus(true, thirteenDaysAgo), + new OnlineStatus(false, oneYearAgo), + new OnlineStatus(true, twoYearsAgo) + )); + assertThat(onlinePeersService.getOnlinePercentageLastWeek(PUBKEY)).isEqualTo(85); + } + + @Test + void getOnlinePercentageLastWeek_is_rounded() { + mockFor23PercentOffline(); + assertThat(onlinePeersService.getOnlinePercentageLastWeek(PUBKEY)).isEqualTo(77); + } + private void assertVeryRecentSince(OnlineReport report) { assertThat(report.since()) - .isAfter(ZonedDateTime.now(ZoneOffset.UTC).minusSeconds(1)) + .isAfter(NOW.minusSeconds(1)) .asString().hasSize(20); } + + private void mockFor23PercentOffline() { + ZonedDateTime longAgo = NOW.minusDays(14); + ZonedDateTime offlineSince = NOW.minusMinutes(2318); + when(dao.getAllForPeer(PUBKEY)).thenReturn(List.of( + new OnlineStatus(false, offlineSince), + new OnlineStatus(true, longAgo) + )); + } } \ No newline at end of file diff --git a/model/src/main/java/de/cotto/lndmanagej/model/OnlineReport.java b/model/src/main/java/de/cotto/lndmanagej/model/OnlineReport.java index 03488906..63887791 100644 --- a/model/src/main/java/de/cotto/lndmanagej/model/OnlineReport.java +++ b/model/src/main/java/de/cotto/lndmanagej/model/OnlineReport.java @@ -2,8 +2,8 @@ package de.cotto.lndmanagej.model; import java.time.ZonedDateTime; -public record OnlineReport(boolean online, ZonedDateTime since) { - public static OnlineReport createFromStatus(OnlineStatus onlineStatus) { - return new OnlineReport(onlineStatus.online(), onlineStatus.since()); +public record OnlineReport(boolean online, ZonedDateTime since, int onlinePercentageLastWeek) { + public static OnlineReport createFromStatus(OnlineStatus onlineStatus, int onlinePercentageLastWeek) { + return new OnlineReport(onlineStatus.online(), onlineStatus.since(), onlinePercentageLastWeek); } } diff --git a/model/src/test/java/de/cotto/lndmanagej/model/OnlineReportTest.java b/model/src/test/java/de/cotto/lndmanagej/model/OnlineReportTest.java index 88d2a7f0..1417d0e3 100644 --- a/model/src/test/java/de/cotto/lndmanagej/model/OnlineReportTest.java +++ b/model/src/test/java/de/cotto/lndmanagej/model/OnlineReportTest.java @@ -21,8 +21,13 @@ class OnlineReportTest { assertThat(ONLINE_REPORT.since()).isEqualTo(TIMESTAMP); } + @Test + void onlinePercentageLastWeek() { + assertThat(ONLINE_REPORT.onlinePercentageLastWeek()).isEqualTo(77); + } + @Test void createFromOnlineStatus() { - assertThat(OnlineReport.createFromStatus(ONLINE_STATUS)).isEqualTo(ONLINE_REPORT); + assertThat(OnlineReport.createFromStatus(ONLINE_STATUS, 77)).isEqualTo(ONLINE_REPORT); } } \ No newline at end of file diff --git a/model/src/testFixtures/java/de/cotto/lndmanagej/model/OnlineReportFixtures.java b/model/src/testFixtures/java/de/cotto/lndmanagej/model/OnlineReportFixtures.java index 091d22a6..75f28d97 100644 --- a/model/src/testFixtures/java/de/cotto/lndmanagej/model/OnlineReportFixtures.java +++ b/model/src/testFixtures/java/de/cotto/lndmanagej/model/OnlineReportFixtures.java @@ -7,6 +7,6 @@ import static java.time.ZoneOffset.UTC; public class OnlineReportFixtures { public static final ZonedDateTime TIMESTAMP = LocalDateTime.of(2021, 12, 23, 1, 2, 3).atZone(UTC); - public static final OnlineReport ONLINE_REPORT = new OnlineReport(true, TIMESTAMP); - public static final OnlineReport ONLINE_REPORT_OFFLINE = new OnlineReport(false, TIMESTAMP); + public static final OnlineReport ONLINE_REPORT = new OnlineReport(true, TIMESTAMP, 77); + public static final OnlineReport ONLINE_REPORT_OFFLINE = new OnlineReport(false, TIMESTAMP, 66); } diff --git a/onlinepeers/src/integrationTest/java/de/cotto/lndmanagej/onlinepeers/persistence/OnlinePeersRepositoryIT.java b/onlinepeers/src/integrationTest/java/de/cotto/lndmanagej/onlinepeers/persistence/OnlinePeersRepositoryIT.java index df08c764..2eab2f38 100644 --- a/onlinepeers/src/integrationTest/java/de/cotto/lndmanagej/onlinepeers/persistence/OnlinePeersRepositoryIT.java +++ b/onlinepeers/src/integrationTest/java/de/cotto/lndmanagej/onlinepeers/persistence/OnlinePeersRepositoryIT.java @@ -51,4 +51,22 @@ class OnlinePeersRepositoryIT { assertThat(repository.findTopByPubkeyOrderByTimestampDesc(PUBKEY.toString())) .map(OnlinePeerJpaDto::isOnline).contains(true); } + + @Test + void findByPubkeyOrderByTimestampDesc() { + repository.save(new OnlinePeerJpaDto(PUBKEY, true, TIMESTAMP)); + repository.save(new OnlinePeerJpaDto(PUBKEY, false, TIMESTAMP.minusSeconds(1))); + repository.save(new OnlinePeerJpaDto(PUBKEY_2, false, TIMESTAMP)); + assertThat(repository.findByPubkeyOrderByTimestampDesc(PUBKEY.toString())).hasSize(2); + } + + @Test + void findByPubkeyOrderByTimestampDesc_ordered_new_to_old() { + repository.save(new OnlinePeerJpaDto(PUBKEY, false, TIMESTAMP.minusSeconds(1))); + repository.save(new OnlinePeerJpaDto(PUBKEY, false, TIMESTAMP.plusSeconds(1))); + repository.save(new OnlinePeerJpaDto(PUBKEY, true, TIMESTAMP)); + long timestamp = TIMESTAMP.toEpochSecond(); + assertThat(repository.findByPubkeyOrderByTimestampDesc(PUBKEY.toString())).map(OnlinePeerJpaDto::getTimestamp) + .containsExactlyInAnyOrder(timestamp + 1, timestamp, timestamp - 1); + } } \ No newline at end of file diff --git a/onlinepeers/src/main/java/de/cotto/lndmanagej/onlinepeers/OnlinePeersDao.java b/onlinepeers/src/main/java/de/cotto/lndmanagej/onlinepeers/OnlinePeersDao.java index ad26656c..9b58c630 100644 --- a/onlinepeers/src/main/java/de/cotto/lndmanagej/onlinepeers/OnlinePeersDao.java +++ b/onlinepeers/src/main/java/de/cotto/lndmanagej/onlinepeers/OnlinePeersDao.java @@ -4,10 +4,13 @@ import de.cotto.lndmanagej.model.OnlineStatus; import de.cotto.lndmanagej.model.Pubkey; import java.time.ZonedDateTime; +import java.util.List; import java.util.Optional; public interface OnlinePeersDao { void saveOnlineStatus(Pubkey pubkey, boolean online, ZonedDateTime timestamp); Optional getMostRecentOnlineStatus(Pubkey pubkey); + + List getAllForPeer(Pubkey pubkey); } diff --git a/onlinepeers/src/main/java/de/cotto/lndmanagej/onlinepeers/persistence/OnlinePeerJpaDto.java b/onlinepeers/src/main/java/de/cotto/lndmanagej/onlinepeers/persistence/OnlinePeerJpaDto.java index 61078654..15a27284 100644 --- a/onlinepeers/src/main/java/de/cotto/lndmanagej/onlinepeers/persistence/OnlinePeerJpaDto.java +++ b/onlinepeers/src/main/java/de/cotto/lndmanagej/onlinepeers/persistence/OnlinePeerJpaDto.java @@ -52,4 +52,8 @@ public class OnlinePeerJpaDto { public String getPubkey() { return pubkey; } + + public long getTimestamp() { + return timestamp; + } } diff --git a/onlinepeers/src/main/java/de/cotto/lndmanagej/onlinepeers/persistence/OnlinePeersDaoImpl.java b/onlinepeers/src/main/java/de/cotto/lndmanagej/onlinepeers/persistence/OnlinePeersDaoImpl.java index 7e490811..b3264c39 100644 --- a/onlinepeers/src/main/java/de/cotto/lndmanagej/onlinepeers/persistence/OnlinePeersDaoImpl.java +++ b/onlinepeers/src/main/java/de/cotto/lndmanagej/onlinepeers/persistence/OnlinePeersDaoImpl.java @@ -7,6 +7,7 @@ import org.springframework.stereotype.Component; import javax.transaction.Transactional; import java.time.ZonedDateTime; +import java.util.List; import java.util.Optional; @Component @@ -27,4 +28,11 @@ class OnlinePeersDaoImpl implements OnlinePeersDao { public Optional getMostRecentOnlineStatus(Pubkey pubkey) { return repository.findTopByPubkeyOrderByTimestampDesc(pubkey.toString()).map(OnlinePeerJpaDto::toModel); } + + @Override + public List getAllForPeer(Pubkey pubkey) { + return repository.findByPubkeyOrderByTimestampDesc(pubkey.toString()).stream() + .map(OnlinePeerJpaDto::toModel) + .toList(); + } } diff --git a/onlinepeers/src/main/java/de/cotto/lndmanagej/onlinepeers/persistence/OnlinePeersRepository.java b/onlinepeers/src/main/java/de/cotto/lndmanagej/onlinepeers/persistence/OnlinePeersRepository.java index 906c645e..9960bb14 100644 --- a/onlinepeers/src/main/java/de/cotto/lndmanagej/onlinepeers/persistence/OnlinePeersRepository.java +++ b/onlinepeers/src/main/java/de/cotto/lndmanagej/onlinepeers/persistence/OnlinePeersRepository.java @@ -2,8 +2,11 @@ package de.cotto.lndmanagej.onlinepeers.persistence; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; import java.util.Optional; public interface OnlinePeersRepository extends JpaRepository { Optional findTopByPubkeyOrderByTimestampDesc(String pubkey); + + List findByPubkeyOrderByTimestampDesc(String pubkey); } diff --git a/onlinepeers/src/test/java/de/cotto/lndmanagej/onlinepeers/persistence/OnlinePeersDaoImplTest.java b/onlinepeers/src/test/java/de/cotto/lndmanagej/onlinepeers/persistence/OnlinePeersDaoImplTest.java index 90ad0b27..4bbdfc6a 100644 --- a/onlinepeers/src/test/java/de/cotto/lndmanagej/onlinepeers/persistence/OnlinePeersDaoImplTest.java +++ b/onlinepeers/src/test/java/de/cotto/lndmanagej/onlinepeers/persistence/OnlinePeersDaoImplTest.java @@ -8,6 +8,7 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import java.time.ZonedDateTime; +import java.util.List; import java.util.Objects; import java.util.Optional; @@ -53,6 +54,14 @@ class OnlinePeersDaoImplTest { assertThat(dao.getMostRecentOnlineStatus(PUBKEY)).contains(ONLINE_STATUS); } + @Test + void getAllForPeer() { + OnlinePeerJpaDto dto1 = new OnlinePeerJpaDto(PUBKEY, true, TIMESTAMP); + OnlinePeerJpaDto dto2 = new OnlinePeerJpaDto(PUBKEY, false, TIMESTAMP.plusSeconds(1)); + when(repository.findByPubkeyOrderByTimestampDesc(PUBKEY.toString())).thenReturn(List.of(dto1, dto2)); + assertThat(dao.getAllForPeer(PUBKEY)).containsExactly(dto1.toModel(), dto2.toModel()); + } + private void verifySave(Pubkey pubkey, boolean expected) { verify(repository).save(argThat(privateChannelJpaDto -> privateChannelJpaDto.isOnline() == expected)); verify(repository).save(argThat(dto -> pubkey.equals(Pubkey.create(Objects.requireNonNull(dto.getPubkey()))))); diff --git a/web/src/integrationTest/java/de/cotto/lndmanagej/controller/NodeControllerIT.java b/web/src/integrationTest/java/de/cotto/lndmanagej/controller/NodeControllerIT.java index 3f86bd99..9cbba977 100644 --- a/web/src/integrationTest/java/de/cotto/lndmanagej/controller/NodeControllerIT.java +++ b/web/src/integrationTest/java/de/cotto/lndmanagej/controller/NodeControllerIT.java @@ -101,6 +101,7 @@ class NodeControllerIT { .andExpect(jsonPath("$.onChainCosts.closeCosts", is("2000"))) .andExpect(jsonPath("$.onChainCosts.sweepCosts", is("3000"))) .andExpect(jsonPath("$.onlineReport.online", is(true))) + .andExpect(jsonPath("$.onlineReport.onlinePercentageLastWeek", is(77))) .andExpect(jsonPath("$.onlineReport.since", is("2021-12-23T01:02:03Z"))); } diff --git a/web/src/main/java/de/cotto/lndmanagej/controller/dto/OnlineReportDto.java b/web/src/main/java/de/cotto/lndmanagej/controller/dto/OnlineReportDto.java index 1a567580..a2e1931d 100644 --- a/web/src/main/java/de/cotto/lndmanagej/controller/dto/OnlineReportDto.java +++ b/web/src/main/java/de/cotto/lndmanagej/controller/dto/OnlineReportDto.java @@ -4,9 +4,9 @@ import de.cotto.lndmanagej.model.OnlineReport; import java.time.format.DateTimeFormatter; -public record OnlineReportDto(boolean online, String since) { +public record OnlineReportDto(boolean online, String since, int onlinePercentageLastWeek) { public static OnlineReportDto createFromModel(OnlineReport onlineReport) { String formattedDateTime = onlineReport.since().format(DateTimeFormatter.ISO_INSTANT); - return new OnlineReportDto(onlineReport.online(), formattedDateTime); + return new OnlineReportDto(onlineReport.online(), formattedDateTime, onlineReport.onlinePercentageLastWeek()); } } diff --git a/web/src/test/java/de/cotto/lndmanagej/controller/dto/OnlineReportDtoTest.java b/web/src/test/java/de/cotto/lndmanagej/controller/dto/OnlineReportDtoTest.java index 633ef600..5f5ec2b9 100644 --- a/web/src/test/java/de/cotto/lndmanagej/controller/dto/OnlineReportDtoTest.java +++ b/web/src/test/java/de/cotto/lndmanagej/controller/dto/OnlineReportDtoTest.java @@ -9,7 +9,11 @@ class OnlineReportDtoTest { @Test void createFromModel() { assertThat(OnlineReportDto.createFromModel(ONLINE_REPORT)).isEqualTo( - new OnlineReportDto(ONLINE_REPORT.online(), ONLINE_REPORT.since().toString()) + new OnlineReportDto( + ONLINE_REPORT.online(), + ONLINE_REPORT.since().toString(), + ONLINE_REPORT.onlinePercentageLastWeek() + ) ); }