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}