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