DefaultIBizProjectImporter.java

/*
 * #%L
 * *********************************************************************************************************************
 *
 * blueHour
 * http://bluehour.tidalwave.it - git clone git@bitbucket.org:tidalwave/bluehour-src.git
 * %%
 * Copyright (C) 2013 - 2023 Tidalwave s.a.s. (http://tidalwave.it)
 * %%
 * *********************************************************************************************************************
 *
 * 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.
 *
 * *********************************************************************************************************************
 *
 *
 * *********************************************************************************************************************
 * #L%
 */
package it.tidalwave.accounting.importer.ibiz.impl;

import javax.annotation.Nonnull;
import java.util.List;
import java.util.stream.Stream;
import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import it.tidalwave.util.NotFoundException;
import it.tidalwave.accounting.importer.ibiz.spi.IBizProjectImporter;
import it.tidalwave.accounting.model.CustomerRegistry;
import it.tidalwave.accounting.model.JobEvent;
import it.tidalwave.accounting.model.JobEventGroup;
import it.tidalwave.accounting.model.types.Money;
import it.tidalwave.accounting.model.ProjectRegistry;
import it.tidalwave.accounting.model.spi.TimedJobEventSpi;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import static java.util.stream.Collectors.toList;

/***********************************************************************************************************************
 *
 * @author  Fabrizio Giudici
 *
 **********************************************************************************************************************/
@Slf4j @RequiredArgsConstructor
public class DefaultIBizProjectImporter implements IBizProjectImporter
  {
    @Nonnull
    private final CustomerRegistry customerRegistry;

    @Nonnull
    private final ProjectRegistry projectRegistry;

    @Nonnull
    private final Path path;

    /*******************************************************************************************************************
     *
     * Imports the projects.
     *
     ******************************************************************************************************************/
    @Override
    public void importProjects()
      throws IOException
      {
        log.debug("importProjects()");
        Files.walkFileTree(path.resolve("Projects"), new SimpleFileVisitor<>()
          {
            @Override
            public FileVisitResult visitFile (@Nonnull final Path file, @Nonnull final BasicFileAttributes attrs)
                    throws IOException
              {
                if (!".DS_Store".equals(file.toFile().getName()))
                  {
                    importProject(file);
                  }

                return FileVisitResult.CONTINUE;
              }
          });
      }
    
    /*******************************************************************************************************************
     *
     * @throws  IOException  in case of error
     * 
     ******************************************************************************************************************/
    private void importProject (@Nonnull final Path file)
      throws IOException
      {
        try
          {
            importProject(IBizUtils.loadConfiguration(file));
          }
        catch (NotFoundException e)
          {
            throw new IOException(e);
          }
      }

    /*******************************************************************************************************************
     *
     * Imports a project from the given configuration object.
     * 
     * @param  projectConfig  the configuration object
     *
     ******************************************************************************************************************/
    private void importProject (@Nonnull final ConfigurationDecorator projectConfig)
      throws NotFoundException
      {
        final var customerId = projectConfig.getId("clientIdentifier");
        final var customer = customerRegistry.findCustomers().withId(customerId).result();
        final var status = IBizProjectStatus.values()[projectConfig.getInt("projectStatus")];

        if (status.getMappedStatus() == null)
          {
            log.warn("IGNORING PROJECT {} with status {}", projectConfig.getString("projectName"), status);  
          }
        else
          {
            final var jobEvents = importJobEvents(projectConfig.getStream("jobEvents"));
            projectRegistry.addProject().withId(projectConfig.getId("uniqueIdentifier"))
                                        .withBudget(projectConfig.getMoney("projectEstimate"))
                                        .withCustomer(customer)
                                        .withName(projectConfig.getString("projectName"))
    //                                           .withDescription("description of project 1")
                                        .withStartDate(projectConfig.getDate("projectStartDate"))
                                        .withEndDate(projectConfig.getDate("projectDueDate"))
                                        .withNotes(projectConfig.getString("projectNotes"))
                                        .withNumber(projectConfig.getString("projectNumber"))
                                        .withStatus(status.getMappedStatus())
                                        .withHourlyRate(getHourlyRate(projectConfig, jobEvents))
                                        .withEvents(jobEvents)
                                        .create();
          }
      }

    /*******************************************************************************************************************
     *
     * Retrieves the hourly rates - if missing from the project description, tries to recover it from the first 
     * meaningful job event.
     *
     ******************************************************************************************************************/
    @Nonnull
    private Money getHourlyRate(final ConfigurationDecorator projectConfig, final List<? extends JobEvent> jobEvents)
      throws NotFoundException 
      {
        var hourlyRate = projectConfig.getMoney("projectRate");
      
        if ((hourlyRate.compareTo(Money.ZERO) == 0) && !jobEvents.isEmpty())
            // don't use equals() - see http://stackoverflow.com/questions/6787142/bigdecimal-equals-versus-compareto
                                        {
                                          var event = jobEvents.get(0);
            
            while ((event instanceof JobEventGroup) && ((JobEventGroup)event).findChildren().count() > 0)
              {
                event = ((JobEventGroup)event).findChildren().firstResult();
              }
            
            if (event instanceof TimedJobEventSpi)
              {
                hourlyRate = ((TimedJobEventSpi)event).getHourlyRate();
              }
          }
        return hourlyRate;
      }

    /*******************************************************************************************************************
     *
     * Imports the job events.
     *
     ******************************************************************************************************************/
    @Nonnull
    private List<JobEvent> importJobEvents (@Nonnull final Stream<? extends ConfigurationDecorator> jobEventsConfig)
      {
        return jobEventsConfig.map(this::importJobEvent).collect(toList());
      }

    /*******************************************************************************************************************
     *
     * Imports a single job event.
     *
     ******************************************************************************************************************/
    @Nonnull
    private JobEvent importJobEvent (@Nonnull final ConfigurationDecorator jobEventConfig)
      {
//        log.debug(">>>> properties: {}", toList(jobEvent.getKeys()));

//        final boolean checkedOut = jobEvent.getBoolean("checkedout");
//        final boolean isExpense = jobEvent.getBoolean("isExpense");
//        final boolean nonBillable = jobEvent.getBoolean("nonBillable");
//        final int taxable = jobEvent.getInt("taxable");
//        final DateTime lastModifiedDate = jobEvent.getDateTime("lastModifiedDate");
//        final int paid = jobEvent.getInt("jobEventPaid");
        final var type = IBizJobEventType.values()[jobEventConfig.getInt("jobEventType")];

        return JobEvent.builder().withId(jobEventConfig.getId("uniqueIdentifier"))
                                 .withType(type.getMappedType())
                                 .withStartDateTime(jobEventConfig.getDateTime("jobEventStartDate"))
                                 .withEndDateTime(jobEventConfig.getDateTime("jobEventEndDate"))
                                 .withName(jobEventConfig.getString("jobEventName"))
                                 .withDescription(jobEventConfig.getString("jobEventNotes"))
                                 .withHourlyRate(jobEventConfig.getMoney("jobEventRate"))
                                 .withEarnings(jobEventConfig.getMoney("jobEventEarnings"))
                                 .withEvents(importJobEvents(jobEventConfig.getStream("children")))
                                 .create();
        /*
                                        <key>tax1</key>
                                        <real>22</real>

         */
      }
  }