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.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.period.server.entity.Period;
024import com.echothree.model.data.period.server.entity.PeriodKind;
025import com.echothree.util.common.message.ExecutionErrors;
026import com.echothree.util.server.message.ExecutionErrorAccumulator;
027import com.echothree.util.server.persistence.EntityPermission;
028import java.time.ZoneId;
029import java.time.ZonedDateTime;
030import java.util.Formatter;
031import java.util.Locale;
032import javax.enterprise.context.ApplicationScoped;
033import javax.inject.Inject;
034
035@ApplicationScoped
036public class FiscalPeriodLogic {
037
038    @Inject
039    PartyControl partyControl;
040
041    @Inject
042    PeriodControl periodControl;
043
044    @Inject
045    PeriodLogic periodLogic;
046
047    protected FiscalPeriodLogic() {
048        super();
049    }
050    
051    private void createMonth(final PeriodKind periodKind, final Period quarterPeriod, final ZonedDateTime yearStart, final int month,
052            final PartyPK createdBy) {
053        var year = yearStart.getYear();
054        var periodName = new StringBuilder().append(year).append('_').append('M');
055        var monthPeriodType = periodControl.getPeriodTypeByName(periodKind, PeriodConstants.PeriodType_MONTH);
056        var monthStart = yearStart.withMonth(month);
057        var monthEnd = monthStart.plusMonths(1).minusNanos(1);
058        
059        new Formatter(periodName, Locale.US).format("%02d", month);
060        var monthPeriod = periodLogic.createPeriod(periodKind, periodName.toString(), quarterPeriod, monthPeriodType,
061                monthStart.toInstant().toEpochMilli(), monthEnd.toInstant().toEpochMilli(), createdBy);
062
063        var periodKindDescriptions = periodControl.getPeriodKindDescriptionsByPeriodKind(periodKind);
064        var yearPeriodType = periodControl.getPeriodTypeByName(periodKind, PeriodConstants.PeriodType_YEAR);
065        periodKindDescriptions.forEach((periodKindDescription) -> {
066            var language = periodKindDescription.getLanguage();
067            var yearPeriodTypeDescription = periodControl.getPeriodTypeDescription(yearPeriodType, language);
068            if (yearPeriodTypeDescription != null) {
069                var monthPeriodTypeDescription = periodControl.getPeriodTypeDescription(monthPeriodType, language);
070                if (monthPeriodTypeDescription != null) {
071                    var description = periodKindDescription.getDescription() +
072                            ' ' + yearPeriodTypeDescription.getDescription() +
073                            ' ' + year +
074                            ' ' + monthPeriodTypeDescription.getDescription() +
075                            ' ' + month;
076
077                    periodControl.createPeriodDescription(monthPeriod, language, description, createdBy);
078                }
079            }
080        });
081    }
082    
083    private void createQuarter(final PeriodKind periodKind, final Period yearPeriod, final ZonedDateTime yearStart, final int quarter,
084            final PartyPK createdBy) {
085        var year = yearStart.getYear();
086        var periodName = String.valueOf(year) + '_' + 'Q' + quarter;
087        var quarterPeriodType = periodControl.getPeriodTypeByName(periodKind, PeriodConstants.PeriodType_QUARTER);
088        var monthOfQuarterStart = 1 + (quarter - 1) * 3;
089        var quarterStart = yearStart.withMonth(monthOfQuarterStart);
090        var quarterEnd = quarterStart.plusMonths(3).minusNanos(1);
091        var quarterPeriod = periodLogic.createPeriod(periodKind, periodName, yearPeriod, quarterPeriodType,
092                quarterStart.toInstant().toEpochMilli(), quarterEnd.toInstant().toEpochMilli(), createdBy);
093
094        var periodKindDescriptions = periodControl.getPeriodKindDescriptionsByPeriodKind(periodKind);
095        var yearPeriodType = periodControl.getPeriodTypeByName(periodKind, PeriodConstants.PeriodType_YEAR);
096        periodKindDescriptions.forEach((periodKindDescription) -> {
097            var language = periodKindDescription.getLanguage();
098            var yearPeriodTypeDescription = periodControl.getPeriodTypeDescription(yearPeriodType, language);
099            if (yearPeriodTypeDescription != null) {
100                var quarterPeriodTypeDescription = periodControl.getPeriodTypeDescription(quarterPeriodType, language);
101                if (quarterPeriodTypeDescription != null) {
102                    var description = periodKindDescription.getDescription() +
103                            ' ' + yearPeriodTypeDescription.getDescription() +
104                            ' ' + year +
105                            ' ' + quarterPeriodTypeDescription.getDescription() +
106                            ' ' + quarter;
107
108                    periodControl.createPeriodDescription(quarterPeriod, language, description, createdBy);
109                }
110            }
111        });
112
113        for(var monthOffset = 0; monthOffset < 3; monthOffset++) {
114            createMonth(periodKind, quarterPeriod, yearStart, monthOfQuarterStart + monthOffset, createdBy);
115        }
116    }
117    
118    private Period createYear(final ExecutionErrorAccumulator eea, final Period perpetualPeriod, final int year,
119            final ZoneId zone, final PartyPK createdBy) {
120        var periodName = String.valueOf(year);
121        var periodKind = periodControl.getPeriodKindByName(PeriodConstants.PeriodKind_FISCAL);
122        var yearPeriod = periodControl.getPeriodByName(periodKind, periodName);
123
124        if(yearPeriod == null) {
125            var periodType = periodControl.getPeriodTypeByName(periodKind, PeriodConstants.PeriodType_YEAR);
126            var yearStart = ZonedDateTime.of(year, 1, 1, 0, 0, 0, 0, zone);
127            var yearEnd = yearStart.plusYears(1).minusNanos(1);
128
129            yearPeriod = periodLogic.createPeriod(periodKind, periodName, perpetualPeriod, periodType,
130                    yearStart.toInstant().toEpochMilli(), yearEnd.toInstant().toEpochMilli(), createdBy);
131
132            var periodKindDescriptions = periodControl.getPeriodKindDescriptionsByPeriodKind(periodKind);
133            for(var periodKindDescription : periodKindDescriptions) {
134                var language = periodKindDescription.getLanguage();
135                var periodTypeDescription = periodControl.getPeriodTypeDescription(periodType, language);
136                
137                if(periodTypeDescription != null) {
138                    var description = periodKindDescription.getDescription() +
139                            ' ' + periodTypeDescription.getDescription() +
140                            ' ' + year;
141                    
142                    periodControl.createPeriodDescription(yearPeriod, language, description, createdBy);
143                }
144            }
145            
146            for(var quarter = 1; quarter < 5; quarter++) {
147                createQuarter(periodKind, yearPeriod, yearStart, quarter, createdBy);
148            }
149        } else {
150            eea.addExecutionError(ExecutionErrors.DuplicatePeriodName.name(), PeriodConstants.PeriodKind_FISCAL, periodName);
151        }
152        
153        return yearPeriod;
154    }
155
156    public Period ensurePerpetual(final ExecutionErrorAccumulator eea, final PartyPK createdBy) {
157        var periodKind = periodControl.getPeriodKindByName(PeriodConstants.PeriodKind_FISCAL);
158        var perpetualPeriod = periodControl.getPeriodByName(periodKind, PeriodConstants.Period_PERPETUAL);
159
160        if(perpetualPeriod == null) {
161            var periodType = periodControl.getPeriodTypeByName(periodKind, PeriodConstants.PeriodType_PERPETUAL);
162
163            perpetualPeriod = periodLogic.createPeriod(periodKind, PeriodConstants.Period_PERPETUAL, null,
164                    periodType, 0L, Long.MAX_VALUE, createdBy);
165
166        }
167
168        return perpetualPeriod;
169    }
170    
171    public Period createFiscalYear(final ExecutionErrorAccumulator eea, final Integer year, final PartyPK createdBy) {
172        var defaultPartyCompany = partyControl.getDefaultPartyCompany();
173        Period fiscalYear = null;
174        
175        if(defaultPartyCompany != null) {
176            var perpetualPeriod = ensurePerpetual(eea, createdBy);
177            var javaTimeZoneName = partyControl.getPreferredTimeZone(defaultPartyCompany.getParty()).getLastDetail().getJavaTimeZoneName();
178            var zone = ZoneId.of(javaTimeZoneName);
179            
180            fiscalYear = createYear(eea, perpetualPeriod, year, zone, createdBy);
181        } else {
182            eea.addExecutionError(ExecutionErrors.MissingDefaultCompany.name());
183        }
184        
185        return fiscalYear;
186    }
187    
188    private Period getFiscalPeriodByName(final ExecutionErrorAccumulator eea, final String periodName,
189            final EntityPermission entityPermission) {
190        var periodKind = periodControl.getPeriodKindByName(PeriodConstants.PeriodKind_FISCAL);
191        var period = periodControl.getPeriodByName(periodKind, periodName, entityPermission);
192        
193        if(periodKind != null) {
194            period = periodControl.getPeriodByName(periodKind, periodName, entityPermission);
195            
196            if(period == null) {
197                eea.addExecutionError(ExecutionErrors.UnknownPeriodName.name(), PeriodConstants.PeriodKind_FISCAL, periodName);
198            }
199        } else {
200            eea.addExecutionError(ExecutionErrors.UnknownPeriodKindName.name(), PeriodConstants.PeriodKind_FISCAL);
201        }
202        
203        return period;
204    }
205    
206    public Period getFiscalPeriodByName(final ExecutionErrorAccumulator eea, final String periodName) {
207        return getFiscalPeriodByName(eea, periodName, EntityPermission.READ_ONLY);
208    }
209    
210    public Period getFiscalPeriodByNameForUpdate(final ExecutionErrorAccumulator eea, final String periodName) {
211        return getFiscalPeriodByName(eea, periodName, EntityPermission.READ_WRITE);
212    }
213    
214}