/* * Copyright 2011-2014 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package pl.com.bottega.ecommerce.sales.application.impl; import javax.inject.Inject; import org.springframework.transaction.annotation.Isolation; import org.springframework.transaction.annotation.Transactional; import pl.com.bottega.ddd.annotations.application.ApplicationService; import pl.com.bottega.ecommerce.canonicalmodel.publishedlanguage.AggregateId; import pl.com.bottega.ecommerce.sales.application.api.command.OrderDetailsCommand; import pl.com.bottega.ecommerce.sales.application.api.service.OfferChangedExcpetion; import pl.com.bottega.ecommerce.sales.application.api.service.OrderingService; import pl.com.bottega.ecommerce.sales.domain.client.Client; import pl.com.bottega.ecommerce.sales.domain.client.ClientRepository; import pl.com.bottega.ecommerce.sales.domain.equivalent.SuggestionService; import pl.com.bottega.ecommerce.sales.domain.offer.DiscountFactory; import pl.com.bottega.ecommerce.sales.domain.offer.DiscountPolicy; import pl.com.bottega.ecommerce.sales.domain.offer.Offer; import pl.com.bottega.ecommerce.sales.domain.payment.Payment; import pl.com.bottega.ecommerce.sales.domain.payment.PaymentRepository; import pl.com.bottega.ecommerce.sales.domain.productscatalog.Product; import pl.com.bottega.ecommerce.sales.domain.productscatalog.ProductRepository; import pl.com.bottega.ecommerce.sales.domain.purchase.Purchase; import pl.com.bottega.ecommerce.sales.domain.purchase.PurchaseFactory; import pl.com.bottega.ecommerce.sales.domain.purchase.PurchaseRepository; import pl.com.bottega.ecommerce.sales.domain.reservation.Reservation; import pl.com.bottega.ecommerce.sales.domain.reservation.ReservationFactory; import pl.com.bottega.ecommerce.sales.domain.reservation.ReservationRepository; import pl.com.bottega.ecommerce.sharedkernel.exceptions.DomainOperationException; import pl.com.bottega.ecommerce.system.application.SystemContext; /** * Ordering Use Case steps<br> * Each step is a Domain Story<br> * <br> * Notice that application language is different (simpler) than domain language, ex: we don'nt want to exposure domain concepts like Purchase and Reservation to the upper layers, we hide them under the Order term * <br> * Technically App Service is just a bunch of procedures, therefore OO principles (ex: CqS, SOLID, GRASP) does not apply here * * @author Slawek */ @ApplicationService public class OrderingServiceImpl implements OrderingService { @Inject private SystemContext systemContext; @Inject private ClientRepository clientRepository; @Inject private ReservationRepository reservationRepository; @Inject private ReservationFactory reservationFactory; @Inject private PurchaseFactory purchaseFactory; @Inject private PurchaseRepository purchaseRepository; @Inject private ProductRepository productRepository; @Inject private PaymentRepository paymentRepository; @Inject private DiscountFactory discountFactory; @Inject private SuggestionService suggestionService; // @Secured requires BUYER role public AggregateId createOrder() { Reservation reservation = reservationFactory.create(loadClient()); reservationRepository.save(reservation); return reservation.getAggregateId(); } /** * DOMAIN STORY<br> * try to read this as a full sentence, this way: subject.predicate(completion)<br> * <br> * Load reservation by orderId<br> * Load product by productId<br> * Check if product is not available<br> * -if so, than suggest equivalent for that product based on client<br> * Reservation add product by given quantity */ @Override public void addProduct(AggregateId orderId, AggregateId productId, int quantity) { Reservation reservation = reservationRepository.load(orderId); Product product = productRepository.load(productId); if (! product.isAvailabe()){ Client client = loadClient(); product = suggestionService.suggestEquivalent(product, client); } reservation.add(product, quantity); reservationRepository.save(reservation); } /** * Can be invoked many times for the same order (with different params).<br> * Offer VO is not stored in the Repo, it is stored on the Client Tier instead. */ public Offer calculateOffer(AggregateId orderId) { Reservation reservation = reservationRepository.load(orderId); DiscountPolicy discountPolicy = discountFactory.create(loadClient()); /* * Sample pattern: Aggregate generates Value Object using function<br> * Higher order function is closured by policy */ return reservation.calculateOffer(discountPolicy); } /** * DOMAIN STORY<br> * try to read this as a full sentence, this way: subject.predicate(completion)<br> * <br> * Load reservation by orderId<br> * Check if reservation is closed - if so, than Error<br> * Generate new offer from reservation using discount created per client<br> * Check if new offer is not the same as seen offer using delta = 5<br> * Create purchase per client based on seen offer<br> * Check if client can not afford total cost of purchase - if so, than Error<br> * Confirm purchase<br> * Close reservation<br> */ @Override @Transactional(isolation = Isolation.SERIALIZABLE)//highest isolation needed because of manipulating many Aggregates public void confirm(AggregateId orderId, OrderDetailsCommand orderDetailsCommand, Offer seenOffer) throws OfferChangedExcpetion { Reservation reservation = reservationRepository.load(orderId); if (reservation.isClosed()) throw new DomainOperationException(reservation.getAggregateId(), "reservation is already closed"); /* * Sample pattern: Aggregate generates Value Object using function<br> * Higher order function is closured by policy */ Offer newOffer = reservation.calculateOffer( discountFactory.create(loadClient())); /* * Sample pattern: Client Tier sends back old VOs, Server generates new VOs based on Aggregate state<br> * Notice that this VO is not stored in Repo, it's stored on the Client Tier. */ if (! newOffer.sameAs(seenOffer, 5))//TODO load delta from conf. throw new OfferChangedExcpetion(reservation.getAggregateId(), seenOffer, newOffer); Client client = loadClient();//create per logged client, not reservation owner Purchase purchase = purchaseFactory.create(reservation.getAggregateId(), client, seenOffer); if (! client.canAfford(purchase.getTotalCost())) throw new DomainOperationException(client.getAggregateId(), "client has insufficent money"); purchaseRepository.save(purchase);//Aggregate must be managed by persistence context before firing events (synchronous listeners may need to load it) /* * Sample model where one aggregate creates another. Client does not manage payment lifecycle, therefore application must manage it. */ Payment payment = client.charge(purchase.getTotalCost()); paymentRepository.save(payment); purchase.confirm(); reservation.close(); reservationRepository.save(reservation); clientRepository.save(client); } private Client loadClient() { return clientRepository.load(systemContext.getSystemUser().getClientId()); } }