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}