001 /*
002 * Copyright 2011 The Kuali Foundation.
003 *
004 * Licensed under the Educational Community 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.opensource.org/licenses/ecl2.php
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 package org.kuali.kfs.module.endow.batch.service.impl;
017
018 import static org.kuali.kfs.module.endow.EndowConstants.NEW_TARGET_TRAN_LINE_PROPERTY_NAME;
019
020 import java.math.BigDecimal;
021 import java.util.ArrayList;
022 import java.util.HashMap;
023 import java.util.List;
024 import java.util.Map;
025
026 import org.kuali.kfs.module.endow.EndowConstants;
027 import org.kuali.kfs.module.endow.EndowParameterKeyConstants;
028 import org.kuali.kfs.module.endow.batch.CreateAccrualTransactionsStep;
029 import org.kuali.kfs.module.endow.batch.service.CreateAccrualTransactionsService;
030 import org.kuali.kfs.module.endow.businessobject.EndowmentTargetTransactionLine;
031 import org.kuali.kfs.module.endow.businessobject.EndowmentTargetTransactionSecurity;
032 import org.kuali.kfs.module.endow.businessobject.EndowmentTransactionLine;
033 import org.kuali.kfs.module.endow.businessobject.EndowmentTransactionSecurity;
034 import org.kuali.kfs.module.endow.businessobject.HoldingTaxLot;
035 import org.kuali.kfs.module.endow.businessobject.Security;
036 import org.kuali.kfs.module.endow.businessobject.TransactionDocumentExceptionReportLine;
037 import org.kuali.kfs.module.endow.businessobject.TransactionDocumentTotalReportLine;
038 import org.kuali.kfs.module.endow.dataaccess.SecurityDao;
039 import org.kuali.kfs.module.endow.document.CashIncreaseDocument;
040 import org.kuali.kfs.module.endow.document.service.HoldingTaxLotService;
041 import org.kuali.kfs.module.endow.document.service.KEMService;
042 import org.kuali.kfs.module.endow.document.validation.event.AddTransactionLineEvent;
043 import org.kuali.kfs.module.endow.util.GloabalVariablesExtractHelper;
044 import org.kuali.kfs.sys.service.ReportWriterService;
045 import org.kuali.rice.kew.exception.WorkflowException;
046 import org.kuali.rice.kns.rule.event.RouteDocumentEvent;
047 import org.kuali.rice.kns.service.BusinessObjectService;
048 import org.kuali.rice.kns.service.DocumentService;
049 import org.kuali.rice.kns.service.KualiConfigurationService;
050 import org.kuali.rice.kns.service.KualiRuleService;
051 import org.kuali.rice.kns.service.ParameterService;
052 import org.kuali.rice.kns.util.KualiDecimal;
053 import org.springframework.transaction.annotation.Transactional;
054
055 /**
056 * This class...
057 */
058 @Transactional
059 public class CreateAccrualTransactionsServiceImpl implements CreateAccrualTransactionsService {
060 protected static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(CreateAccrualTransactionsServiceImpl.class);
061
062 private BusinessObjectService businessObjectService;
063 private KEMService kemService;
064 private HoldingTaxLotService holdingTaxLotService;
065 private SecurityDao securityDao;
066 private DocumentService documentService;
067 private KualiConfigurationService configService;
068 private KualiRuleService kualiRuleService;
069 protected ParameterService parameterService;
070
071 protected ReportWriterService accrualTransactionsExceptionReportWriterService;
072 protected ReportWriterService accrualTransactionsTotalReportWriterService;
073
074 protected TransactionDocumentExceptionReportLine exceptionReportLine = null;
075 protected TransactionDocumentTotalReportLine totalReportLine = null;
076
077 protected boolean isFistTimeForWritingTotalReport = true;
078 protected boolean isFistTimeForWritingExceptionReport = true;
079
080 /**
081 * Constructs a CreateAccrualTransactionsServiceImpl.java.
082 */
083 public CreateAccrualTransactionsServiceImpl() {
084
085 }
086
087 /**
088 * @see org.kuali.kfs.module.endow.batch.service.CreateAccrualTransactionsService#createAccrualTransactions()
089 */
090 public boolean createAccrualTransactions() {
091 boolean success = true;
092
093 LOG.debug("createAccrualTransactions() started");
094
095 int maxNumberOfTranLines = kemService.getMaxNumberOfTransactionLinesPerDocument();
096
097 // get all securities with next income pay date equal to current date
098 List<Security> securities = getAllSecuritiesWithNextPayDateEqualCurrentDate();
099
100 for (Security security : securities) {
101
102 // get all tax lots that have an accrued income greater than zero
103 List<HoldingTaxLot> taxLots = holdingTaxLotService.getAllTaxLotsWithAccruedIncomeGreaterThanZeroPerSecurity(security.getId());
104
105 // build a map that groups tax lots based on registration code ( a map from registration code to taxlots)
106 Map<String, List<HoldingTaxLot>> regCodeMap = groupTaxLotsBasedOnRegistrationCode(taxLots);
107
108 // create Cash Increase documents for each security and registration code
109 for (String registrationCode : regCodeMap.keySet()) {
110
111 // 4. create new CashIncreaseDocument
112 CashIncreaseDocument cashIncreaseDocument = createNewCashIncreaseDocument(security.getId(), registrationCode);
113
114 if (cashIncreaseDocument != null) {
115
116 // create a new totalReportLine and exceptionReportLine for a new CashIncreaseDocument
117 initializeTotalAndExceptionReportLines(cashIncreaseDocument.getDocumentNumber(), security.getId());
118
119 // group tax lots by kemid and ip indicator; for each kemid and ip indicator we create a new transaction line
120 Map<String, List<HoldingTaxLot>> kemidIpMap = groupTaxLotsBasedOnKemidAndIPIndicator(regCodeMap.get(registrationCode));
121
122 // keep track of the tax lots to be updated
123 List<HoldingTaxLot> taxLotsForUpdate = new ArrayList<HoldingTaxLot>();
124 // keep a counter to create a new document if there are more that maximum number of allowed transaction lines
125 // per
126 // document
127 int counter = 0;
128
129 // create a new transaction line for each kemid and ip indicator
130 for (String kemidIp : kemidIpMap.keySet()) {
131
132 // compute the total amount for the transaction line
133 KualiDecimal totalAmount = KualiDecimal.ZERO;
134 String kemid = null;
135
136 for (HoldingTaxLot lot : kemidIpMap.get(kemidIp)) {
137 totalAmount = totalAmount.add(new KualiDecimal(lot.getCurrentAccrual()));
138
139 // initialize kemid
140 if (kemid == null) {
141 kemid = lot.getKemid();
142 }
143 }
144
145 // collect tax lots for update: when the document is submitted the tax lots accrual is copied to prior
146 // accrual
147 // and the current accrual is reset to zero
148 taxLotsForUpdate.addAll(kemidIpMap.get(kemidIp));
149
150 // if we have already reached the maximum number of transaction lines on the current document then create a
151 // new
152 // document
153 if (counter == maxNumberOfTranLines) {
154 // submit the current ECI doc and update the values in the tax lots used already
155 submitCashIncreaseDocumentAndUpdateTaxLots(cashIncreaseDocument, taxLotsForUpdate);
156
157 // clear tax lots for update and create a new Cash Increase document
158 taxLotsForUpdate.clear();
159 cashIncreaseDocument = createNewCashIncreaseDocument(security.getId(), registrationCode);
160
161 if (cashIncreaseDocument != null) {
162 // create a new totalReportLine and exceptionReportLine for a new CashIncreaseDocument
163 initializeTotalAndExceptionReportLines(cashIncreaseDocument.getDocumentNumber(), security.getId());
164
165 counter = 0;
166 }
167 }
168
169 if (cashIncreaseDocument != null) {
170 // add a new transaction line
171 if (addTransactionLine(cashIncreaseDocument, security, kemid, totalAmount)) {
172 counter++;
173 }
174 else {
175 // if unable to add a new transaction line then remove the tax lots from the tax lots to update list
176 taxLotsForUpdate.remove(kemidIp);
177 }
178 }
179 }
180
181 // submit the current ECI doc and update the values in the tax lots used already
182 submitCashIncreaseDocumentAndUpdateTaxLots(cashIncreaseDocument, taxLotsForUpdate);
183 }
184
185 }
186
187 LOG.debug("createAccrualTransactions() done");
188 }
189
190 return success;
191 }
192
193 /**
194 * Builds a map that groups tax lots based on registration code ( a map from registration code to taxlots)
195 *
196 * @param taxLots
197 * @return a map from registration code to taxlots
198 */
199 protected Map<String, List<HoldingTaxLot>> groupTaxLotsBasedOnRegistrationCode(List<HoldingTaxLot> taxLots) {
200 // build a map that groups tax lots based on registration code ( a map from registration code to taxlots)
201 Map<String, List<HoldingTaxLot>> regCodeMap = new HashMap<String, List<HoldingTaxLot>>();
202
203 for (HoldingTaxLot holdingTaxLot : taxLots) {
204 String registrationCode = holdingTaxLot.getRegistrationCode();
205 if (regCodeMap.containsKey(registrationCode)) {
206 regCodeMap.get(registrationCode).add(holdingTaxLot);
207 }
208 else {
209 List<HoldingTaxLot> tmpTaxLots = new ArrayList<HoldingTaxLot>();
210 tmpTaxLots.add(holdingTaxLot);
211 regCodeMap.put(registrationCode, tmpTaxLots);
212 }
213 }
214
215 return regCodeMap;
216 }
217
218 /**
219 * Builds a map that groups tax lots based on kemid and income principal indicator ( a map from kemid and IP to taxlots).
220 *
221 * @param taxLots
222 * @return a map from kemid and IP to taxlots
223 */
224 protected Map<String, List<HoldingTaxLot>> groupTaxLotsBasedOnKemidAndIPIndicator(List<HoldingTaxLot> taxLots) {
225 // group tax lots by kemid and ip indicator
226 Map<String, List<HoldingTaxLot>> kemidIpMap = new HashMap<String, List<HoldingTaxLot>>();
227
228 for (HoldingTaxLot holdingTaxLot : taxLots) {
229 String kemidAndIp = holdingTaxLot.getKemid() + holdingTaxLot.getIncomePrincipalIndicator();
230 if (kemidIpMap.containsKey(kemidAndIp)) {
231 kemidIpMap.get(kemidAndIp).add(holdingTaxLot);
232 }
233 else {
234 List<HoldingTaxLot> tmpTaxLots = new ArrayList<HoldingTaxLot>();
235 tmpTaxLots.add(holdingTaxLot);
236 kemidIpMap.put(kemidAndIp, tmpTaxLots);
237 }
238 }
239
240 return kemidIpMap;
241 }
242
243 /**
244 * Creates and adds a new transaction line to the cash increase document.
245 *
246 * @param cashIncreaseDocument
247 * @param security
248 * @param kemid
249 * @param totalAmount
250 * @return true if transaction line successfully added, false otherwise
251 */
252 protected boolean addTransactionLine(CashIncreaseDocument cashIncreaseDocument, Security security, String kemid, KualiDecimal totalAmount) {
253 boolean success = true;
254
255 // Create a new transaction line
256 EndowmentTransactionLine endowmentTransactionLine = new EndowmentTargetTransactionLine();
257 endowmentTransactionLine.setDocumentNumber(cashIncreaseDocument.getDocumentNumber());
258 endowmentTransactionLine.setKemid(kemid);
259 endowmentTransactionLine.setEtranCode(security.getClassCode().getSecurityIncomeEndowmentTransactionPostCode());
260 endowmentTransactionLine.setTransactionIPIndicatorCode(EndowConstants.IncomePrincipalIndicator.INCOME);
261 endowmentTransactionLine.setTransactionAmount(totalAmount);
262
263 boolean rulesPassed = kualiRuleService.applyRules(new AddTransactionLineEvent(NEW_TARGET_TRAN_LINE_PROPERTY_NAME, cashIncreaseDocument, endowmentTransactionLine));
264
265 if (rulesPassed) {
266 cashIncreaseDocument.addTargetTransactionLine((EndowmentTargetTransactionLine) endowmentTransactionLine);
267 totalReportLine.addIncomeAmount(totalAmount);
268 }
269 else {
270 success = false;
271
272 // write an exception line when a transaction line fails to pass the validation.
273 exceptionReportLine.setKemid(kemid);
274 exceptionReportLine.setIncomeAmount(totalAmount);
275 if (isFistTimeForWritingExceptionReport) {
276 accrualTransactionsExceptionReportWriterService.writeTableHeader(exceptionReportLine);
277 isFistTimeForWritingExceptionReport = false;
278 }
279 accrualTransactionsExceptionReportWriterService.writeTableRow(exceptionReportLine);
280 List<String> errorMessages = GloabalVariablesExtractHelper.extractGlobalVariableErrors();
281 for (String errorMessage : errorMessages) {
282 accrualTransactionsExceptionReportWriterService.writeFormattedMessageLine("Reason: %s", errorMessage);
283 accrualTransactionsExceptionReportWriterService.writeNewLines(1);
284 }
285 }
286 return success;
287 }
288
289 /**
290 * Creates a new CashIncreaseDocument with source type Automated, transaction sub-type Cash, target security id set to the input
291 * security id.
292 *
293 * @param securityId
294 * @return a new CashIncreaseDocument
295 */
296 protected CashIncreaseDocument createNewCashIncreaseDocument(String securityId, String registrationCode) {
297 CashIncreaseDocument cashIncreaseDocument = null;
298 try {
299
300 cashIncreaseDocument = (CashIncreaseDocument) documentService.getNewDocument(getCashIncreaseDocumentType());
301 String documentDescription = parameterService.getParameterValue(CreateAccrualTransactionsStep.class, EndowParameterKeyConstants.DESCRIPTION);
302 cashIncreaseDocument.getDocumentHeader().setDocumentDescription(documentDescription);
303 cashIncreaseDocument.setTransactionSourceTypeCode(EndowConstants.TransactionSourceTypeCode.AUTOMATED);
304 cashIncreaseDocument.setTransactionSubTypeCode(EndowConstants.TransactionSubTypeCode.CASH);
305
306 // set security details
307 EndowmentTransactionSecurity targetTransactionSecurity = new EndowmentTargetTransactionSecurity();
308 targetTransactionSecurity.setSecurityID(securityId);
309 targetTransactionSecurity.setRegistrationCode(registrationCode);
310 cashIncreaseDocument.setTargetTransactionSecurity(targetTransactionSecurity);
311
312 }
313 catch (WorkflowException ex) {
314 if (isFistTimeForWritingExceptionReport) {
315 if (exceptionReportLine == null) {
316 exceptionReportLine = new TransactionDocumentExceptionReportLine(getCashIncreaseDocumentType(), "", securityId);
317 }
318 accrualTransactionsExceptionReportWriterService.writeTableHeader(exceptionReportLine);
319 isFistTimeForWritingExceptionReport = false;
320 }
321 accrualTransactionsExceptionReportWriterService.writeTableRow(exceptionReportLine);
322 accrualTransactionsExceptionReportWriterService.writeFormattedMessageLine("Reason: %s", "WorkflowException while creating a CashIncreaseDocument for Accrual Transactions: " + ex.toString());
323 accrualTransactionsExceptionReportWriterService.writeNewLines(1);
324 }
325
326 return cashIncreaseDocument;
327 }
328
329 /**
330 * Submits the ECI doc and updates the values in the tax lots list.
331 *
332 * @param cashIncreaseDocument
333 * @param taxLotsForUpdate
334 */
335 protected void submitCashIncreaseDocumentAndUpdateTaxLots(CashIncreaseDocument cashIncreaseDocument, List<HoldingTaxLot> taxLotsForUpdate) {
336
337 boolean rulesPassed = kualiRuleService.applyRules(new RouteDocumentEvent(cashIncreaseDocument));
338
339 if (rulesPassed) {
340
341 String noRouteIndVal = parameterService.getParameterValue(CreateAccrualTransactionsStep.class, EndowParameterKeyConstants.NO_ROUTE_IND);
342 boolean noRouteIndicator = EndowConstants.YES.equalsIgnoreCase(noRouteIndVal) ? true : false;
343
344 try {
345 cashIncreaseDocument.setNoRouteIndicator(noRouteIndicator);
346 // TODO figure out if/how we use the ad hoc recipients list
347 documentService.routeDocument(cashIncreaseDocument, "Created by Accrual Transactions Batch process.", null);
348
349 // write a total report line for a CashIncreaseDocument
350 if (isFistTimeForWritingTotalReport) {
351 accrualTransactionsTotalReportWriterService.writeTableHeader(totalReportLine);
352 isFistTimeForWritingTotalReport = false;
353 }
354
355 accrualTransactionsTotalReportWriterService.writeTableRow(totalReportLine);
356
357 // set accrued income to zero and copy current value in prior accrued income
358 for (HoldingTaxLot taxLotForUpdate : taxLotsForUpdate) {
359 taxLotForUpdate.setPriorAccrual(taxLotForUpdate.getCurrentAccrual());
360 taxLotForUpdate.setCurrentAccrual(BigDecimal.ZERO);
361 }
362
363 // save changes
364 businessObjectService.save(taxLotsForUpdate);
365 }
366 catch (WorkflowException ex) {
367 // save document if it can not be routed....
368 try {
369 documentService.saveDocument(cashIncreaseDocument);
370 } catch (WorkflowException savewfe) {
371
372 }
373
374 if (isFistTimeForWritingExceptionReport) {
375 accrualTransactionsExceptionReportWriterService.writeTableHeader(exceptionReportLine);
376 isFistTimeForWritingExceptionReport = false;
377 }
378
379 accrualTransactionsExceptionReportWriterService.writeTableRow(exceptionReportLine);
380 accrualTransactionsExceptionReportWriterService.writeFormattedMessageLine("Reason: %s", "WorkflowException while routing a CashIncreaseDocument for Accrual Transactions batch process: " + ex.toString());
381 accrualTransactionsExceptionReportWriterService.writeNewLines(1);
382 }
383 }
384 else {
385 try {
386 exceptionReportLine.setSecurityId(cashIncreaseDocument.getTargetTransactionSecurity().getSecurityID());
387 exceptionReportLine.setIncomeAmount(cashIncreaseDocument.getTargetIncomeTotal());
388 accrualTransactionsExceptionReportWriterService.writeTableRow(exceptionReportLine);
389 List<String> errorMessages = GloabalVariablesExtractHelper.extractGlobalVariableErrors();
390 for (String errorMessage : errorMessages) {
391 accrualTransactionsExceptionReportWriterService.writeFormattedMessageLine("Reason: %s", errorMessage);
392 accrualTransactionsExceptionReportWriterService.writeNewLines(1);
393 }
394
395 // try to save the document
396 documentService.saveDocument(cashIncreaseDocument);
397
398 }
399 catch (WorkflowException ex) {
400 // have to write a table header before write the table row.
401 if (isFistTimeForWritingExceptionReport) {
402 accrualTransactionsExceptionReportWriterService.writeTableHeader(exceptionReportLine);
403 isFistTimeForWritingExceptionReport = false;
404 }
405 accrualTransactionsExceptionReportWriterService.writeTableRow(exceptionReportLine);
406 // Write reason as a formatted message in a second line
407 accrualTransactionsExceptionReportWriterService.writeFormattedMessageLine("Reason: %s", "WorkflowException while saving a CashIncreaseDocument for Accrual Transactions batch process: " + ex.toString());
408 accrualTransactionsExceptionReportWriterService.writeNewLines(1);
409
410 }
411
412 }
413 }
414
415 /**
416 * Gets the CashIncreaseDocument type.
417 *
418 * @return the CashIncreaseDocument type
419 */
420 private String getCashIncreaseDocumentType() {
421 return "ECI";
422 }
423
424 /**
425 * Locates all Security records for which the next income pay date is equal to the current date.
426 *
427 * @return
428 */
429 protected List<Security> getAllSecuritiesWithNextPayDateEqualCurrentDate() {
430 List<Security> result = new ArrayList<Security>();
431
432 result = securityDao.getAllSecuritiesWithNextPayDateEqualCurrentDate();
433
434 return result;
435 }
436
437 /**
438 * Initializes the total report line and the exception report line.
439 *
440 * @param theDocumentId
441 * @param theSecurityId
442 */
443 protected void initializeTotalAndExceptionReportLines(String theDocumentId, String theSecurityId) {
444 // create a new totalReportLine for each new CashIncreaseDocument
445 this.totalReportLine = new TransactionDocumentTotalReportLine(getCashIncreaseDocumentType(), theDocumentId, theSecurityId);
446
447 // create an exceptionReportLine instance that can be reused for reporting multiple errors for a CashIncreaseDocument
448 this.exceptionReportLine = new TransactionDocumentExceptionReportLine(getCashIncreaseDocumentType(), theDocumentId, theSecurityId);
449
450 }
451
452 /**
453 * Sets the businessObjectService.
454 *
455 * @param businessObjectService
456 */
457 public void setBusinessObjectService(BusinessObjectService businessObjectService) {
458 this.businessObjectService = businessObjectService;
459 }
460
461 /**
462 * Sets the kemService.
463 *
464 * @param kemService
465 */
466 public void setKemService(KEMService kemService) {
467 this.kemService = kemService;
468 }
469
470 /**
471 * Sets the holdingTaxLotService.
472 *
473 * @param holdingTaxLotService
474 */
475 public void setHoldingTaxLotService(HoldingTaxLotService holdingTaxLotService) {
476 this.holdingTaxLotService = holdingTaxLotService;
477 }
478
479 /**
480 * Sets the securityDao.
481 *
482 * @param securityDao
483 */
484 public void setSecurityDao(SecurityDao securityDao) {
485 this.securityDao = securityDao;
486 }
487
488 /**
489 * Sets the documenyService.
490 *
491 * @param documentService
492 */
493 public void setDocumentService(DocumentService documentService) {
494 this.documentService = documentService;
495 }
496
497 /**
498 * Sets the configService.
499 *
500 * @param configService
501 */
502 public void setConfigService(KualiConfigurationService configService) {
503 this.configService = configService;
504 }
505
506 /**
507 * Sets the kualiRuleService.
508 *
509 * @param kualiRuleService
510 */
511 public void setKualiRuleService(KualiRuleService kualiRuleService) {
512 this.kualiRuleService = kualiRuleService;
513 }
514
515 /**
516 * Sets the parameterService.
517 *
518 * @param parameterService
519 */
520 public void setParameterService(ParameterService parameterService) {
521 this.parameterService = parameterService;
522 }
523
524 /**
525 * Gets the accrualTransactionsExceptionReportWriterService.
526 *
527 * @return accrualTransactionsExceptionReportWriterService
528 */
529 public ReportWriterService getAccrualTransactionsExceptionReportWriterService() {
530 return accrualTransactionsExceptionReportWriterService;
531 }
532
533 /**
534 * Sets the accrualTransactionsExceptionReportWriterService.
535 *
536 * @param accrualTransactionsExceptionReportWriterService
537 */
538 public void setAccrualTransactionsExceptionReportWriterService(ReportWriterService accrualTransactionsExceptionReportWriterService) {
539 this.accrualTransactionsExceptionReportWriterService = accrualTransactionsExceptionReportWriterService;
540 }
541
542 /**
543 * Gets the accrualTransactionsTotalReportWriterService.
544 *
545 * @return accrualTransactionsTotalReportWriterService
546 */
547 public ReportWriterService getAccrualTransactionsTotalReportWriterService() {
548 return accrualTransactionsTotalReportWriterService;
549 }
550
551 /**
552 * Sets the accrualTransactionsTotalReportWriterService.
553 *
554 * @param accrualTransactionsTotalReportWriterService
555 */
556 public void setAccrualTransactionsTotalReportWriterService(ReportWriterService accrualTransactionsTotalReportWriterService) {
557 this.accrualTransactionsTotalReportWriterService = accrualTransactionsTotalReportWriterService;
558 }
559 }