Skip to content

Commit

Permalink
#230 - New OrderCancelled event and overhaul of event handling in gen…
Browse files Browse the repository at this point in the history
…eral.

We now publish an OrderCancelled event if an Order instance is cancelled, potentially roll back the inventory updates and create a compensating ProductPaymentEntry in the accountancy.

Fixed event handling in accountancy to handle OrderPaid events, not OrderCompleted ones. Polished implementation of InventoryOrderEventListener by tweaking the APIs in OrderCompletionReport for better composability.

Added Order.addOrderLine(…) as a replacement for ….add(OrderLine) and added lookup method to find OrderLine instances by product as the Order doesn't deduplicate them like the Cart does.

Added notes on the published events to state transitioning methods in OrderManager.
  • Loading branch information
odrotbohm committed Nov 23, 2018
1 parent 5682b0d commit 70668fa
Show file tree
Hide file tree
Showing 14 changed files with 437 additions and 60 deletions.
Expand Up @@ -109,4 +109,24 @@ public final Optional<LocalDateTime> getDate() {
public AccountancyEntryIdentifier getId() {
return accountancyEntryIdentifier;
}

/**
* Returns whether the entry is considered revenue, i.e. its value is zero or positive.
*
* @return
* @since 7.1
*/
public boolean isRevenue() {
return value.isPositiveOrZero();
}

/**
* Returns whether the entry is considered expense, i.e. its value is negative.
*
* @return
* @since 7.1
*/
public boolean isExpense() {
return value.isNegative();
}
}
Expand Up @@ -19,7 +19,9 @@
import lombok.RequiredArgsConstructor;

import org.salespointframework.order.Order;
import org.salespointframework.order.Order.OrderCancelled;
import org.salespointframework.order.Order.OrderCompleted;
import org.salespointframework.order.Order.OrderPaid;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
Expand All @@ -36,11 +38,37 @@ public class AccountancyOrderEventListener {

private final @NonNull Accountancy accountancy;

/**
* Creates a new revenue {@link ProductPaymentEntry} for the order that has been paid.
*
* @param event must not be {@literal null}.
*/
@EventListener
public void on(OrderCompleted event) {
public void on(OrderPaid event) {

Order order = event.getOrder();

accountancy.add(ProductPaymentEntry.of(order, "Rechnung Nr. ".concat(order.getId().toString())));
accountancy.add(ProductPaymentEntry.of(order, String.format("Rechnung Nr. %s", order.getId())));
}

/**
* Creates a counter {@link ProductPaymentEntry} for the order that is cancelled if there's a revenue entry for the
* given order already, i.e. the order has been paid before.
*
* @param event must not be {@literal null}.
* @since 7.1
*/
@EventListener
public void on(OrderCancelled event) {

Order order = event.getOrder();

if (accountancy.findAll().stream() //
.map(ProductPaymentEntry.class::cast) //
.anyMatch(it -> it.belongsTo(order) && it.isRevenue())) {

accountancy.add(ProductPaymentEntry.rollback(order,
String.format("Order %s cancelled! Reason: %s.", order.getId(), event.getReason())));
}
}
}
Expand Up @@ -28,6 +28,7 @@
import javax.persistence.Lob;
import javax.persistence.OneToOne;

import org.salespointframework.core.Currencies;
import org.salespointframework.order.Order;
import org.salespointframework.order.OrderIdentifier;
import org.salespointframework.payment.PaymentMethod;
Expand All @@ -44,7 +45,7 @@
*/
@Entity
@Getter
@ToString
@ToString(callSuper = true)
@NoArgsConstructor(force = true, access = AccessLevel.PRIVATE)
public class ProductPaymentEntry extends AccountancyEntry {

Expand All @@ -68,7 +69,26 @@ public class ProductPaymentEntry extends AccountancyEntry {
private @Lob PaymentMethod paymentMethod;

public static ProductPaymentEntry of(Order order, String description) {
return new ProductPaymentEntry(order.getId(), order.getUserAccount(), order.getTotalPrice(), description,
return new ProductPaymentEntry(order.getId(), order.getUserAccount(), order.getTotal(), description,
order.getPaymentMethod());
}

/**
* Creates a new {@link ProductPaymentEntry} that rolls back the payment for the given {@link Order}.
*
* @param order must not be {@literal null}.
* @param description must not be {@literal null}.
* @return
* @since 7.1
*/
public static ProductPaymentEntry rollback(Order order, String description) {

Assert.notNull(order, "Order must not be null!");
Assert.notNull(description, "Description must not be null!");

MonetaryAmount amount = Currencies.ZERO_EURO.subtract(order.getTotal());

return new ProductPaymentEntry(order.getId(), order.getUserAccount(), amount, description,
order.getPaymentMethod());
}

Expand Down Expand Up @@ -97,4 +117,17 @@ public ProductPaymentEntry(OrderIdentifier orderIdentifier, UserAccount userAcco
this.userAccount = userAccount;
this.paymentMethod = paymentMethod;
}

/**
* Returns whether the {@link ProductPaymentEntry} belongs to the given {@link Order}.
*
* @param order must not be {@literal null}.
* @return
*/
public boolean belongsTo(Order order) {

Assert.notNull(order, "Order must not be null!");

return this.orderIdentifier.equals(order.getId());
}
}
Expand Up @@ -92,7 +92,7 @@ public boolean hasSufficientQuantity(Quantity quantity) {
*
* @param quantity must not be {@literal null}.
*/
public void decreaseQuantity(Quantity quantity) {
public InventoryItem decreaseQuantity(Quantity quantity) {

Assert.notNull(quantity, "Quantity must not be null!");
Assert.isTrue(this.quantity.isGreaterThanOrEqualTo(quantity),
Expand All @@ -101,18 +101,22 @@ public void decreaseQuantity(Quantity quantity) {
product.verify(quantity);

this.quantity = this.quantity.subtract(quantity);

return this;
}

/**
* Increases the quantity of the current {@link InventoryItem} by the given {@link Quantity}.
*
* @param quantity must not be {@literal null}.
*/
public void increaseQuantity(Quantity quantity) {
public InventoryItem increaseQuantity(Quantity quantity) {

Assert.notNull(quantity, "Quantity must not be null!");
product.verify(quantity);

this.quantity = this.quantity.add(quantity);

return this;
}
}
Expand Up @@ -27,6 +27,7 @@
import org.salespointframework.catalog.Product;
import org.salespointframework.catalog.ProductIdentifier;
import org.salespointframework.order.Order;
import org.salespointframework.order.Order.OrderCancelled;
import org.salespointframework.order.Order.OrderCompleted;
import org.salespointframework.order.OrderCompletionFailure;
import org.salespointframework.order.OrderCompletionReport;
Expand All @@ -47,7 +48,7 @@
*/
@Component
@RequiredArgsConstructor
class InventoryOrderEventListener {
public class InventoryOrderEventListener {

private static final String NOT_ENOUGH_STOCK = "Number of items requested by the OrderLine is greater than the number available in the Inventory. Please re-stock.";
private static final String NO_INVENTORY_ITEM = "No inventory item with given product indentifier found in inventory. Have you initialized your inventory? Do you need to re-stock it?";
Expand All @@ -74,11 +75,27 @@ public void on(OrderCompleted event) throws OrderCompletionFailure {
.map(this::verify)//
.collect(Collectors.toList());

OrderCompletionReport report = OrderCompletionReport.forCompletions(order, collect);
OrderCompletionReport.forCompletions(order, collect) //
.onError(OrderCompletionFailure::new);
}

/**
* Rolls back the stock decreases handled for {@link OrderCompleted} events.
*
* @param event must not be {@literal null}.
*/
@EventListener
public void on(OrderCancelled event) {

Order order = event.getOrder();

if (report.hasErrors()) {
throw new OrderCompletionFailure(report);
if (!order.isCompleted()) {
return;
}

order.getOrderLines() //
.map(this::updateStockFor) //
.forEach(inventory::save);
}

/**
Expand All @@ -91,24 +108,32 @@ private OrderLineCompletion verify(OrderLine orderLine) {

Assert.notNull(orderLine, "OrderLine must not be null!");

if (LineItemFilter.shouldBeHandled(orderLine, filters)) {
if (!LineItemFilter.shouldBeHandled(orderLine, filters)) {
return OrderLineCompletion.success(orderLine);
}

ProductIdentifier identifier = orderLine.getProductIdentifier();
Optional<InventoryItem> inventoryItem = inventory.findByProductIdentifier(identifier);

ProductIdentifier identifier = orderLine.getProductIdentifier();
Optional<InventoryItem> inventoryItem = inventory.findByProductIdentifier(identifier);
return inventoryItem //
.map(it -> it.hasSufficientQuantity(orderLine.getQuantity())) //
.map(sufficient -> sufficient ? success(orderLine) : error(orderLine, NOT_ENOUGH_STOCK)) //
.orElse(error(orderLine, NO_INVENTORY_ITEM)) //
.onSuccess(it -> {

OrderLineCompletion completion = inventoryItem //
.map(it -> it.hasSufficientQuantity(orderLine.getQuantity())) //
.map(sufficient -> sufficient ? success(orderLine) : error(orderLine, NOT_ENOUGH_STOCK)) //
.orElse(error(orderLine, NO_INVENTORY_ITEM));
inventoryItem //
.map(item -> item.decreaseQuantity(it.getQuantity())) //
.ifPresent(inventory::save);
});
}

if (!completion.isFailure()) {
inventoryItem.ifPresent(it -> it.decreaseQuantity(orderLine.getQuantity()));
}
private InventoryItem updateStockFor(OrderLine orderLine) {

return completion;
ProductIdentifier productIdentifier = orderLine.getProductIdentifier();

} else {
return OrderLineCompletion.success(orderLine);
}
return inventory.findByProductIdentifier(productIdentifier) //
.orElseThrow(() -> new IllegalArgumentException(
String.format("Couldn't find InventoryItem for product %s!", productIdentifier))) //
.increaseQuantity(orderLine.getQuantity());
}
}

0 comments on commit 70668fa

Please sign in to comment.