Skip to content

Commit

Permalink
feat: Export reward list of a period - MEED-7487 - Meeds-io/MIPs#154 (#…
Browse files Browse the repository at this point in the history
…611)

This PR will implement the ability of export reward list of a period.
  • Loading branch information
AzmiTouil authored Nov 14, 2024
1 parent 7f496ae commit bd6e9b0
Show file tree
Hide file tree
Showing 7 changed files with 254 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@
*/
package io.meeds.wallet.reward.service;

import java.io.InputStream;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

Expand Down Expand Up @@ -208,4 +210,17 @@ public interface RewardReportService {
*
*/
Map<Long, Boolean> getRewardSettingChanged();

/**
* Export wallet rewards into an {@link InputStream} containing a file of format
* XLS
*
* @param periodId Reward Period id
* @param status Wallet reward status
* @param zoneId Wallet reward status
* @param fileName fileName to export
* @param locale locale
* @return {@link InputStream} of a file of format XLS
*/
InputStream exportXlsx(long periodId, String status, ZoneId zoneId, String fileName, Locale locale);
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import static io.meeds.wallet.utils.RewardUtils.timeToSecondsAtDayStart;

import java.io.InputStream;
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.ZoneId;
Expand All @@ -26,7 +27,6 @@
import java.time.temporal.TemporalAdjusters;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import javax.ws.rs.core.*;
Expand All @@ -48,6 +48,8 @@

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.bind.DefaultValue;
import org.springframework.core.io.InputStreamResource;
import org.springframework.core.io.Resource;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
Expand Down Expand Up @@ -147,6 +149,40 @@ public PagedModel<EntityModel<WalletReward>> getWalletRewards(Pageable pageable,
return assembler.toModel(walletRewards);
}

@GetMapping(path = "/export")
@Secured("rewarding")
@Operation(
summary = "Exports wallet rewards for a specified period as an XLSX file",
method = "GET",
description = "Generates and returns an XLSX file containing wallet rewards data for the specified period.")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Request fulfilled, file download initiated"),
@ApiResponse(responseCode = "400", description = "Invalid query input"),
@ApiResponse(responseCode = "401", description = "Unauthorized access to export data"),
@ApiResponse(responseCode = "500", description = "Internal server error, file export failed")})
public ResponseEntity<Resource> exportXlsx(HttpServletRequest request,
@Parameter(description = "Period id", required = true)
@RequestParam("periodId")
long periodId,
@Parameter(description = "Wallet reward status filtering, possible values: VALId and INVALID. Default value = VALId.")
@RequestParam(value = "status", defaultValue = "VALID")
String status,
@Parameter(description = "Wallet reward exported file name")
@RequestParam(value = "fileName")
String fileName) {

InputStream inputStream = rewardReportService.exportXlsx(periodId,
status,
rewardSettingsService.getSettings().zoneId(),
fileName,
request.getLocale());
InputStreamResource resource = new InputStreamResource(inputStream);
return ResponseEntity.ok()
.header("Content-Disposition", "attachment; filename=" + fileName + ".xlsx")
.header("Content-Type", "application/vnd.ms-excel")
.body(resource);
}

@PostMapping(path = "forecast")
@Secured("rewarding")
@Operation(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,13 @@
import static io.meeds.wallet.utils.WalletUtils.getResourceBundleKey;
import static io.meeds.wallet.utils.WalletUtils.isUserRewardingAdmin;

import java.io.*;
import java.math.BigInteger;
import java.nio.file.Files;
import java.nio.file.attribute.FileAttribute;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.*;
Expand All @@ -41,10 +47,17 @@
import java.util.stream.Collectors;

import io.meeds.gamification.constant.RealizationStatus;
import io.meeds.gamification.utils.Utils;
import io.meeds.wallet.model.*;
import lombok.Getter;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.SystemUtils;
import org.apache.poi.ss.usermodel.CreationHelper;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.exoplatform.services.resources.ResourceBundleService;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
Expand Down Expand Up @@ -74,6 +87,12 @@ public class WalletRewardReportService implements RewardReportService {

private static final String EMPTY_SETTINGS = "Error computing rewards using empty settings";

// File header
private static final String[] COLUMNS = new String[] { "fullName", "address", "points", "rewards",
"status", "sentDate", "transactionHash" };

private static final String SHEET_NAME = "reward-detail";

private WalletAccountService walletAccountService;

private WalletTokenAdminService walletTokenAdminService;
Expand All @@ -84,6 +103,8 @@ public class WalletRewardReportService implements RewardReportService {

private RealizationService realizationService;

private ResourceBundleService resourceBundleService;

@Setter
private boolean rewardSendingInProgress;

Expand All @@ -94,12 +115,14 @@ public WalletRewardReportService(WalletAccountService walletAccountService,
WalletTokenAdminService walletTokenAdminService,
RewardSettingsService rewardSettingsService,
WalletRewardReportStorage rewardReportStorage,
RealizationService realizationService) {
RealizationService realizationService,
ResourceBundleService resourceBundleService) {
this.walletAccountService = walletAccountService;
this.walletTokenAdminService = walletTokenAdminService;
this.rewardSettingsService = rewardSettingsService;
this.rewardReportStorage = rewardReportStorage;
this.realizationService = realizationService;
this.resourceBundleService = resourceBundleService;
}

@Override
Expand Down Expand Up @@ -394,7 +417,36 @@ public void replaceRewardTransactions(String oldHash, String newHash) {
public Page<WalletReward> findWalletRewardsByPeriodIdAndStatus(long periodId, String status, ZoneId zoneId, Pageable pageable) {
boolean isValid = !status.equals("INVALID");
return rewardReportStorage.findWalletRewardsByPeriodIdAndStatus(periodId, isValid, zoneId, pageable);
}
}

@Override
public InputStream exportXlsx(long periodId,
String status,
ZoneId zoneId,
String fileName, Locale locale) {
File temp = null;
try { // NOSONAR
temp = createTempFile(fileName);
Page<WalletReward> walletRewardPage = findWalletRewardsByPeriodIdAndStatus(periodId, status, zoneId, null);
try (XSSFWorkbook workbook = new XSSFWorkbook(); FileOutputStream outputStream = new FileOutputStream(temp)) {
int rowIndex = 0;
CreationHelper helper = workbook.getCreationHelper();
Sheet sheet = workbook.createSheet(SHEET_NAME);
appendRewardsHeaderRow(sheet, rowIndex++, helper, locale);
for (WalletReward walletReward : walletRewardPage.getContent()) {
appendWalletRewardRow(sheet, rowIndex++, helper, walletReward);
}
workbook.write(outputStream);
}
return new FileInputStream(temp);
} catch (IOException e) {
throw new IllegalStateException("Error exporting XLSX file for wallet rewards ", e);
} finally {
if (temp != null && temp.exists()) {
temp.deleteOnExit();
}
}
}

@Override
public double countWalletRewardsPointsByPeriodIdAndStatus(long periodId, boolean isValid) {
Expand All @@ -405,6 +457,52 @@ public void setRewardSettingChanged(Map<Long, Boolean> updatedSettings) {
rewardSettingChanged.putAll(updatedSettings);
}

private File createTempFile(String fileName) throws IOException {
SimpleDateFormat formatter = new SimpleDateFormat("yy-MM-dd_HH-mm-ss");
fileName += formatter.format(new Date());
if (SystemUtils.IS_OS_UNIX) {
FileAttribute<Set<PosixFilePermission>> tempFileAttributes =
PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rw-------"));
return Files.createTempFile(fileName, ".xlsx", tempFileAttributes).toFile();
} else {
File temp = Files.createTempFile(fileName, ".xlsx").toFile();
if (!temp.setReadable(true, true) || !temp.setWritable(true, true)) {
throw new IllegalStateException("Can't write a temp file to export XLS achievements file");
}
return temp;
}
}

private void appendRewardsHeaderRow(Sheet sheet, int rowIndex, CreationHelper helper, Locale locale) {
Row row = sheet.createRow(rowIndex);
ResourceBundle resourceBundle = resourceBundleService.getResourceBundle("locale.addon.Wallet", locale);
for (int i = 0; i < COLUMNS.length; i++) {
row.createCell(i).setCellValue(helper.createRichTextString(resourceBundle.getString("wallet.administration.rewardDetails.label." + COLUMNS[i])));
}
}

private void appendWalletRewardRow(Sheet sheet, int rowIndex, CreationHelper helper, WalletReward walletReward) {
Row row = sheet.createRow(rowIndex);
try {
int cellIndex = 0;
row.createCell(cellIndex++).setCellValue(Utils.getUserFullName(String.valueOf(walletReward.getIdentityId())));
row.createCell(cellIndex++).setCellValue(walletReward.getWallet().getAddress());
row.createCell(cellIndex++).setCellValue(walletReward.getPoints());
row.createCell(cellIndex++).setCellValue(walletReward.getAmount());
row.createCell(cellIndex++).setCellValue(walletReward.getStatus());
appendTransactionCells(row, cellIndex, helper, walletReward.getTransaction());
} catch (Exception e) {
LOG.error("Error when computing to XLSX ", e);
}
}

private void appendTransactionCells(Row row, int cellIndex, CreationHelper helper, TransactionDetail transaction) {
if (transaction != null) {
row.createCell(cellIndex++).setCellValue(helper.createRichTextString(String.valueOf(new Date(transaction.getSentTimestamp()))));
row.createCell(cellIndex).setCellValue(transaction.getHash());
}
}

private RewardPeriod getRewardPeriod(LocalDate date) {
RewardSettings rewardSettings = rewardSettingsService.getSettings();
RewardPeriodType periodType = rewardSettings.getPeriodType();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
*/
package io.meeds.wallet.reward.service;

import java.io.InputStream;
import java.math.BigInteger;
import java.time.LocalDate;
import java.time.YearMonth;
Expand All @@ -27,9 +28,16 @@
import io.meeds.gamification.model.filter.RealizationFilter;
import io.meeds.gamification.service.RealizationService;

import io.meeds.gamification.utils.Utils;
import io.meeds.wallet.model.*;
import io.meeds.wallet.utils.WalletUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.usermodel.WorkbookFactory;
import org.exoplatform.container.PortalContainer;
import org.exoplatform.services.resources.ResourceBundleService;
import org.exoplatform.services.security.Identity;
import org.exoplatform.services.security.IdentityRegistry;
import org.exoplatform.services.security.MembershipEntry;
Expand All @@ -45,6 +53,7 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;

import static org.junit.Assert.assertThrows;
Expand Down Expand Up @@ -74,6 +83,9 @@ public class WalletRewardReportServiceTest { // NOSONAR
@MockBean
private RealizationService realizationService;

@MockBean
private ResourceBundleService resourceBundleService;

@Autowired
private RewardReportService rewardReportService;

Expand Down Expand Up @@ -368,6 +380,54 @@ void testListRewards() {
verify(rewardReportStorage, times(1)).countRewards(1);
}

@Test
void testExportRewards() throws Exception {
RewardSettings newSettings = new RewardSettings();

WalletReward walletReward = new WalletReward();
walletReward.setIdentityId(1);
walletReward.setPoints(200);
walletReward.setAmount(50);
Wallet wallet = new Wallet();
wallet.setAddress("address1");
walletReward.setWallet(new Wallet());
TransactionDetail transactionDetail = new TransactionDetail();
transactionDetail.setSucceeded(true);
transactionDetail.setHash("transactionHash");
transactionDetail.setSentTimestamp(new Date().getTime());
walletReward.setTransaction(transactionDetail);

when(rewardReportStorage.findWalletRewardsByPeriodIdAndStatus(1,
true,
newSettings.zoneId(),
null)).thenReturn(new PageImpl<>(List.of(walletReward)));
ResourceBundle resourceBundle = mock(ResourceBundle.class);
when(resourceBundle.getString(anyString())).thenReturn("header");
when(resourceBundleService.getResourceBundle(anyString(), any(Locale.class))).thenReturn(resourceBundle);

InputStream exportInputStream = rewardReportService.exportXlsx(1, "VALID", newSettings.zoneId(), "file-name", Locale.ENGLISH);
assertNotNull(exportInputStream);

Workbook workbook = WorkbookFactory.create(exportInputStream);
assertNotNull(workbook);
Sheet sheet = workbook.getSheetAt(0);
assertNotNull(sheet);
assertEquals(1, sheet.getLastRowNum());
Row header = sheet.getRow(0);
assertNotNull(header);
assertEquals(7, header.getLastCellNum());
assertTrue(StringUtils.isNotBlank(header.getCell(header.getFirstCellNum()).getStringCellValue()));
assertTrue(StringUtils.isNotBlank(header.getCell(header.getLastCellNum() - 1).getStringCellValue()));

Row row1 = sheet.getRow(1);
assertNotNull(row1);
assertEquals(7, row1.getLastCellNum());
assertEquals(200, row1.getCell(2).getNumericCellValue());
assertEquals(50, row1.getCell(3).getNumericCellValue());
assertEquals("success", row1.getCell(4).getStringCellValue());
assertEquals("transactionHash", row1.getCell(6).getStringCellValue());
}

protected Wallet newWallet(long identityId) {
Wallet wallet = new Wallet();
wallet.setTechnicalId(identityId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -567,12 +567,17 @@ wallet.administration.rewardDetails.label.status=Status
wallet.administration.rewardDetails.label.points=Points
wallet.administration.rewardDetails.label.rewards=Rewards
wallet.administration.rewardDetails.label.actions=Actions
wallet.administration.rewardDetails.label.fullName=Full name
wallet.administration.rewardDetails.label.address=Address
wallet.administration.rewardDetails.label.sentDate=Sent Date
wallet.administration.rewardDetails.label.transactionHash=Transaction hash
wallet.administration.rewardDetails.label.openTransaction=Open Transaction
wallet.administration.rewardDetails.label.seeHistory=See History
wallet.administration.rewardDetails.label.latestRewardsSent=Latest Rewards Sent on:
wallet.administration.rewardDetails.label.rewardsToSend={0} MEED for {1} points
wallet.administration.rewardDetails.label.reward=Reward
wallet.administration.rewardDetails.label.notSentYet=Not sent yet
wallet.administration.rewardDetails.label.export=Export
wallet.administration.rewardDetails.label=Rewards Details

wallet.overview.rewards.title=Wallet History
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,22 @@ export function computeRewardsByUser(date) {
});
}

export function exportXlsxUrl(paramsObj) {
const formData = new FormData();
if (paramsObj) {
Object.keys(paramsObj).forEach(key => {
const value = paramsObj[key];
if (window.Array && Array.isArray && Array.isArray(value)) {
value.forEach(val => formData.append(key, val));
} else {
formData.append(key, value);
}
});
}
const params = new URLSearchParams(formData).toString();
return `/wallet/rest/reward/export?${params}`;
}

export function getRewardsByUser(limit) {
return fetch(`/wallet/rest/reward/list?limit=${limit || 10}`, {
method: 'GET',
Expand Down
Loading

0 comments on commit bd6e9b0

Please sign in to comment.