001// --------------------------------------------------------------------------------
002// Copyright 2002-2026 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.control.user.period.common.spec.FiscalPeriodUniversalSpec;
020import com.echothree.model.control.core.common.ComponentVendors;
021import com.echothree.model.control.core.common.EntityTypes;
022import com.echothree.model.control.core.common.exception.InvalidParameterCountException;
023import com.echothree.model.control.core.server.logic.EntityInstanceLogic;
024import com.echothree.model.control.party.server.control.PartyControl;
025import com.echothree.model.control.period.common.PeriodConstants;
026import com.echothree.model.control.period.common.exception.InvalidPeriodKindNameException;
027import com.echothree.model.control.period.server.control.PeriodControl;
028import com.echothree.model.data.party.common.pk.PartyPK;
029import com.echothree.model.data.period.server.entity.Period;
030import com.echothree.model.data.period.server.entity.PeriodKind;
031import com.echothree.model.data.period.server.entity.PeriodType;
032import com.echothree.util.common.message.ExecutionErrors;
033import com.echothree.util.server.control.BaseLogic;
034import com.echothree.util.server.message.ExecutionErrorAccumulator;
035import com.echothree.util.server.persistence.EntityPermission;
036import java.time.ZoneId;
037import java.time.ZonedDateTime;
038import java.util.Formatter;
039import java.util.Locale;
040import javax.enterprise.context.ApplicationScoped;
041import javax.inject.Inject;
042
043@ApplicationScoped
044public class FiscalPeriodLogic
045        extends BaseLogic {
046
047    @Inject
048    PartyControl partyControl;
049
050    @Inject
051    PeriodControl periodControl;
052
053    @Inject
054    EntityInstanceLogic entityInstanceLogic;
055
056    @Inject
057    PeriodKindLogic periodKindLogic;
058
059    @Inject
060    PeriodLogic periodLogic;
061
062    protected FiscalPeriodLogic() {
063        super();
064    }
065    
066    private void createMonth(final PeriodKind periodKind, final Period quarterPeriod, final ZonedDateTime yearStart, final int month,
067            final PartyPK createdBy) {
068        var year = yearStart.getYear();
069        var periodName = new StringBuilder().append(year).append('_').append('M');
070        var monthPeriodType = periodControl.getPeriodTypeByName(periodKind, PeriodConstants.PeriodType_MONTH);
071        var monthStart = yearStart.withMonth(month);
072        var monthEnd = monthStart.plusMonths(1).minusNanos(1);
073        
074        new Formatter(periodName, Locale.US).format("%02d", month);
075        var monthPeriod = periodLogic.createPeriod(periodKind, periodName.toString(), quarterPeriod, monthPeriodType,
076                monthStart.toInstant().toEpochMilli(), monthEnd.toInstant().toEpochMilli(), createdBy);
077
078        var periodKindDescriptions = periodControl.getPeriodKindDescriptionsByPeriodKind(periodKind);
079        var yearPeriodType = periodControl.getPeriodTypeByName(periodKind, PeriodConstants.PeriodType_YEAR);
080        periodKindDescriptions.forEach((periodKindDescription) -> {
081            var language = periodKindDescription.getLanguage();
082            var yearPeriodTypeDescription = periodControl.getPeriodTypeDescription(yearPeriodType, language);
083            if (yearPeriodTypeDescription != null) {
084                var monthPeriodTypeDescription = periodControl.getPeriodTypeDescription(monthPeriodType, language);
085                if (monthPeriodTypeDescription != null) {
086                    var description = periodKindDescription.getDescription() +
087                            ' ' + yearPeriodTypeDescription.getDescription() +
088                            ' ' + year +
089                            ' ' + monthPeriodTypeDescription.getDescription() +
090                            ' ' + month;
091
092                    periodControl.createPeriodDescription(monthPeriod, language, description, createdBy);
093                }
094            }
095        });
096    }
097    
098    private void createQuarter(final PeriodKind periodKind, final Period yearPeriod, final ZonedDateTime yearStart, final int quarter,
099            final PartyPK createdBy) {
100        var year = yearStart.getYear();
101        var periodName = String.valueOf(year) + '_' + 'Q' + quarter;
102        var quarterPeriodType = periodControl.getPeriodTypeByName(periodKind, PeriodConstants.PeriodType_QUARTER);
103        var monthOfQuarterStart = 1 + (quarter - 1) * 3;
104        var quarterStart = yearStart.withMonth(monthOfQuarterStart);
105        var quarterEnd = quarterStart.plusMonths(3).minusNanos(1);
106        var quarterPeriod = periodLogic.createPeriod(periodKind, periodName, yearPeriod, quarterPeriodType,
107                quarterStart.toInstant().toEpochMilli(), quarterEnd.toInstant().toEpochMilli(), createdBy);
108
109        var periodKindDescriptions = periodControl.getPeriodKindDescriptionsByPeriodKind(periodKind);
110        var yearPeriodType = periodControl.getPeriodTypeByName(periodKind, PeriodConstants.PeriodType_YEAR);
111        periodKindDescriptions.forEach((periodKindDescription) -> {
112            var language = periodKindDescription.getLanguage();
113            var yearPeriodTypeDescription = periodControl.getPeriodTypeDescription(yearPeriodType, language);
114            if (yearPeriodTypeDescription != null) {
115                var quarterPeriodTypeDescription = periodControl.getPeriodTypeDescription(quarterPeriodType, language);
116                if (quarterPeriodTypeDescription != null) {
117                    var description = periodKindDescription.getDescription() +
118                            ' ' + yearPeriodTypeDescription.getDescription() +
119                            ' ' + year +
120                            ' ' + quarterPeriodTypeDescription.getDescription() +
121                            ' ' + quarter;
122
123                    periodControl.createPeriodDescription(quarterPeriod, language, description, createdBy);
124                }
125            }
126        });
127
128        for(var monthOffset = 0; monthOffset < 3; monthOffset++) {
129            createMonth(periodKind, quarterPeriod, yearStart, monthOfQuarterStart + monthOffset, createdBy);
130        }
131    }
132    
133    private Period createYear(final ExecutionErrorAccumulator eea, final Period perpetualPeriod, final int year,
134            final ZoneId zone, final PartyPK createdBy) {
135        var periodName = String.valueOf(year);
136        var periodKind = periodControl.getPeriodKindByName(PeriodConstants.PeriodKind_FISCAL);
137        var yearPeriod = periodControl.getPeriodByName(periodKind, periodName);
138
139        if(yearPeriod == null) {
140            var periodType = periodControl.getPeriodTypeByName(periodKind, PeriodConstants.PeriodType_YEAR);
141            var yearStart = ZonedDateTime.of(year, 1, 1, 0, 0, 0, 0, zone);
142            var yearEnd = yearStart.plusYears(1).minusNanos(1);
143
144            yearPeriod = periodLogic.createPeriod(periodKind, periodName, perpetualPeriod, periodType,
145                    yearStart.toInstant().toEpochMilli(), yearEnd.toInstant().toEpochMilli(), createdBy);
146
147            var periodKindDescriptions = periodControl.getPeriodKindDescriptionsByPeriodKind(periodKind);
148            for(var periodKindDescription : periodKindDescriptions) {
149                var language = periodKindDescription.getLanguage();
150                var periodTypeDescription = periodControl.getPeriodTypeDescription(periodType, language);
151                
152                if(periodTypeDescription != null) {
153                    var description = periodKindDescription.getDescription() +
154                            ' ' + periodTypeDescription.getDescription() +
155                            ' ' + year;
156                    
157                    periodControl.createPeriodDescription(yearPeriod, language, description, createdBy);
158                }
159            }
160            
161            for(var quarter = 1; quarter < 5; quarter++) {
162                createQuarter(periodKind, yearPeriod, yearStart, quarter, createdBy);
163            }
164        } else {
165            eea.addExecutionError(ExecutionErrors.DuplicatePeriodName.name(), PeriodConstants.PeriodKind_FISCAL, periodName);
166        }
167        
168        return yearPeriod;
169    }
170
171    public Period ensurePerpetual(final ExecutionErrorAccumulator eea, final PartyPK createdBy) {
172        var periodKind = periodControl.getPeriodKindByName(PeriodConstants.PeriodKind_FISCAL);
173        var perpetualPeriod = periodControl.getPeriodByName(periodKind, PeriodConstants.Period_PERPETUAL);
174
175        if(perpetualPeriod == null) {
176            var periodType = periodControl.getPeriodTypeByName(periodKind, PeriodConstants.PeriodType_PERPETUAL);
177
178            perpetualPeriod = periodLogic.createPeriod(periodKind, PeriodConstants.Period_PERPETUAL, null,
179                    periodType, 0L, Long.MAX_VALUE, createdBy);
180
181        }
182
183        return perpetualPeriod;
184    }
185    
186    public Period createFiscalYear(final ExecutionErrorAccumulator eea, final Integer year, final PartyPK createdBy) {
187        var defaultPartyCompany = partyControl.getDefaultPartyCompany();
188        Period fiscalYear = null;
189        
190        if(defaultPartyCompany != null) {
191            var perpetualPeriod = ensurePerpetual(eea, createdBy);
192            var javaTimeZoneName = partyControl.getPreferredTimeZone(defaultPartyCompany.getParty()).getLastDetail().getJavaTimeZoneName();
193            var zone = ZoneId.of(javaTimeZoneName);
194            
195            fiscalYear = createYear(eea, perpetualPeriod, year, zone, createdBy);
196        } else {
197            eea.addExecutionError(ExecutionErrors.MissingDefaultCompany.name());
198        }
199        
200        return fiscalYear;
201    }
202    
203    private Period getFiscalPeriodByName(final ExecutionErrorAccumulator eea, final String periodName,
204            final EntityPermission entityPermission) {
205        var periodKind = periodControl.getPeriodKindByName(PeriodConstants.PeriodKind_FISCAL);
206        var period = periodControl.getPeriodByName(periodKind, periodName, entityPermission);
207        
208        if(periodKind != null) {
209            period = periodControl.getPeriodByName(periodKind, periodName, entityPermission);
210            
211            if(period == null) {
212                eea.addExecutionError(ExecutionErrors.UnknownPeriodName.name(), PeriodConstants.PeriodKind_FISCAL, periodName);
213            }
214        } else {
215            eea.addExecutionError(ExecutionErrors.UnknownPeriodKindName.name(), PeriodConstants.PeriodKind_FISCAL);
216        }
217        
218        return period;
219    }
220    
221    public Period getFiscalPeriodByName(final ExecutionErrorAccumulator eea, final String periodName) {
222        return getFiscalPeriodByName(eea, periodName, EntityPermission.READ_ONLY);
223    }
224    
225    public Period getFiscalPeriodByNameForUpdate(final ExecutionErrorAccumulator eea, final String periodName) {
226        return getFiscalPeriodByName(eea, periodName, EntityPermission.READ_WRITE);
227    }
228    
229    public PeriodKind getFiscalPeriodKind(final ExecutionErrorAccumulator eea) {
230        return periodKindLogic.getPeriodKindByName(eea, PeriodConstants.PeriodKind_FISCAL);
231    }
232
233    public Period getFiscalPeriodByUniversalSpec(final ExecutionErrorAccumulator eea,
234            final FiscalPeriodUniversalSpec spec, final EntityPermission entityPermission) {
235        var periodKind = getFiscalPeriodKind(eea);
236        Period period = null;
237
238        if(!hasExecutionErrors(eea)) {
239            var periodName = spec.getPeriodName();
240            var entitySpecCount = entityInstanceLogic.countPossibleEntitySpecs(spec);
241
242            if(entitySpecCount == 0 && periodName != null) {
243                if(!hasExecutionErrors(eea)) {
244                    period = periodLogic.getPeriodByName(eea, periodKind, periodName, entityPermission);
245                }
246            } else if(entitySpecCount == 1 && periodName == null) {
247                var instance = entityInstanceLogic.getEntityInstance(eea, spec, ComponentVendors.ECHO_THREE.name(), EntityTypes.Period.name());
248
249                if(!hasExecutionErrors(eea)) {
250                    period = periodControl.getPeriodByEntityInstance(instance, entityPermission);
251                    var actualPeriodKind = period.getLastDetail().getPeriodKind();
252
253                    if(!actualPeriodKind.equals(getFiscalPeriodKind(eea))) {
254                        handleExecutionError(InvalidPeriodKindNameException.class, eea, ExecutionErrors.InvalidPeriodKindName.name(), periodKind.getLastDetail().getPeriodKindName());
255                        period = null;
256                    }
257                }
258            } else {
259                handleExecutionError(InvalidParameterCountException.class, eea, ExecutionErrors.InvalidParameterCount.name());
260            }
261        }
262
263        return period;
264    }
265
266}