332 lines
14 KiB
Java
332 lines
14 KiB
Java
package org.hso.ecommerce.controller.cronjob;
|
|
|
|
import java.sql.Timestamp;
|
|
import java.util.Calendar;
|
|
import java.util.Collections;
|
|
import java.util.GregorianCalendar;
|
|
import java.util.HashMap;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Map.Entry;
|
|
|
|
import javax.annotation.PostConstruct;
|
|
|
|
import org.hso.ecommerce.action.booking.CreateBookingAction;
|
|
import org.hso.ecommerce.action.cronjob.ReadSupplierDataAction;
|
|
import org.hso.ecommerce.action.cronjob.ReadSupplierDataAction.ArticleIdentifier;
|
|
import org.hso.ecommerce.action.cronjob.ReorderAction;
|
|
import org.hso.ecommerce.action.cronjob.UpdateOffersAction;
|
|
import org.hso.ecommerce.entities.booking.Booking;
|
|
import org.hso.ecommerce.entities.booking.BookingAccountEntry;
|
|
import org.hso.ecommerce.entities.booking.BookingReason;
|
|
import org.hso.ecommerce.entities.cron.BackgroundJob;
|
|
import org.hso.ecommerce.entities.shop.Article;
|
|
import org.hso.ecommerce.entities.supplier.ArticleOffer;
|
|
import org.hso.ecommerce.entities.supplier.Supplier;
|
|
import org.hso.ecommerce.entities.supplier.SupplierOrder;
|
|
import org.hso.ecommerce.repos.booking.BookingAccountEntryRepository;
|
|
import org.hso.ecommerce.repos.booking.BookingRepository;
|
|
import org.hso.ecommerce.repos.cronjob.BackgroundJobRepository;
|
|
import org.hso.ecommerce.repos.shop.ArticleRepository;
|
|
import org.hso.ecommerce.repos.shop.CustomerOderRepository;
|
|
import org.hso.ecommerce.repos.supplier.ArticleOfferRepository;
|
|
import org.hso.ecommerce.repos.supplier.SupplierOrderRepository;
|
|
import org.hso.ecommerce.repos.supplier.SupplierRepository;
|
|
import org.hso.ecommerce.repos.warehouse.WarehouseBookingPositionSlotEntryRepository;
|
|
import org.slf4j.Logger;
|
|
import org.slf4j.LoggerFactory;
|
|
import org.springframework.beans.factory.annotation.Autowired;
|
|
import org.springframework.stereotype.Component;
|
|
|
|
interface ICronjob {
|
|
/**
|
|
* Calculate the earliest cronjob execution time that happens after the given reference time.
|
|
*
|
|
* @param reference Position in time to start searching. The implementor is allowed to modify the reference time.
|
|
* @return A new Calendar instance (or the same) containing the time for next execution.
|
|
*/
|
|
Calendar nextExecution(Calendar reference);
|
|
|
|
/**
|
|
* Calculate the latest cronjob execution time that happens before or exactly at the given refernce time.
|
|
*
|
|
* @param reference Position in time to start searching. The implementor is allowed to modify the reference time.
|
|
* @return A new Calendar instance (or the same) containing the time of the last execution.
|
|
*/
|
|
Calendar previousExecution(Calendar reference);
|
|
|
|
/**
|
|
* Execute this cronjob.
|
|
*
|
|
* @param time The point in time this execution was scheduled. In case of a missed cronjob, the actual time of
|
|
* this call might be much later.
|
|
* @param controller Back-reference that allows to use repositories.
|
|
*/
|
|
void executeAt(Calendar time, CronjobController controller);
|
|
}
|
|
|
|
@Component
|
|
class Reorder implements ICronjob {
|
|
@Override
|
|
public Calendar nextExecution(Calendar reference) {
|
|
if (reference.get(Calendar.HOUR_OF_DAY) >= 8) {
|
|
reference.add(Calendar.DAY_OF_MONTH, 1);
|
|
}
|
|
reference.set(Calendar.HOUR_OF_DAY, 8);
|
|
reference.set(Calendar.MINUTE, 0);
|
|
reference.set(Calendar.SECOND, 0);
|
|
reference.set(Calendar.MILLISECOND, 0);
|
|
return reference;
|
|
}
|
|
|
|
@Override
|
|
public Calendar previousExecution(Calendar reference) {
|
|
if (reference.get(Calendar.HOUR_OF_DAY) < 8) {
|
|
reference.add(Calendar.DAY_OF_MONTH, -1);
|
|
}
|
|
reference.set(Calendar.HOUR_OF_DAY, 8);
|
|
reference.set(Calendar.MINUTE, 0);
|
|
reference.set(Calendar.SECOND, 0);
|
|
reference.set(Calendar.MILLISECOND, 0);
|
|
return reference;
|
|
}
|
|
|
|
/**
|
|
* Calculates the amount of ordered articles by customers for the given article type in the time between begin and
|
|
* end.
|
|
*
|
|
* @param article The article to search orders for.
|
|
* @param begin The start time for the search (included)
|
|
* @param end The end time for the search (excluded)
|
|
* @return The number of articles that were ordered by customers in the given range.
|
|
*/
|
|
private Integer getOrderedAmounts(Article article, Calendar begin, Calendar end, CronjobController controller) {
|
|
return controller.customerOrderRepository.countOrdersOfArticleInTimespan(
|
|
article.id,
|
|
new Timestamp(begin.getTimeInMillis()), new Timestamp(end.getTimeInMillis()));
|
|
}
|
|
|
|
/**
|
|
* Calculates the amount of ordered articles by customers for the given article type in the three days before the
|
|
* given reference time. The return-array contains 3 fields: Index 0: Orders 72 to 48 hours ago; Index 1: Orders 48
|
|
* to 24 hours ago; Index 2: Orders 24 to 0 hours ago.
|
|
*
|
|
* @param article The article for which the customer orders are checked.
|
|
* @param time The reference time to use for calculation of the last orders.
|
|
* @return A 3-element array containing the orders of the last three days.
|
|
*/
|
|
private Integer[] getOrderedAmounts(Article article, Calendar time, CronjobController controller) {
|
|
Calendar oneDayBefore = (Calendar) time.clone();
|
|
oneDayBefore.add(Calendar.DAY_OF_MONTH, -1);
|
|
Calendar twoDaysBefore = (Calendar) time.clone();
|
|
twoDaysBefore.add(Calendar.DAY_OF_MONTH, -2);
|
|
Calendar threeDaysBefore = (Calendar) time.clone();
|
|
threeDaysBefore.add(Calendar.DAY_OF_MONTH, -3);
|
|
|
|
return new Integer[] { //
|
|
getOrderedAmounts(article, threeDaysBefore, twoDaysBefore, controller), //
|
|
getOrderedAmounts(article, twoDaysBefore, oneDayBefore, controller), //
|
|
getOrderedAmounts(article, oneDayBefore, time, controller), //
|
|
};
|
|
}
|
|
|
|
private HashMap<ArticleIdentifier, ArticleOffer> mapArticleOffers(List<ArticleOffer> articleOffers) {
|
|
HashMap<ArticleIdentifier, ArticleOffer> map = new HashMap<>();
|
|
for (ArticleOffer articleOffer : articleOffers) {
|
|
ArticleIdentifier identifier = new ArticleIdentifier(articleOffer.manufacturer, articleOffer.articleNumber);
|
|
map.put(identifier, articleOffer);
|
|
}
|
|
return map;
|
|
}
|
|
|
|
@Override
|
|
public void executeAt(Calendar time, CronjobController controller) {
|
|
List<Supplier> suppliers = controller.supplierRepository.findAll();
|
|
ReadSupplierDataAction.Result supplierData = new ReadSupplierDataAction(suppliers).finish();
|
|
|
|
// Save the new offers in the database
|
|
List<ArticleOffer> allOffers = controller.articleOfferRepository.findAll();
|
|
allOffers = new UpdateOffersAction(allOffers, supplierData.cheapestOffer).finish();
|
|
controller.articleOfferRepository.saveAll(allOffers);
|
|
|
|
HashMap<ArticleIdentifier, ArticleOffer> mappedOffers = mapArticleOffers(allOffers);
|
|
|
|
// Reorder
|
|
List<Article> allArticles = controller.articleRepository.findAll();
|
|
for (Article article : allArticles) {
|
|
Integer[] orderedAmounts = getOrderedAmounts(article, time, controller);
|
|
|
|
Integer undeliveredReorders = controller.supplierOrderRepository
|
|
.countUndeliveredReorders(article.related.articleNumber);
|
|
|
|
int amountInStock = controller.warehouseBookingPositionSlotEntryRepository.getArticleStock(article.id)
|
|
.orElse(0);
|
|
|
|
ReorderAction action = new ReorderAction(article, orderedAmounts,
|
|
undeliveredReorders,
|
|
amountInStock,
|
|
supplierData.cheapestOffer, mappedOffers);
|
|
SupplierOrder order = action.finish();
|
|
if (order != null) {
|
|
controller.supplierOrderRepository.save(order);
|
|
|
|
// Create bookings for this order
|
|
int netPrice = order.totalPriceNet;
|
|
int vatPercent = order.ordered.vatPercent;
|
|
int vatAmount = netPrice * vatPercent / 100;
|
|
int grossPrice = netPrice + vatAmount;
|
|
|
|
// Obligation towards the supplier
|
|
BookingAccountEntry mainAccount = controller.bookingAccountEntryRepository.getByMain()
|
|
.orElseGet(BookingAccountEntry::newMain);
|
|
BookingAccountEntry supplierAccount = controller.bookingAccountEntryRepository
|
|
.getBySupplier(order.supplier.id)
|
|
.orElseGet(() -> BookingAccountEntry.newSupplier(order.supplier));
|
|
BookingReason obligationReason = new BookingReason(order);
|
|
Booking obligationBooking = new CreateBookingAction(mainAccount,
|
|
supplierAccount,
|
|
obligationReason,
|
|
grossPrice).finish();
|
|
controller.bookingRepository.save(obligationBooking);
|
|
|
|
// Input Tax
|
|
BookingAccountEntry vatAccount = controller.bookingAccountEntryRepository.getByVat()
|
|
.orElseGet(BookingAccountEntry::newVat);
|
|
mainAccount = controller.bookingAccountEntryRepository.getByMain().get();
|
|
BookingReason inputTaxReason = new BookingReason(order);
|
|
Booking inputTaxBooking = new CreateBookingAction(vatAccount, mainAccount, inputTaxReason, vatAmount)
|
|
.finish();
|
|
controller.bookingRepository.save(inputTaxBooking);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
class ScheduledCronjob {
|
|
public final Calendar executionTime;
|
|
public final ICronjob cronjob;
|
|
public final BackgroundJob model;
|
|
|
|
public ScheduledCronjob(Calendar executionTime, ICronjob cronjob, BackgroundJob model) {
|
|
this.executionTime = executionTime;
|
|
this.cronjob = cronjob;
|
|
this.model = model;
|
|
}
|
|
}
|
|
|
|
@Component
|
|
class CronjobController {
|
|
private static final Logger log = LoggerFactory.getLogger(CronjobController.class);
|
|
|
|
private static final Map<String, ICronjob> cronjobs = getCronjobs();
|
|
|
|
@Autowired
|
|
private final BackgroundJobRepository cronjobRepository = null;
|
|
|
|
@Autowired
|
|
final ArticleRepository articleRepository = null;
|
|
|
|
@Autowired
|
|
final ArticleOfferRepository articleOfferRepository = null;
|
|
|
|
@Autowired
|
|
final CustomerOderRepository customerOrderRepository = null;
|
|
|
|
@Autowired
|
|
final BookingRepository bookingRepository = null;
|
|
|
|
@Autowired
|
|
final BookingAccountEntryRepository bookingAccountEntryRepository = null;
|
|
|
|
@Autowired
|
|
final WarehouseBookingPositionSlotEntryRepository warehouseBookingPositionSlotEntryRepository = null;
|
|
|
|
@Autowired
|
|
final SupplierRepository supplierRepository = null;
|
|
|
|
@Autowired
|
|
final SupplierOrderRepository supplierOrderRepository = null;
|
|
|
|
private static Map<String, ICronjob> getCronjobs() {
|
|
HashMap<String, ICronjob> map = new HashMap<>();
|
|
|
|
// Register all existing cronjobs
|
|
map.put(BackgroundJob.JOB_REORDER, new Reorder());
|
|
|
|
return Collections.unmodifiableMap(map);
|
|
}
|
|
|
|
private ScheduledCronjob getNextCronjob() {
|
|
Calendar currentTime = new GregorianCalendar();
|
|
Iterable<BackgroundJob> jobs = cronjobRepository.findAll();
|
|
HashMap<String, BackgroundJob> alreadyExecuted = new HashMap<>();
|
|
for (BackgroundJob job : jobs) {
|
|
alreadyExecuted.put(job.jobName, job);
|
|
}
|
|
ScheduledCronjob earliestJob = null;
|
|
for (Entry<String, ICronjob> entry : cronjobs.entrySet()) {
|
|
ScheduledCronjob resultingJob;
|
|
BackgroundJob dbEntry = alreadyExecuted.get(entry.getKey());
|
|
if (dbEntry != null) {
|
|
Calendar previousExecution = new GregorianCalendar();
|
|
previousExecution.setTimeInMillis(dbEntry.lastExecution.getTime());
|
|
Calendar followingSchedule = entry.getValue().nextExecution((Calendar) previousExecution.clone());
|
|
Calendar lastSchedule = entry.getValue().previousExecution((Calendar) currentTime.clone());
|
|
if (lastSchedule.getTimeInMillis() > followingSchedule.getTimeInMillis()) {
|
|
// This happens, if more than one execution was missed.
|
|
// In this case, run the job only once.
|
|
followingSchedule = lastSchedule;
|
|
}
|
|
resultingJob = new ScheduledCronjob(followingSchedule, entry.getValue(), dbEntry);
|
|
} else {
|
|
// This cronjob has never been executed before.
|
|
Calendar lastScheduleTime = entry.getValue().previousExecution((Calendar) currentTime.clone());
|
|
BackgroundJob model = new BackgroundJob();
|
|
model.jobName = entry.getKey();
|
|
resultingJob = new ScheduledCronjob(lastScheduleTime, entry.getValue(), model);
|
|
}
|
|
|
|
// Look for the job with earliest executionTime - it will run next
|
|
if (earliestJob == null
|
|
|| resultingJob.executionTime.getTimeInMillis() < earliestJob.executionTime.getTimeInMillis()) {
|
|
earliestJob = resultingJob;
|
|
}
|
|
}
|
|
return earliestJob;
|
|
}
|
|
|
|
private void runCronjobExecutionLoop() {
|
|
Thread.currentThread().setName("Cronjob");
|
|
try {
|
|
while (true) {
|
|
ScheduledCronjob nextJob = getNextCronjob();
|
|
if (nextJob == null) {
|
|
// In case there are no cronjobs
|
|
return;
|
|
}
|
|
long waitingTime = nextJob.executionTime.getTimeInMillis() - System.currentTimeMillis();
|
|
if (waitingTime > 0) {
|
|
Thread.sleep(waitingTime);
|
|
}
|
|
|
|
try {
|
|
nextJob.cronjob.executeAt(nextJob.executionTime, this);
|
|
} catch (Throwable t) {
|
|
log.error("Failed to execute cronjob " + nextJob.cronjob.getClass() + ":");
|
|
t.printStackTrace();
|
|
}
|
|
|
|
nextJob.model.lastExecution = new Timestamp(nextJob.executionTime.getTimeInMillis());
|
|
cronjobRepository.save(nextJob.model);
|
|
}
|
|
} catch (InterruptedException e) {
|
|
log.error("The cronjob execution thread has been interrupted");
|
|
}
|
|
}
|
|
|
|
@PostConstruct
|
|
public void onPostConstruct() {
|
|
new Thread(this::runCronjobExecutionLoop).start();
|
|
}
|
|
}
|