001// --------------------------------------------------------------------------------
002// Copyright 2002-2024 Echo Three, LLC
003//
004// Licensed under the Apache License, Version 2.0 (the "License");
005// you may not use this file except in compliance with the License.
006// You may obtain a copy of the License at
007//
008//     http://www.apache.org/licenses/LICENSE-2.0
009//
010// Unless required by applicable law or agreed to in writing, software
011// distributed under the License is distributed on an "AS IS" BASIS,
012// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013// See the License for the specific language governing permissions and
014// limitations under the License.
015// --------------------------------------------------------------------------------
016
017package com.echothree.model.control.period.server.logic;
018
019import com.echothree.model.control.party.server.control.PartyControl;
020import com.echothree.model.control.period.common.PeriodConstants;
021import com.echothree.model.control.period.server.control.PeriodControl;
022import com.echothree.model.data.party.common.pk.PartyPK;
023import com.echothree.model.data.party.server.entity.Language;
024import com.echothree.model.data.party.server.entity.PartyCompany;
025import com.echothree.model.data.period.server.entity.Period;
026import com.echothree.model.data.period.server.entity.PeriodKind;
027import com.echothree.model.data.period.server.entity.PeriodKindDescription;
028import com.echothree.model.data.period.server.entity.PeriodType;
029import com.echothree.model.data.period.server.entity.PeriodTypeDescription;
030import com.echothree.util.common.message.ExecutionErrors;
031import com.echothree.util.server.message.ExecutionErrorAccumulator;
032import com.echothree.util.server.persistence.EntityPermission;
033import com.echothree.util.server.persistence.Session;
034import java.time.ZoneId;
035import java.time.ZonedDateTime;
036import java.util.Formatter;
037import java.util.List;
038import java.util.Locale;
039
040public class FiscalPeriodLogic {
041
042    private FiscalPeriodLogic() {
043        super();
044    }
045
046    private static class FiscalPeriodLogicHolder {
047        static FiscalPeriodLogic instance = new FiscalPeriodLogic();
048    }
049
050    public static FiscalPeriodLogic getInstance() {
051        return FiscalPeriodLogicHolder.instance;
052    }
053    
054    private void createMonth(final PeriodKind periodKind, final Period quarterPeriod, final ZonedDateTime yearStart, final int month,
055            final PartyPK createdBy) {
056        var periodControl = Session.getModelController(PeriodControl.class);
057        int year = yearStart.getYear();
058        StringBuilder periodName = new StringBuilder().append(year).append('_').append('M');
059        PeriodType monthPeriodType = periodControl.getPeriodTypeByName(periodKind, PeriodConstants.PeriodType_MONTH);
060        ZonedDateTime monthStart = yearStart.withMonth(month);
061        ZonedDateTime monthEnd = monthStart.plusMonths(1).minusNanos(1);
062        
063        new Formatter(periodName, Locale.US).format("%02d", month);
064        Period monthPeriod = PeriodLogic.getInstance().createPeriod(periodKind, periodName.toString(), quarterPeriod, monthPeriodType,
065                monthStart.toInstant().toEpochMilli(), monthEnd.toInstant().toEpochMilli(), createdBy);
066        
067        List<PeriodKindDescription> periodKindDescriptions = periodControl.getPeriodKindDescriptionsByPeriodKind(periodKind);
068        PeriodType yearPeriodType = periodControl.getPeriodTypeByName(periodKind, PeriodConstants.PeriodType_YEAR);
069        periodKindDescriptions.forEach((periodKindDescription) -> {
070            Language language = periodKindDescription.getLanguage();
071            PeriodTypeDescription yearPeriodTypeDescription = periodControl.getPeriodTypeDescription(yearPeriodType, language);
072            if (yearPeriodTypeDescription != null) {
073                PeriodTypeDescription monthPeriodTypeDescription = periodControl.getPeriodTypeDescription(monthPeriodType, language);
074                if (monthPeriodTypeDescription != null) {
075                    StringBuilder description = new StringBuilder(periodKindDescription.getDescription())
076                            .append(' ').append(yearPeriodTypeDescription.getDescription())
077                            .append(' ').append(year)
078                            .append(' ').append(monthPeriodTypeDescription.getDescription())
079                            .append(' ').append(month);
080
081                    periodControl.createPeriodDescription(monthPeriod, language, description.toString(), createdBy);
082                }
083            }
084        });
085    }
086    
087    private void createQuarter(final PeriodKind periodKind, final Period yearPeriod, final ZonedDateTime yearStart, final int quarter,
088            final PartyPK createdBy) {
089        var periodControl = Session.getModelController(PeriodControl.class);
090        int year = yearStart.getYear();
091        String periodName = new StringBuilder().append(year).append('_').append('Q').append(quarter).toString();
092        PeriodType quarterPeriodType = periodControl.getPeriodTypeByName(periodKind, PeriodConstants.PeriodType_QUARTER);
093        int monthOfQuarterStart = 1 + (quarter - 1) * 3;
094        ZonedDateTime quarterStart = yearStart.withMonth(monthOfQuarterStart);
095        ZonedDateTime quarterEnd = quarterStart.plusMonths(3).minusNanos(1);
096        Period quarterPeriod = PeriodLogic.getInstance().createPeriod(periodKind, periodName, yearPeriod, quarterPeriodType,
097                quarterStart.toInstant().toEpochMilli(), quarterEnd.toInstant().toEpochMilli(), createdBy);
098        
099        List<PeriodKindDescription> periodKindDescriptions = periodControl.getPeriodKindDescriptionsByPeriodKind(periodKind);
100        PeriodType yearPeriodType = periodControl.getPeriodTypeByName(periodKind, PeriodConstants.PeriodType_YEAR);
101        periodKindDescriptions.forEach((periodKindDescription) -> {
102            Language language = periodKindDescription.getLanguage();
103            PeriodTypeDescription yearPeriodTypeDescription = periodControl.getPeriodTypeDescription(yearPeriodType, language);
104            if (yearPeriodTypeDescription != null) {
105                PeriodTypeDescription quarterPeriodTypeDescription = periodControl.getPeriodTypeDescription(quarterPeriodType, language);
106                if (quarterPeriodTypeDescription != null) {
107                    StringBuilder description = new StringBuilder(periodKindDescription.getDescription())
108                            .append(' ').append(yearPeriodTypeDescription.getDescription())
109                            .append(' ').append(year)
110                            .append(' ').append(quarterPeriodTypeDescription.getDescription())
111                            .append(' ').append(quarter);
112
113                    periodControl.createPeriodDescription(quarterPeriod, language, description.toString(), createdBy);
114                }
115            }
116        });
117
118        for(int monthOffset = 0; monthOffset < 3; monthOffset++) {
119            createMonth(periodKind, quarterPeriod, yearStart, monthOfQuarterStart + monthOffset, createdBy);
120        }
121    }
122    
123    private Period createYear(final ExecutionErrorAccumulator eea, final int year, final ZoneId zone, final PartyPK createdBy) {
124        var periodControl = Session.getModelController(PeriodControl.class);
125        String periodName = new StringBuilder().append(year).toString();
126        PeriodKind periodKind = periodControl.getPeriodKindByName(PeriodConstants.PeriodKind_FISCAL);
127        Period yearPeriod = periodControl.getPeriodByName(periodKind, periodName);
128
129        if(yearPeriod == null) {
130            PeriodType periodType = periodControl.getPeriodTypeByName(periodKind, PeriodConstants.PeriodType_YEAR);
131            ZonedDateTime yearStart = ZonedDateTime.of(year, 1, 1, 0, 0, 0, 0, zone);
132            ZonedDateTime yearEnd = yearStart.plusYears(1).minusNanos(1);
133
134            yearPeriod = PeriodLogic.getInstance().createPeriod(periodKind, periodName, null, periodType,
135                    yearStart.toInstant().toEpochMilli(), yearEnd.toInstant().toEpochMilli(), createdBy);
136            
137            List<PeriodKindDescription> periodKindDescriptions = periodControl.getPeriodKindDescriptionsByPeriodKind(periodKind);
138            for(var periodKindDescription : periodKindDescriptions) {
139                Language language = periodKindDescription.getLanguage();
140                PeriodTypeDescription periodTypeDescription = periodControl.getPeriodTypeDescription(periodType, language);
141                
142                if(periodTypeDescription != null) {
143                    StringBuilder description = new StringBuilder(periodKindDescription.getDescription())
144                            .append(' ').append(periodTypeDescription.getDescription())
145                            .append(' ').append(year);
146                    
147                    periodControl.createPeriodDescription(yearPeriod, language, description.toString(), createdBy);
148                }
149            }
150            
151            for(int quarter = 1; quarter < 5; quarter++) {
152                createQuarter(periodKind, yearPeriod, yearStart, quarter, createdBy);
153            }
154        } else {
155            eea.addExecutionError(ExecutionErrors.DuplicatePeriodName.name(), PeriodConstants.PeriodKind_FISCAL, periodName);
156        }
157        
158        return yearPeriod;
159    }
160    
161    public Period createFiscalYear(final ExecutionErrorAccumulator eea, final Integer year, final PartyPK createdBy) {
162        var partyControl = Session.getModelController(PartyControl.class);
163        PartyCompany defaultPartyCompany = partyControl.getDefaultPartyCompany();
164        Period fiscalYear = null;
165        
166        if(defaultPartyCompany != null) {
167            String javaTimeZoneName = partyControl.getPreferredTimeZone(defaultPartyCompany.getParty()).getLastDetail().getJavaTimeZoneName();
168            ZoneId zone = ZoneId.of(javaTimeZoneName);
169            
170            fiscalYear = createYear(eea, year, zone, createdBy);
171        } else {
172            eea.addExecutionError(ExecutionErrors.MissingDefaultCompany.name());
173        }
174        
175        return fiscalYear;
176    }
177    
178    private Period getFiscalPeriodByName(final ExecutionErrorAccumulator eea, final String periodName,
179            final EntityPermission entityPermission) {
180        var periodControl = Session.getModelController(PeriodControl.class);
181        PeriodKind periodKind = periodControl.getPeriodKindByName(PeriodConstants.PeriodKind_FISCAL);
182        Period period = periodControl.getPeriodByName(periodKind, periodName, entityPermission);
183        
184        if(periodKind != null) {
185            period = periodControl.getPeriodByName(periodKind, periodName, entityPermission);
186            
187            if(period == null) {
188                eea.addExecutionError(ExecutionErrors.UnknownPeriodName.name(), PeriodConstants.PeriodKind_FISCAL, periodName);
189            }
190        } else {
191            eea.addExecutionError(ExecutionErrors.UnknownPeriodKindName.name(), PeriodConstants.PeriodKind_FISCAL);
192        }
193        
194        return period;
195    }
196    
197    public Period getFiscalPeriodByName(final ExecutionErrorAccumulator eea, final String periodName) {
198        return getFiscalPeriodByName(eea, periodName, EntityPermission.READ_ONLY);
199    }
200    
201    public Period getFiscalPeriodByNameForUpdate(final ExecutionErrorAccumulator eea, final String periodName) {
202        return getFiscalPeriodByName(eea, periodName, EntityPermission.READ_WRITE);
203    }
204    
205}