Skip to content

Commit

Permalink
Merge pull request #465 from JulesFleuren/master
Browse files Browse the repository at this point in the history
Issues with sales export (closes #464)
  • Loading branch information
07joshua03 authored Jan 26, 2024
2 parents 9f69989 + 19a962d commit 1117fe3
Show file tree
Hide file tree
Showing 7 changed files with 235 additions and 67 deletions.
Original file line number Diff line number Diff line change
@@ -1,38 +1,30 @@
package ch.wisv.events.admin.controller;

import ch.wisv.events.core.model.order.PaymentMethod;
import ch.wisv.events.core.admin.TreasurerData;
import ch.wisv.events.core.admin.SalesExportSubmission;
import ch.wisv.events.utils.LdapGroup;
import ch.wisv.events.admin.utils.AggregatedProduct;


import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.*;
import java.io.InputStream;
import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
import com.google.common.collect.Lists;

import ch.wisv.events.core.repository.OrderRepository;
import org.apache.commons.lang3.tuple.ImmutableTriple;
import org.apache.commons.lang3.tuple.Triple;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.javatuples.Septet;

import org.springframework.core.io.InputStreamResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;


/**
Expand Down Expand Up @@ -87,55 +79,9 @@ public HttpEntity<? extends Object> csvExport(@ModelAttribute SalesExportSubmiss

List<TreasurerData> treasurerData = orderRepository.findallPaymentsByMonth(SalesExportSubmission.getMonth(), SalesExportSubmission.getYear(), paymentMethods, SalesExportSubmission.isFreeProductsIncluded());

ListIterator<TreasurerData> listIterator = treasurerData.listIterator(treasurerData.size());

// Loop through all orders in TreasurerData and add orders of the same product together

// Key: Product ID, Value: Septet with: event title, organized by, product title, total income, amount, vatRate, price
Map<Integer, Septet<String, Integer, String, Double, Integer, String, Double>> map = new HashMap<>();

while (listIterator.hasPrevious()) {
TreasurerData data = listIterator.previous();
Map<Integer, AggregatedProduct> map = aggregateOrders(treasurerData);

if (!map.containsKey(data.getProductId())) {
map.put(
data.getProductId(),
Septet.with(
data.getEventTitle(),
data.getOrganizedBy(),
data.getProductTitle(),
data.getPrice(),
data.getAmount(),
data.getVatRate(),
data.getPrice()));
} else {
Double totalIncome = map.get(data.getProductId()).getValue3();
Integer totalAmount = map.get(data.getProductId()).getValue4();
map.put(
data.getProductId(),
Septet.with(
data.getEventTitle(),
data.getOrganizedBy(),
data.getProductTitle(),
totalIncome + data.getPrice(),
totalAmount + data.getAmount(),
data.getVatRate(),
data.getPrice()));
}
}

// String csvContent = "Options:\n" + SalesExportSubmission.toString() + "\n\n";
// csvContent += "Event;Organized by;Product;Total income;Total amount;VAT rate\n";
String csvContent = "Event;Organized by;Product;Total income;Total amount;VAT rate;price\n";
for (Map.Entry<Integer, Septet<String, Integer, String, Double, Integer, String, Double>> entry : map.entrySet()) {
csvContent += entry.getValue().getValue0() // event title
+ ";" + LdapGroup.intToString(entry.getValue().getValue1()) // organized by
+ ";" + entry.getValue().getValue2() // product title
+ ";" + entry.getValue().getValue3() // total income
+ ";" + entry.getValue().getValue4() // total amount
+ ";" + entry.getValue().getValue5() // vat rate
+ ";" + entry.getValue().getValue6() + "\n"; // price
}
String csvContent = generateCsvContent(map);

InputStream bufferedInputStream = new ByteArrayInputStream(csvContent.getBytes(StandardCharsets.UTF_8));
InputStreamResource fileInputStream = new InputStreamResource(bufferedInputStream);
Expand All @@ -154,4 +100,73 @@ public HttpEntity<? extends Object> csvExport(@ModelAttribute SalesExportSubmiss
HttpStatus.OK
);
}

/**
* Go through all orders in a given month and add the total amount and income together in Aggregated product
*
* @param treasurerData containing all orders that have to be aggregated
*
* @return map with entry per product, with the product id as key
*/
public static Map<Integer, AggregatedProduct> aggregateOrders(List<TreasurerData> treasurerData) {

ListIterator<TreasurerData> listIterator = treasurerData.listIterator(treasurerData.size());

// Loop through all orders in treasurerData and add orders of the same product together in AggregatedProduct
// Key: Product ID, Value: Aggregated product
Map<Integer, AggregatedProduct> map = new HashMap<>();

while (listIterator.hasPrevious()) {
TreasurerData data = listIterator.previous();

if (!map.containsKey(data.getProductId())) {
AggregatedProduct product = new AggregatedProduct();

product.eventTitle = data.getEventTitle();
product.organizedBy = LdapGroup.intToString(data.getOrganizedBy());
product.productTitle = data.getProductTitle();
product.totalIncome = data.getPrice();
product.totalAmount = data.getAmount();
product.vatRate = data.getVatRate();
product.price = data.getPrice();

map.put(data.getProductId(),product);

} else {
AggregatedProduct product = map.get(data.getProductId());
product.totalIncome += data.getPrice()*data.getAmount();
product.totalAmount += data.getAmount();
map.put(data.getProductId(),product);
}
}

return map;
}




/**
* Format all aggregated products into string in csv format
*
* @param map containing all aggregated orders.
*
* @return string that should be written to csv file
*/
public static String generateCsvContent(Map<Integer, AggregatedProduct> map) {

// String csvContent = "Options:\n" + SalesExportSubmission.toString() + "\n\n";
// csvContent += "Event;Organized by;Product;Total income;Total amount;VAT rate\n";
StringBuilder csvContent = new StringBuilder("Event;Organized by;Product;Total income;Total amount;VAT rate;price\n");
for (Map.Entry<Integer, AggregatedProduct> entry : map.entrySet()) {
csvContent.append(entry.getValue().eventTitle)
.append(";").append(entry.getValue().organizedBy) // organized by
.append(";").append(entry.getValue().productTitle) // product title
.append(";").append(String.format("%.2f", entry.getValue().totalIncome)) // total income
.append(";").append(entry.getValue().totalAmount) // total amount
.append(";").append(entry.getValue().vatRate) // vat rate
.append(";").append(String.format("%.2f", entry.getValue().price)).append("\n"); // price
}
return csvContent.toString();
}
}
16 changes: 16 additions & 0 deletions src/main/java/ch/wisv/events/admin/utils/AggregatedProduct.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package ch.wisv.events.admin.utils;


/**
* AggregatedProduct class. Is used to store all information about a single product
* when adding the sales over a month together in the Sales Export tab
*/
public class AggregatedProduct {
public String eventTitle;
public String organizedBy;
public String productTitle;
public Double totalIncome;
public Integer totalAmount;
public String vatRate;
public Double price;
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import lombok.Setter;
import java.util.*;
import java.time.LocalDate;
import java.time.LocalDate;
import com.google.common.collect.Lists;


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import java.util.List;
import java.util.Optional;
import java.util.Collection;
import java.time.LocalDate;

import ch.wisv.events.core.admin.TreasurerData;
import org.springframework.data.jpa.repository.JpaRepository;
Expand Down
2 changes: 2 additions & 0 deletions src/main/resources/templates/admin/salesexport/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ <h6 class="card-header">Month</h6>
type="number"
class="form-control"
th:field="*{month}"
max="12"
min="1"
/></label>
</div>
</div>
Expand Down
18 changes: 18 additions & 0 deletions src/test/java/ch/wisv/events/ControllerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,24 @@ protected Order createOrder(Customer customer, List<Product> products, OrderStat
return order;
}

protected Order createOrderSingleProduct(Customer customer, Product product, Long amount, OrderStatus status, String createdBy) {
Order order = new Order();
order.setOwner(customer);
order.setStatus(status);
order.setCreatedBy(createdBy);
order.setPaymentMethod(PaymentMethod.IDEAL);


OrderProduct orderProduct = new OrderProduct(product, product.getCost(), amount);
orderProductRepository.saveAndFlush(orderProduct);
order.addOrderProduct(orderProduct);


orderRepository.saveAndFlush(order);

return order;
}

protected OrderProduct createOrderProduct(Product product) {
OrderProduct orderProduct = new OrderProduct();
orderProduct.setProduct(product);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,24 @@
import ch.wisv.events.core.model.order.Order;
import ch.wisv.events.core.model.order.OrderStatus;
import ch.wisv.events.core.model.product.Product;
import ch.wisv.events.core.admin.TreasurerData;
import ch.wisv.events.admin.utils.AggregatedProduct;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import java.time.LocalDate;
import java.util.Map;
import java.util.TreeMap;
import org.json.simple.JSONObject;


import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.commons.lang3.tuple.ImmutableTriple;
import org.apache.commons.lang3.tuple.Triple;

import java.util.*;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
Expand All @@ -35,6 +34,126 @@
@ActiveProfiles("test")
public class DashboardSalesExportControllerTest extends ControllerTest {

@Test
public void testIndex() throws Exception {
mockMvc.perform(get("/administrator/salesexport"))
.andExpect(status().is2xxSuccessful())
.andExpect(view().name("admin/salesexport/index"));
}

@Test
public void testAggregateOrders() throws Exception {
Event event = this.createEvent();
Customer customer = this.createCustomer();

Product product1 = this.createProduct();
product1.setCost(0.10d);
event.addProduct(product1);

Order order1 = this.createOrderSingleProduct(customer, product1, 2L, OrderStatus.PENDING, "test");
orderService.updateOrderStatus(order1, OrderStatus.PAID);
Order order2 = this.createOrderSingleProduct(customer, product1, 1L, OrderStatus.PENDING, "test");
orderService.updateOrderStatus(order2, OrderStatus.PAID);

Product product2 = this.createProduct();
product2.setCost(13.13d);
event.addProduct(product2);

Order order3 = this.createOrderSingleProduct(customer, product2, 1L, OrderStatus.PENDING, "test");
orderService.updateOrderStatus(order3, OrderStatus.PAID);
// this last order should not be in the aggregated product, because OrderStatus is not set to PAID
this.createOrderSingleProduct(customer, product2, 1L, OrderStatus.PENDING, "test");

List<Integer> paymentMethods = new ArrayList<>();
paymentMethods.add(order1.getPaymentMethod().toInt());
List<TreasurerData> treasurerData = orderRepository.findallPaymentsByMonth(LocalDate.now().getMonthValue(), LocalDate.now().getYear(), paymentMethods, false);

Map<Integer, AggregatedProduct> map = DashboardSalesExportController.aggregateOrders(treasurerData);


assertTrue(map.containsKey(product1.getId()));
AggregatedProduct aggrProduct1 = map.get(product1.getId());
assertEquals(event.toString(), aggrProduct1.eventTitle);
assertEquals(event.getOrganizedBy().getName(), aggrProduct1.organizedBy);
assertEquals(product1.getTitle(), aggrProduct1.productTitle);
assertEquals((Double) (3*(product1.getCost())), aggrProduct1.totalIncome);
assertEquals((Integer) 3, aggrProduct1.totalAmount);
assertEquals(product1.getVatRate().name(), aggrProduct1.vatRate);
assertEquals(product1.getCost(), aggrProduct1.price);

AggregatedProduct aggrProduct2 = map.get(product2.getId());
assertEquals(product2.getCost(), aggrProduct2.totalIncome);
assertEquals((Integer) 1, aggrProduct2.totalAmount);


}

@Test
public void testGenerateCsvContent() throws Exception {
// Mostly test if amounts are correctly rounded to 2 decimals

Event event = this.createEvent();

Product product1 = this.createProduct();
product1.setCost(0.10d);
event.addProduct(product1);

AggregatedProduct aggregatedProduct1 = new AggregatedProduct();
aggregatedProduct1.eventTitle = event.getTitle();
aggregatedProduct1.organizedBy = event.getOrganizedBy().getName();
aggregatedProduct1.productTitle = product1.getTitle();
aggregatedProduct1.vatRate = product1.getVatRate().name();
aggregatedProduct1.price = product1.getCost();
aggregatedProduct1.totalIncome = product1.getCost();
aggregatedProduct1.totalAmount = 1;

aggregatedProduct1.totalAmount += 2;
aggregatedProduct1.totalIncome += 2*product1.getCost();

Product product2 = this.createProduct();
product2.setCost(0.30d);
event.addProduct(product1);

AggregatedProduct aggregatedProduct2 = new AggregatedProduct();
aggregatedProduct2.eventTitle = event.getTitle();
aggregatedProduct2.organizedBy = event.getOrganizedBy().getName();
aggregatedProduct2.productTitle = product2.getTitle();
aggregatedProduct2.vatRate = product2.getVatRate().name();
aggregatedProduct2.price = product2.getCost();
aggregatedProduct2.totalIncome = 128.1;
aggregatedProduct2.totalAmount = 123;

aggregatedProduct2.totalAmount += 0;
aggregatedProduct2.totalIncome += 0.2;

Map<Integer, AggregatedProduct> map = new HashMap<>();

map.put(product1.getId(), aggregatedProduct1);
map.put(product2.getId(), aggregatedProduct2);

String csvContent = DashboardSalesExportController.generateCsvContent(map);

String expectedLine1 = event.getTitle()
+ ";" + event.getOrganizedBy().getName()
+ ";" + product1.getTitle()
+ ";" + "0.30" // total income
+ ";" + "3" // total amount
+ ";" + product1.getVatRate().name()
+ ";" + "0.10";

String expectedLine2 = event.getTitle()
+ ";" + event.getOrganizedBy().getName()
+ ";" + product2.getTitle()
+ ";" + "128.30" // total income
+ ";" + "123" // total amount
+ ";" + product2.getVatRate().name()
+ ";" + "0.30";

assertTrue(csvContent.contains(expectedLine1));
assertTrue(csvContent.contains(expectedLine2));

}

@Test
public void testCsvExport() throws Exception {
Product product = this.createProduct();
Expand Down

0 comments on commit 1117fe3

Please sign in to comment.