/// <summary> /// Add a snapshot for the project. /// </summary> /// <param name="project"></param> /// <param name="model"></param> /// <param name="metadata"></param> private void AddSnapshot(Entity.Project project, Model.ImportProjectModel model, Entity.Models.DisposalProjectSnapshotMetadata metadata) { var today = model.SnapshotOn ?? DateTime.Today.ToUniversalTime(); // Allows for all imports done on the same day to create the same snapshot time. metadata.NetProceeds = model.PriorNetProceeds; // Temporarily set it to the prior amount for the snapshot. var snapshot = new Entity.ProjectSnapshot(project) { SnapshotOn = today, Metadata = JsonSerializer.Serialize(metadata, _serializerOptions) }; project.Snapshots.Add(snapshot); metadata.NetProceeds = model.NetProceeds; // Set it back to the current value. }
/// <summary> /// Override the model values with the specified defaults. /// </summary> /// <param name="model"></param> /// <param name="fromSnapshot"></param> /// <param name="defaults"></param> private void ApplyDefaults(Model.ImportProjectModel model, DateTime?fromSnapshot = null, string[] defaults = null) { // Use defaults if required if (defaults?.Any() ?? false) { var props = typeof(Model.ImportProjectModel).GetCachedProperties(); foreach (var kv in defaults) { var keyValue = kv.Trim().Split("="); if (keyValue.Length < 2) { throw new ArgumentException($"Argument '{kv}' is not valid."); } var prop = props.FirstOrDefault(p => String.Compare(p.Name, keyValue[0], true) == 0); var modelValue = prop?.GetValue(model); if (prop != null && modelValue == null || modelValue.Equals(prop.PropertyType.GetDefault())) { if (prop.PropertyType == typeof(DateTime) || prop.PropertyType == typeof(DateTime?)) { var value = DateTime.Parse(keyValue[1]); prop.SetValue(model, value); } else { var value = Convert.ChangeType(keyValue[1], prop.PropertyType); prop.SetValue(model, value); } } } } if (fromSnapshot.HasValue && !model.SnapshotOn.HasValue) { model.SnapshotOn = fromSnapshot; } }
/// <summary> /// Copy the 'model' project properties into existing projects, or add new projects to PIMS. /// </summary> /// <param name="project"></param> /// <param name="model"></param> /// <param name="stopOnError"></param> /// <param name="defaults"></param> /// <returns></returns> private Entity.Project Merge(Entity.Project project, Model.ImportProjectModel model, bool stopOnError = true, string[] defaults = null) { try { // Use defaults if required if (defaults?.Any() ?? false) { var props = typeof(Model.ImportProjectModel).GetCachedProperties(); foreach (var kv in defaults) { var keyValue = kv.Trim().Split("="); if (keyValue.Length < 2) { throw new ArgumentException($"Argument '{kv}' is not valid."); } var prop = props.FirstOrDefault(p => String.Compare(p.Name, keyValue[0], true) == 0); var modelValue = prop?.GetValue(model); if (prop != null && modelValue == null || modelValue.Equals(prop.PropertyType.GetDefault())) { var value = Convert.ChangeType(keyValue[1], prop.PropertyType); prop.SetValue(model, value); } } } _logger.LogDebug($"Importing Project '{model.ProjectNumber}', agency:'{model.Agency}', workflow:'{model.Workflow}', status:'{model.Status}'"); project ??= new Entity.Project(); project.ProjectNumber = model.ProjectNumber; project.Name = model.Description; project.Description = model.Description; project.Manager = model.Manager; project.ActualFiscalYear = model.ActualFiscalYear; project.ReportedFiscalYear = model.ReportedFiscalYear; project.Agency = GetAgency(model.Agency); project.AgencyId = project.Agency.Id; project.Workflow = GetWorkflow(model.Workflow); project.WorkflowId = project.Workflow.Id; project.Status = GetStatus(model.Status); project.StatusId = project.Status.Id; project.TierLevel = GetTier(model.Estimated, project.Properties.Count()); // TODO: Need to import or link project properties. project.TierLevelId = project.TierLevel.Id; project.Risk = GetRisk(model.Risk); project.RiskId = project.Risk.Id; project.OcgFinancialStatement = model.OcgFinancialStatement; project.NetBook = model.NetBook; project.Estimated = model.Estimated; project.ProgramCost = model.ProgramCost; project.SalesCost = model.SalesCost; project.InterestComponent = model.InterestComponent; project.NetProceeds = model.NetProceeds; project.GainLoss = model.GainLoss; project.SaleWithLeaseInPlace = model.SaleWithLeaseInPlace; if (model.PriorNetProceeds.HasValue) { project.Snapshots.Add(new Entity.ProjectSnapshot(project) { NetProceeds = model.PriorNetProceeds.Value }); } project.MarketedOn = model.MarketedOn; project.CompletedOn = model.CompletedOn; project.PrivateNote = model.PrivateNote; // The data doesn't provide the purchasing agency information so the response will be the current owning agency. if (model.AgencyResponseDate.HasValue) { var response = project.Responses.FirstOrDefault(r => r.AgencyId == project.AgencyId); if (response == null) { project.Responses.Add(new Entity.ProjectAgencyResponse(project, project.Agency, Entity.NotificationResponses.Watch, model.AgencyResponseDate)); } else { response.Response = Entity.NotificationResponses.Watch; response.ReceivedOn = model.AgencyResponseDate ?? response.ReceivedOn; } } if (!String.IsNullOrWhiteSpace(model.FinancialNote)) { var financialNote = project.Notes.FirstOrDefault(n => n.NoteType == Entity.NoteTypes.Financial); if (financialNote == null) { project.Notes.Add(new Entity.ProjectNote(project, Entity.NoteTypes.Financial, model.FinancialNote)); } else { financialNote.Note = model.FinancialNote; } } // Add tasks if they haven't been added already. if (!project.Tasks.Any()) { // Assumption we'll need to add all tasks from Submit to SPL. var tasks = _service.Task.GetForWorkflow("SUBMIT-DISPOSAL"); tasks = tasks.Concat(_service.Task.GetForWorkflow("ASSESS-DISPOSAL")); tasks = tasks.Concat(_service.Task.GetForWorkflow("ERP")); tasks = tasks.Concat(_service.Task.GetForWorkflow("SPL")); project.AddTask(tasks.DistinctBy(t => t.Id).ToArray()); project.Tasks.ForEach(t => { t.IsCompleted = !t.Task.IsOptional; }); } _logger.LogDebug($"Parsed project '{project.ProjectNumber}' - '{project.Status.Code}'", project); return(project); } catch (Exception ex) { _logger.LogError(ex, $"Failed to parse this project while importing '{model.ProjectNumber}' - {ex.Message}"); if (stopOnError) { throw; } return(null); } }
/// <summary> /// Copy the 'model' project properties into existing projects, or add new projects to PIMS. /// </summary> /// <param name="project"></param> /// <param name="model"></param> /// <returns></returns> private Entity.Project Merge(Entity.Project project, Model.ImportProjectModel model) { var status = GetStatus(model.Status); // Default to the last workflow this status is associated with. // This isn't ideal and could end up with projects associated with an incorrect workflow. // Without better data from SIS it isn't entirely possible to determine the correct workflow automatically. var workflow = !String.IsNullOrWhiteSpace(model.Workflow) ? GetWorkflow(model.Workflow) : model.Activity switch { "Completed Deal" => GetWorkflow("SPL"), "Contract in Place" => GetWorkflow("SPL"), "On Market" => GetWorkflow("SPL"), "Pre-Market" => GetWorkflow("SPL"), _ => _adminService.Workflow.GetForStatus(model.Status).OrderBy(w => w.SortOrder).Last() }; project ??= new Entity.Project(); project.ProjectNumber = String.IsNullOrWhiteSpace(model.ProjectNumber) ? $"TEMP-{DateTime.UtcNow.Ticks:00000}" : model.ProjectNumber; project.Name = model.Description.Truncate(100); project.Description = model.Description; project.Manager = model.Manager; project.ActualFiscalYear = model.ActualFiscalYear; project.ReportedFiscalYear = model.ReportedFiscalYear; project.Agency = GetAgency(model.Agency); project.AgencyId = project.Agency.Id; project.Workflow = workflow; project.WorkflowId = workflow.Id; project.Status = status; project.StatusId = status.Id; // Extract properties from PID note. var pidNote = model.Notes?.FirstOrDefault(n => n.Key == "PID").Value; var pids = Regex.Matches(pidNote ?? "", "[0-9]{3}-[0-9]{3}-[0-9]{3}").Select(m => m.Value).NotNull().Distinct(); if (!String.IsNullOrWhiteSpace(pidNote)) { // Need to load any properties currently linked to this project. if (project.Id > 0) { var existingProject = _service.Project.Get(project.ProjectNumber); pids.ForEach(pid => { // If the parcel has not already been added, add it to the project. var addProperty = true; foreach (var property in existingProject.Properties.Where(p => p.PropertyType == Entity.PropertyTypes.Land)) { var parcel = _service.Parcel.Get(property.ParcelId.Value); if (parcel.ParcelIdentity == pid) { addProperty = false; break; } } if (addProperty) { AddProperty(project, pid); } }); } else { pids.ForEach(pid => AddProperty(project, pid)); } } project.TierLevel = GetTier(model.Market, pids.Any() ? pids.Count() : project.Properties.Count()); // Most projects have no properties linked. project.TierLevelId = project.TierLevel.Id; project.Risk = GetRisk(model.Risk); project.RiskId = project.Risk.Id; // If there are properties linked, load them into context. decimal assessed = 0; if (project.Properties.Any()) { project.Properties.Where(p => p.PropertyType == Entity.PropertyTypes.Land).ForEach(p => { var property = _service.Parcel.Get(p.ParcelId.Value); assessed += property.Evaluations.Where(e => e.Key == Entity.EvaluationKeys.Assessed).OrderByDescending(e => e.Date).FirstOrDefault()?.Value ?? 0; }); } project.Assessed = model.Assessed ?? assessed; project.NetBook = model.NetBook; project.Market = model.Market; project.Appraised = model.Appraised; project.SubmittedOn = new DateTime(model.ReportedFiscalYear - 1, 1, 1); // Don't have a source for this information. project.ApprovedOn = project.SubmittedOn; // Don't have a source for this information. project.CompletedOn = model.CompletedOn; var metadata = new Entity.Models.DisposalProjectSnapshotMetadata { InitialNotificationSentOn = null, // Don't have a source for this information. ThirtyDayNotificationSentOn = null, // Don't have a source for this information. SixtyDayNotificationSentOn = null, // Don't have a source for this information. NinetyDayNotificationSentOn = null, // Don't have a source for this information. OnHoldNotificationSentOn = null, // Don't have a source for this information. InterestedReceivedOn = model.InterestedReceivedOn, TransferredWithinGreOn = null, // Don't have a source for this information. ClearanceNotificationSentOn = null, // Don't have a source for this information. RequestForSplReceivedOn = model.RequestForSplReceivedOn, // Don't have a source for this information. ApprovedForSplOn = model.ApprovedForSplOn, MarketedOn = model.MarketedOn, Purchaser = model.Purchaser, OcgFinancialStatement = model.OcgFinancialStatement, AppraisedBy = model.AppraisedBy, ProgramCost = model.ProgramCost, SalesCost = model.SalesCost, InterestComponent = model.InterestComponent, NetProceeds = model.NetProceeds, GainLoss = model.GainLoss, SaleWithLeaseInPlace = model.SaleWithLeaseInPlace, DisposedOn = model.DisposedOn ?? (project.Status.Code == "DIS" ? model.CompletedOn : null), OfferAmount = project.Status.Code == "DIS" ? model.Market : (decimal?)null, // This value would only be accurate if the property is disposed. OfferAcceptedOn = null // Don't have a source for this information. }; // A prior net proceeds was provided, which means a prior snapshot needs to be generated. // If the project already exists, don't add prior snapshots. if (model.PriorNetProceeds.HasValue && project.Id == 0) { AddSnapshot(project, model, metadata); } project.Metadata = JsonSerializer.Serialize(metadata, _serializerOptions); // The data doesn't provide the purchasing agency information so the response will be the current owning agency. if (model.InterestedReceivedOn.HasValue) { var response = project.Responses.FirstOrDefault(r => r.AgencyId == project.AgencyId); if (response == null) { project.Responses.Add(new Entity.ProjectAgencyResponse(project, project.Agency, Entity.NotificationResponses.Watch, model.InterestedReceivedOn)); } else { response.Response = Entity.NotificationResponses.Watch; response.ReceivedOn = model.InterestedReceivedOn ?? response.ReceivedOn; } } foreach (var note in model.Notes) { AddNote(project, note.Key, note.Value); } // Add tasks if they haven't been added already. if (!project.Tasks.Any()) { // Assumption we'll need to add all tasks from Submit to SPL. var tasks = _service.Task.GetForWorkflow("SUBMIT-DISPOSAL"); tasks = tasks.Concat(_service.Task.GetForWorkflow("ASSESS-DISPOSAL")); var distinctTasks = tasks.DistinctBy(t => t.Id).ToArray(); project.AddTask(distinctTasks); project.Tasks.ForEach(t => { t.IsCompleted = !t.Task.IsOptional; // Complete all required steps. t.Task = null; }); var erpTasks = _service.Task.GetForWorkflow("ERP"); var splTasks = erpTasks.Concat(_service.Task.GetForWorkflow("SPL")); distinctTasks = splTasks.Where(t => !distinctTasks.Any(dt => dt.Id == t.Id)).DistinctBy(t => t.Id).ToArray(); project.AddTask(distinctTasks); project.Tasks.ForEach(t => { t.IsCompleted = false; t.Task = null; }); } _logger.LogDebug($"Parsed project '{project.ProjectNumber}' - '{project.Status.Code}'", project); return(project); }