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