From 98d9157aa14df113947d227ce57bbc48ceba25a7 Mon Sep 17 00:00:00 2001 From: James Gunn Date: Mon, 8 Jan 2024 15:33:29 +0000 Subject: [PATCH] Sync MQ audit history --- .../Postgres/Models/MandatoryQualification.cs | 49 +- .../Dqt/DateTimeExtensions.cs | 7 +- .../Dqt/Models/GeneratedCode.cs | 423 ++++++++++++++++-- .../Dqt/Models/GeneratedOptionSets.cs | 374 ++++++++++++++++ .../MandatoryQualificationCreatedEvent.cs | 9 + ...ndatoryQualificationDqtReactivatedEvent.cs | 9 + .../MandatoryQualificationUpdatedEvent.cs | 21 + .../Jobs/SyncAllMqsFromCrmJob.cs | 2 + .../Jobs/SyncAllPersonsFromCrmJob.cs | 2 + .../HostApplicationBuilderExtensions.cs | 33 +- .../Services/TrsDataSync/TrsDataSyncHelper.cs | 414 ++++++++++++++++- .../Persons/PersonDetail/ChangeLog.cshtml.cs | 6 +- .../appsettings.Production.json | 1 - ...aSyncHelperTests.MandatoryQualification.cs | 15 +- .../Services/TrsDataSync/zzAuditTests.cs | 83 ++++ crm_attributes.json | 11 + tools/coretools/CrmSvcUtil.exe.config | 4 +- 17 files changed, 1340 insertions(+), 123 deletions(-) create mode 100644 TeachingRecordSystem/src/TeachingRecordSystem.Core/Events/MandatoryQualificationCreatedEvent.cs create mode 100644 TeachingRecordSystem/src/TeachingRecordSystem.Core/Events/MandatoryQualificationDqtReactivatedEvent.cs create mode 100644 TeachingRecordSystem/src/TeachingRecordSystem.Core/Events/MandatoryQualificationUpdatedEvent.cs create mode 100644 TeachingRecordSystem/tests/TeachingRecordSystem.Core.Tests/Services/TrsDataSync/zzAuditTests.cs diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Models/MandatoryQualification.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Models/MandatoryQualification.cs index fe84c6d459..4d78483755 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Models/MandatoryQualification.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/DataStore/Postgres/Models/MandatoryQualification.cs @@ -1,5 +1,5 @@ using TeachingRecordSystem.Core.Dqt; -using TeachingRecordSystem.Core.Events; +using TeachingRecordSystem.Core.Services.TrsDataSync; namespace TeachingRecordSystem.Core.DataStore.Postgres.Models; @@ -19,51 +19,6 @@ public static async Task MapFromDqtQualification(dfeta_q var mqEstablishments = await referenceDataCache.GetMqEstablishments(); var mqSpecialisms = await referenceDataCache.GetMqSpecialisms(); - return MapFromDqtQualification(qualification, mqEstablishments, mqSpecialisms); - } - - public static MandatoryQualification MapFromDqtQualification( - dfeta_qualification qualification, - IEnumerable mqEstablishments, - IEnumerable mqSpecialisms) - { - if (qualification.dfeta_Type != dfeta_qualification_dfeta_Type.MandatoryQualification) - { - throw new ArgumentException("Qualification is not a mandatory qualification.", nameof(qualification)); - } - - var deletedEvent = qualification.dfeta_TrsDeletedEvent is not null and not "{}" ? - EventInfo.Deserialize(qualification.dfeta_TrsDeletedEvent).Event : - null; - - MandatoryQualificationProvider.TryMapFromDqtMqEstablishment( - mqEstablishments.SingleOrDefault(e => e.Id == qualification.dfeta_MQ_MQEstablishmentId!?.Id), out var provider); - - MandatoryQualificationSpecialism? specialism = qualification.dfeta_MQ_SpecialismId is not null ? - mqSpecialisms.Single(s => s.Id == qualification.dfeta_MQ_SpecialismId.Id).ToMandatoryQualificationSpecialism() : - null; - - MandatoryQualificationStatus? status = qualification.dfeta_MQ_Status?.ToMandatoryQualificationStatus() ?? - (qualification.dfeta_MQ_Date.HasValue ? MandatoryQualificationStatus.Passed : null); - - return new MandatoryQualification() - { - QualificationId = qualification.Id, - CreatedOn = qualification.CreatedOn!.Value, - UpdatedOn = qualification.ModifiedOn!.Value, - DeletedOn = deletedEvent?.CreatedUtc, - PersonId = qualification.dfeta_PersonId.Id, - DqtQualificationId = qualification.Id, - DqtState = (int)qualification.StateCode!, - DqtCreatedOn = qualification.CreatedOn!.Value, - DqtModifiedOn = qualification.ModifiedOn!.Value, - ProviderId = provider?.MandatoryQualificationProviderId, - Specialism = specialism, - Status = status, - StartDate = qualification.dfeta_MQStartDate.ToDateOnlyWithDqtBstFix(isLocalTime: true), - EndDate = qualification.dfeta_MQ_Date.ToDateOnlyWithDqtBstFix(isLocalTime: true), - DqtMqEstablishmentId = qualification.dfeta_MQ_MQEstablishmentId?.Id, - DqtSpecialismId = qualification.dfeta_MQ_SpecialismId?.Id - }; + return TrsDataSyncHelper.MapMandatoryQualificationFromDqtQualification(qualification, mqEstablishments, mqSpecialisms); } } diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/DateTimeExtensions.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/DateTimeExtensions.cs index 631b204384..112666517d 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/DateTimeExtensions.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/DateTimeExtensions.cs @@ -19,6 +19,9 @@ public static DateOnly ToDateOnlyWithDqtBstFix(this DateTime dateTime, bool isLo public static DateOnly? ToDateOnlyWithDqtBstFix(this DateTime? dateTime, bool isLocalTime) => dateTime.HasValue ? ToDateOnlyWithDqtBstFix(dateTime.Value, isLocalTime) : null; - public static DateTime? WithDqtBstFix(this DateTime? dateTime, bool isLocalTime) => - dateTime.HasValue ? (isLocalTime ? TimeZoneInfo.ConvertTimeFromUtc(dateTime.Value, _gmt) : dateTime.Value) : null; + public static DateTime? ToUtc(this DateTime dateTime) => + TimeZoneInfo.ConvertTimeToUtc(DateTime.SpecifyKind(dateTime, DateTimeKind.Unspecified), _gmt); + + public static DateTime? ToUtc(this DateTime? dateTime) => + dateTime.HasValue ? ToUtc(dateTime.Value) : null; } diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/Models/GeneratedCode.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/Models/GeneratedCode.cs index 216e826d70..c5e44226e2 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/Models/GeneratedCode.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/Models/GeneratedCode.cs @@ -1302,6 +1302,299 @@ public TeachingRecordSystem.Core.Dqt.Models.Task Task_Annotation } } + /// + /// Track changes to records for analysis, record keeping, and compliance. + /// + [System.Runtime.Serialization.DataContractAttribute()] + [Microsoft.Xrm.Sdk.Client.EntityLogicalNameAttribute("audit")] + public partial class Audit : Microsoft.Xrm.Sdk.Entity, System.ComponentModel.INotifyPropertyChanging, System.ComponentModel.INotifyPropertyChanged + { + + /// + /// Available fields, a the time of codegen, for the audit entity + /// + public static class Fields + { + public const string Action = "action"; + public const string AuditId = "auditid"; + public const string Id = "auditid"; + public const string CallingUserId = "callinguserid"; + public const string CreatedOn = "createdon"; + public const string ObjectId = "objectid"; + public const string ObjectTypeCode = "objecttypecode"; + public const string Operation = "operation"; + public const string UserId = "userid"; + public const string lk_audit_callinguserid = "lk_audit_callinguserid"; + public const string lk_audit_userid = "lk_audit_userid"; + } + + /// + /// Default Constructor. + /// + [System.Diagnostics.DebuggerNonUserCode()] + public Audit() : + base(EntityLogicalName) + { + } + + public const string EntitySchemaName = "Audit"; + + public const string PrimaryIdAttribute = "auditid"; + + public const string EntityLogicalName = "audit"; + + public const string EntityLogicalCollectionName = "audits"; + + public const string EntitySetName = "audits"; + + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + + public event System.ComponentModel.PropertyChangingEventHandler PropertyChanging; + + [System.Diagnostics.DebuggerNonUserCode()] + private void OnPropertyChanged(string propertyName) + { + if ((this.PropertyChanged != null)) + { + this.PropertyChanged(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName)); + } + } + + [System.Diagnostics.DebuggerNonUserCode()] + private void OnPropertyChanging(string propertyName) + { + if ((this.PropertyChanging != null)) + { + this.PropertyChanging(this, new System.ComponentModel.PropertyChangingEventArgs(propertyName)); + } + } + + /// + /// Actions the user can perform that cause a change + /// + [Microsoft.Xrm.Sdk.AttributeLogicalNameAttribute("action")] + public virtual Audit_Action? Action + { + [System.Diagnostics.DebuggerNonUserCode()] + get + { + return ((Audit_Action?)(EntityOptionSetEnum.GetEnum(this, "action"))); + } + [System.Diagnostics.DebuggerNonUserCode()] + set + { + this.OnPropertyChanging("Action"); + this.SetAttributeValue("action", value.HasValue ? new Microsoft.Xrm.Sdk.OptionSetValue((int)value) : null); + this.OnPropertyChanged("Action"); + } + } + + /// + /// Unique identifier of the auditing instance + /// + [Microsoft.Xrm.Sdk.AttributeLogicalNameAttribute("auditid")] + public System.Nullable AuditId + { + [System.Diagnostics.DebuggerNonUserCode()] + get + { + return this.GetAttributeValue>("auditid"); + } + [System.Diagnostics.DebuggerNonUserCode()] + set + { + this.OnPropertyChanging("AuditId"); + this.SetAttributeValue("auditid", value); + if (value.HasValue) + { + base.Id = value.Value; + } + else + { + base.Id = System.Guid.Empty; + } + this.OnPropertyChanged("AuditId"); + } + } + + [Microsoft.Xrm.Sdk.AttributeLogicalNameAttribute("auditid")] + public override System.Guid Id + { + [System.Diagnostics.DebuggerNonUserCode()] + get + { + return base.Id; + } + [System.Diagnostics.DebuggerNonUserCode()] + set + { + this.AuditId = value; + } + } + + /// + /// Unique identifier of the calling user in case of an impersonated call + /// + [Microsoft.Xrm.Sdk.AttributeLogicalNameAttribute("callinguserid")] + public Microsoft.Xrm.Sdk.EntityReference CallingUserId + { + [System.Diagnostics.DebuggerNonUserCode()] + get + { + return this.GetAttributeValue("callinguserid"); + } + [System.Diagnostics.DebuggerNonUserCode()] + set + { + this.OnPropertyChanging("CallingUserId"); + this.SetAttributeValue("callinguserid", value); + this.OnPropertyChanged("CallingUserId"); + } + } + + /// + /// Date and time when the audit record was created. + /// + [Microsoft.Xrm.Sdk.AttributeLogicalNameAttribute("createdon")] + public System.Nullable CreatedOn + { + [System.Diagnostics.DebuggerNonUserCode()] + get + { + return this.GetAttributeValue>("createdon"); + } + [System.Diagnostics.DebuggerNonUserCode()] + set + { + this.OnPropertyChanging("CreatedOn"); + this.SetAttributeValue("createdon", value); + this.OnPropertyChanged("CreatedOn"); + } + } + + /// + /// Unique identifier of the record that is being audited + /// + [Microsoft.Xrm.Sdk.AttributeLogicalNameAttribute("objectid")] + public Microsoft.Xrm.Sdk.EntityReference ObjectId + { + [System.Diagnostics.DebuggerNonUserCode()] + get + { + return this.GetAttributeValue("objectid"); + } + [System.Diagnostics.DebuggerNonUserCode()] + set + { + this.OnPropertyChanging("ObjectId"); + this.SetAttributeValue("objectid", value); + this.OnPropertyChanged("ObjectId"); + } + } + + /// + /// Unique identifier of the entity that is being audited + /// + [Microsoft.Xrm.Sdk.AttributeLogicalNameAttribute("objecttypecode")] + public string ObjectTypeCode + { + [System.Diagnostics.DebuggerNonUserCode()] + get + { + return this.GetAttributeValue("objecttypecode"); + } + [System.Diagnostics.DebuggerNonUserCode()] + set + { + this.OnPropertyChanging("ObjectTypeCode"); + this.SetAttributeValue("objecttypecode", value); + this.OnPropertyChanged("ObjectTypeCode"); + } + } + + /// + /// The action that causes the audit--it will be create, delete, update, upsert or archive + /// + [Microsoft.Xrm.Sdk.AttributeLogicalNameAttribute("operation")] + public virtual Audit_Operation? Operation + { + [System.Diagnostics.DebuggerNonUserCode()] + get + { + return ((Audit_Operation?)(EntityOptionSetEnum.GetEnum(this, "operation"))); + } + [System.Diagnostics.DebuggerNonUserCode()] + set + { + this.OnPropertyChanging("Operation"); + this.SetAttributeValue("operation", value.HasValue ? new Microsoft.Xrm.Sdk.OptionSetValue((int)value) : null); + this.OnPropertyChanged("Operation"); + } + } + + /// + /// Unique identifier of the user who caused a change + /// + [Microsoft.Xrm.Sdk.AttributeLogicalNameAttribute("userid")] + public Microsoft.Xrm.Sdk.EntityReference UserId + { + [System.Diagnostics.DebuggerNonUserCode()] + get + { + return this.GetAttributeValue("userid"); + } + [System.Diagnostics.DebuggerNonUserCode()] + set + { + this.OnPropertyChanging("UserId"); + this.SetAttributeValue("userid", value); + this.OnPropertyChanged("UserId"); + } + } + + /// + /// N:1 lk_audit_callinguserid + /// + [Microsoft.Xrm.Sdk.AttributeLogicalNameAttribute("callinguserid")] + [Microsoft.Xrm.Sdk.RelationshipSchemaNameAttribute("lk_audit_callinguserid")] + public TeachingRecordSystem.Core.Dqt.Models.SystemUser lk_audit_callinguserid + { + [System.Diagnostics.DebuggerNonUserCode()] + get + { + return this.GetRelatedEntity("lk_audit_callinguserid", null); + } + [System.Diagnostics.DebuggerNonUserCode()] + set + { + this.OnPropertyChanging("lk_audit_callinguserid"); + this.SetRelatedEntity("lk_audit_callinguserid", null, value); + this.OnPropertyChanged("lk_audit_callinguserid"); + } + } + + /// + /// N:1 lk_audit_userid + /// + [Microsoft.Xrm.Sdk.AttributeLogicalNameAttribute("userid")] + [Microsoft.Xrm.Sdk.RelationshipSchemaNameAttribute("lk_audit_userid")] + public TeachingRecordSystem.Core.Dqt.Models.SystemUser lk_audit_userid + { + [System.Diagnostics.DebuggerNonUserCode()] + get + { + return this.GetRelatedEntity("lk_audit_userid", null); + } + [System.Diagnostics.DebuggerNonUserCode()] + set + { + this.OnPropertyChanging("lk_audit_userid"); + this.SetRelatedEntity("lk_audit_userid", null, value); + this.OnPropertyChanged("lk_audit_userid"); + } + } + } + /// /// Business, division, or department in the Microsoft Dynamics 365 database. /// @@ -8553,8 +8846,8 @@ public partial class dfeta_qtsregistration : Microsoft.Xrm.Sdk.Entity, System.Co /// public static class Fields { - public const string CreatedOn = "createdon"; - public const string dfeta_EarlyYearsStatusId = "dfeta_earlyyearsstatusid"; + public const string CreatedOn = "createdon"; + public const string dfeta_EarlyYearsStatusId = "dfeta_earlyyearsstatusid"; public const string dfeta_EYTSDate = "dfeta_eytsdate"; public const string dfeta_InductionId = "dfeta_inductionid"; public const string dfeta_name = "dfeta_name"; @@ -8619,6 +8912,26 @@ private void OnPropertyChanging(string propertyName) } } + /// + /// Date and time when the record was created. + /// + [Microsoft.Xrm.Sdk.AttributeLogicalNameAttribute("createdon")] + public System.Nullable CreatedOn + { + [System.Diagnostics.DebuggerNonUserCode()] + get + { + return this.GetAttributeValue>("createdon"); + } + [System.Diagnostics.DebuggerNonUserCode()] + set + { + this.OnPropertyChanging("CreatedOn"); + this.SetAttributeValue("createdon", value); + this.OnPropertyChanged("CreatedOn"); + } + } + /// /// Unique identifier for Early Years Status associated with QTS Registration. /// @@ -8638,32 +8951,11 @@ public Microsoft.Xrm.Sdk.EntityReference dfeta_EarlyYearsStatusId this.OnPropertyChanged("dfeta_EarlyYearsStatusId"); } } - - /// - /// Date and time when the record was created. - /// - [Microsoft.Xrm.Sdk.AttributeLogicalNameAttribute("createdon")] - public System.Nullable CreatedOn - { - [System.Diagnostics.DebuggerNonUserCode()] - get - { - return this.GetAttributeValue>("createdon"); - } - [System.Diagnostics.DebuggerNonUserCode()] - set - { - this.OnPropertyChanging("CreatedOn"); - this.SetAttributeValue("createdon", value); - this.OnPropertyChanged("CreatedOn"); - } - } - - - /// - /// - /// - [Microsoft.Xrm.Sdk.AttributeLogicalNameAttribute("dfeta_eytsdate")] + + /// + /// + /// + [Microsoft.Xrm.Sdk.AttributeLogicalNameAttribute("dfeta_eytsdate")] public System.Nullable dfeta_EYTSDate { [System.Diagnostics.DebuggerNonUserCode()] @@ -9091,6 +9383,7 @@ public partial class dfeta_qualification : Microsoft.Xrm.Sdk.Entity, System.Comp /// public static class Fields { + public const string CreatedBy = "createdby"; public const string CreatedOn = "createdon"; public const string dfeta_CompletionorAwardDate = "dfeta_completionorawarddate"; public const string dfeta_createdbyapi = "dfeta_createdbyapi"; @@ -9193,6 +9486,26 @@ private void OnPropertyChanging(string propertyName) } } + /// + /// Unique identifier of the user who created the record. + /// + [Microsoft.Xrm.Sdk.AttributeLogicalNameAttribute("createdby")] + public Microsoft.Xrm.Sdk.EntityReference CreatedBy + { + [System.Diagnostics.DebuggerNonUserCode()] + get + { + return this.GetAttributeValue("createdby"); + } + [System.Diagnostics.DebuggerNonUserCode()] + set + { + this.OnPropertyChanging("CreatedBy"); + this.SetAttributeValue("createdby", value); + this.OnPropertyChanged("CreatedBy"); + } + } + /// /// Date and time when the record was created. /// @@ -18385,6 +18698,8 @@ public static class Fields public const string lk_annotationbase_createdonbehalfby = "lk_annotationbase_createdonbehalfby"; public const string lk_annotationbase_modifiedby = "lk_annotationbase_modifiedby"; public const string lk_annotationbase_modifiedonbehalfby = "lk_annotationbase_modifiedonbehalfby"; + public const string lk_audit_callinguserid = "lk_audit_callinguserid"; + public const string lk_audit_userid = "lk_audit_userid"; public const string lk_businessunit_createdonbehalfby = "lk_businessunit_createdonbehalfby"; public const string lk_businessunit_modifiedonbehalfby = "lk_businessunit_modifiedonbehalfby"; public const string lk_businessunitbase_createdby = "lk_businessunitbase_createdby"; @@ -19059,6 +19374,46 @@ public System.Collections.Generic.IEnumerable + /// 1:N lk_audit_callinguserid + /// + [Microsoft.Xrm.Sdk.RelationshipSchemaNameAttribute("lk_audit_callinguserid")] + public System.Collections.Generic.IEnumerable lk_audit_callinguserid + { + [System.Diagnostics.DebuggerNonUserCode()] + get + { + return this.GetRelatedEntities("lk_audit_callinguserid", null); + } + [System.Diagnostics.DebuggerNonUserCode()] + set + { + this.OnPropertyChanging("lk_audit_callinguserid"); + this.SetRelatedEntities("lk_audit_callinguserid", null, value); + this.OnPropertyChanged("lk_audit_callinguserid"); + } + } + + /// + /// 1:N lk_audit_userid + /// + [Microsoft.Xrm.Sdk.RelationshipSchemaNameAttribute("lk_audit_userid")] + public System.Collections.Generic.IEnumerable lk_audit_userid + { + [System.Diagnostics.DebuggerNonUserCode()] + get + { + return this.GetRelatedEntities("lk_audit_userid", null); + } + [System.Diagnostics.DebuggerNonUserCode()] + set + { + this.OnPropertyChanging("lk_audit_userid"); + this.SetRelatedEntities("lk_audit_userid", null, value); + this.OnPropertyChanged("lk_audit_userid"); + } + } + /// /// 1:N lk_businessunit_createdonbehalfby /// @@ -22570,6 +22925,18 @@ public System.Linq.IQueryable A } } + /// + /// Gets a binding to the set of all entities. + /// + public System.Linq.IQueryable AuditSet + { + [System.Diagnostics.DebuggerNonUserCode()] + get + { + return this.CreateQuery(); + } + } + /// /// Gets a binding to the set of all entities. /// diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/Models/GeneratedOptionSets.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/Models/GeneratedOptionSets.cs index 72af1b94aa..594e492c08 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/Models/GeneratedOptionSets.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Dqt/Models/GeneratedOptionSets.cs @@ -529,6 +529,380 @@ public enum activitypointer_DeliveryPriorityCode Normal = 1, } + [System.Runtime.Serialization.DataContractAttribute()] + public enum Audit_Action + { + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Activate", 4)] + Activate = 4, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Add Item", 31)] + AddItem = 37, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Add Member", 25)] + AddMember = 31, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Add Members", 29)] + AddMembers = 35, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Add Privileges to Role", 51)] + AddPrivilegestoRole = 57, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Add Substitute", 33)] + AddSubstitute = 39, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Add To Queue", 46)] + AddToQueue = 52, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Approve", 22)] + Approve = 28, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Archive", 75)] + Archive = 115, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Assign", 8)] + Assign = 13, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Assign Role To Team", 47)] + AssignRoleToTeam = 53, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Assign Role To User", 49)] + AssignRoleToUser = 55, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Associate Entities", 27)] + AssociateEntities = 33, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Attribute Audit Started", 66)] + AttributeAuditStarted = 106, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Attribute Audit Stopped", 69)] + AttributeAuditStopped = 109, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Audit Change at Attribute Level", 63)] + AuditChangeatAttributeLevel = 103, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Audit Change at Entity Level", 62)] + AuditChangeatEntityLevel = 102, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Audit Change at Org Level", 64)] + AuditChangeatOrgLevel = 104, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Audit Disabled", 70)] + AuditDisabled = 110, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Audit Enabled", 67)] + AuditEnabled = 107, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Audit Log Deletion", 71)] + AuditLogDeletion = 111, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Book", 44)] + Book = 50, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Cancel", 12)] + Cancel = 17, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Cascade", 6)] + Cascade = 11, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Clone", 55)] + Clone = 61, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Close", 11)] + Close = 16, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Complete", 13)] + Complete = 18, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Create", 1)] + Create = 1, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Deactivate", 5)] + Deactivate = 5, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Delete", 3)] + Delete = 3, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Delete Attribute", 61)] + DeleteAttribute = 101, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Delete Entity", 60)] + DeleteEntity = 100, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Disassociate Entities", 28)] + DisassociateEntities = 34, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Disqualify", 19)] + Disqualify = 25, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Enabled for organization", 57)] + Enabledfororganization = 63, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Entity Audit Started", 65)] + EntityAuditStarted = 105, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Entity Audit Stopped", 68)] + EntityAuditStopped = 108, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Fulfill", 16)] + Fulfill = 22, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Generate Quote From Opportunity", 45)] + GenerateQuoteFromOpportunity = 51, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Hold", 24)] + Hold = 30, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Import Mappings", 54)] + ImportMappings = 60, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Internal Processing", 40)] + InternalProcessing = 46, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Invoice", 23)] + Invoice = 29, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("IPFirewallAcccesAllowed", 79)] + IPFirewallAcccesAllowed = 119, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("IPFirewallAcccesDenied", 78)] + IPFirewallAcccesDenied = 118, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Lose", 39)] + Lose = 45, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Merge", 7)] + Merge = 12, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Modify Share", 42)] + ModifyShare = 48, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Paid", 17)] + Paid = 23, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Qualify", 18)] + Qualify = 24, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Reject", 21)] + Reject = 27, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Remove Item", 32)] + RemoveItem = 38, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Remove Member", 26)] + RemoveMember = 32, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Remove Members", 30)] + RemoveMembers = 36, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Remove Privileges From Role", 52)] + RemovePrivilegesFromRole = 58, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Remove Role From Team", 48)] + RemoveRoleFromTeam = 54, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Remove Role From User", 50)] + RemoveRoleFromUser = 56, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Remove Substitute", 34)] + RemoveSubstitute = 40, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Renew", 36)] + Renew = 42, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Reopen", 15)] + Reopen = 21, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Replace Privileges In Role", 53)] + ReplacePrivilegesInRole = 59, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Reschedule", 41)] + Reschedule = 47, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Resolve", 14)] + Resolve = 20, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Restore", 80)] + Restore = 120, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Retain", 76)] + Retain = 116, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Retrieve", 10)] + Retrieve = 15, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Revise", 37)] + Revise = 43, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("RollbackRetain", 77)] + RollbackRetain = 117, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Send Direct Email", 56)] + SendDirectEmail = 62, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Set State", 35)] + SetState = 41, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Share", 9)] + Share = 14, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Submit", 20)] + Submit = 26, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Unknown", 0)] + Unknown = 0, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Unshare", 43)] + Unshare = 49, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Update", 2)] + Update = 2, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Upsert", 74)] + Upsert = 6, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("User Access Audit Started", 72)] + UserAccessAuditStarted = 112, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("User Access Audit Stopped", 73)] + UserAccessAuditStopped = 113, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("User Access via Web", 58)] + UserAccessviaWeb = 64, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("User Access via Web Services", 59)] + UserAccessviaWebServices = 65, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Win", 38)] + Win = 44, + } + + [System.Runtime.Serialization.DataContractAttribute()] + public enum Audit_Operation + { + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Access", 3)] + Access = 4, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Archive", 5)] + Archive = 115, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Create", 0)] + Create = 1, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("CustomOperation", 9)] + CustomOperation = 200, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Delete", 2)] + Delete = 3, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Restore", 8)] + Restore = 118, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Retain", 6)] + Retain = 116, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("RollbackRetain", 7)] + RollbackRetain = 117, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Update", 1)] + Update = 2, + + [System.Runtime.Serialization.EnumMemberAttribute()] + [OptionSetMetadataAttribute("Upsert", 4)] + Upsert = 5, + } + [System.Runtime.Serialization.DataContractAttribute()] public enum BusinessUnit_Address1_AddressTypeCode { diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Events/MandatoryQualificationCreatedEvent.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Events/MandatoryQualificationCreatedEvent.cs new file mode 100644 index 0000000000..e433965757 --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Events/MandatoryQualificationCreatedEvent.cs @@ -0,0 +1,9 @@ +using TeachingRecordSystem.Core.Events.Models; + +namespace TeachingRecordSystem.Core.Events; + +public record MandatoryQualificationCreatedEvent : EventBase, IEventWithPersonId, IEventWithMandatoryQualification +{ + public required Guid PersonId { get; init; } + public required MandatoryQualification MandatoryQualification { get; init; } +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Events/MandatoryQualificationDqtReactivatedEvent.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Events/MandatoryQualificationDqtReactivatedEvent.cs new file mode 100644 index 0000000000..36fba5398d --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Events/MandatoryQualificationDqtReactivatedEvent.cs @@ -0,0 +1,9 @@ +using TeachingRecordSystem.Core.Events.Models; + +namespace TeachingRecordSystem.Core.Events; + +public record MandatoryQualificationDqtReactivatedEvent : EventBase, IEventWithPersonId, IEventWithMandatoryQualification +{ + public required Guid PersonId { get; init; } + public required MandatoryQualification MandatoryQualification { get; init; } +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Events/MandatoryQualificationUpdatedEvent.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Events/MandatoryQualificationUpdatedEvent.cs new file mode 100644 index 0000000000..76f5171de6 --- /dev/null +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Events/MandatoryQualificationUpdatedEvent.cs @@ -0,0 +1,21 @@ +using TeachingRecordSystem.Core.Events.Models; + +namespace TeachingRecordSystem.Core.Events; + +public record MandatoryQualificationUpdatedEvent : EventBase, IEventWithPersonId, IEventWithMandatoryQualification +{ + public required Guid PersonId { get; init; } + public required MandatoryQualification MandatoryQualification { get; init; } + public required MandatoryQualificationUpdatedEventChanges Changes { get; init; } +} + +[Flags] +public enum MandatoryQualificationUpdatedEventChanges +{ + None = 0, + Provider = 1 << 0, + Specialism = 1 << 2, + Status = 1 << 3, + StartDate = 1 << 4, + EndDate = 1 << 5, +} diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Jobs/SyncAllMqsFromCrmJob.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Jobs/SyncAllMqsFromCrmJob.cs index 99e56fb774..e60c68d7c1 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Jobs/SyncAllMqsFromCrmJob.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Jobs/SyncAllMqsFromCrmJob.cs @@ -1,3 +1,4 @@ +using Hangfire; using Microsoft.Extensions.Options; using Microsoft.Xrm.Sdk.Query; using TeachingRecordSystem.Core.Dqt; @@ -5,6 +6,7 @@ namespace TeachingRecordSystem.Core.Jobs; +[AutomaticRetry(Attempts = 0)] public class SyncAllMqsFromCrmJob { private readonly ICrmServiceClientProvider _crmServiceClientProvider; diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Jobs/SyncAllPersonsFromCrmJob.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Jobs/SyncAllPersonsFromCrmJob.cs index 899b2446e9..c9721d67a2 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Jobs/SyncAllPersonsFromCrmJob.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Jobs/SyncAllPersonsFromCrmJob.cs @@ -1,3 +1,4 @@ +using Hangfire; using Microsoft.Extensions.Options; using Microsoft.Xrm.Sdk.Query; using TeachingRecordSystem.Core.Dqt; @@ -5,6 +6,7 @@ namespace TeachingRecordSystem.Core.Jobs; +[AutomaticRetry(Attempts = 0)] public class SyncAllPersonsFromCrmJob { private readonly ICrmServiceClientProvider _crmServiceClientProvider; diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Services/TrsDataSync/HostApplicationBuilderExtensions.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Services/TrsDataSync/HostApplicationBuilderExtensions.cs index 97c4bb53da..9ebb51e7d5 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Services/TrsDataSync/HostApplicationBuilderExtensions.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Services/TrsDataSync/HostApplicationBuilderExtensions.cs @@ -14,31 +14,24 @@ public static IHostApplicationBuilder AddTrsSyncService(this IHostApplicationBui { var runService = builder.Configuration.GetValue("TrsSyncService:RunService"); - // The RegisterServiceClient option is because jobs that backfill data use the same named ServiceClient - // so that needs to be around, even if the TrsSyncService itself isn't running. + builder.Services.AddOptions() + .Bind(builder.Configuration.GetSection("TrsSyncService")) + .ValidateDataAnnotations() + .ValidateOnStart(); - if (runService || - builder.Configuration.GetValue("TrsSyncService:RegisterServiceClient")) + if (runService) { - builder.Services.AddOptions() - .Bind(builder.Configuration.GetSection("TrsSyncService")) - .ValidateDataAnnotations() - .ValidateOnStart(); - - if (runService) - { - builder.Services.AddSingleton(); - } + builder.Services.AddSingleton(); + } - builder.Services.AddNamedServiceClient( - TrsDataSyncService.CrmClientName, - ServiceLifetime.Singleton, - sp => new ServiceClient(sp.GetRequiredService>().Value.CrmConnectionString)); + builder.Services.AddNamedServiceClient( + TrsDataSyncService.CrmClientName, + ServiceLifetime.Singleton, + sp => new ServiceClient(sp.GetRequiredService>().Value.CrmConnectionString)); - builder.Services.AddCrmEntityChangesService(name: TrsDataSyncService.CrmClientName); + builder.Services.AddCrmEntityChangesService(name: TrsDataSyncService.CrmClientName); - builder.Services.TryAddSingleton(); - } + builder.Services.TryAddSingleton(); return builder; } diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Services/TrsDataSync/TrsDataSyncHelper.cs b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Services/TrsDataSync/TrsDataSyncHelper.cs index 991a613a29..593e749edd 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.Core/Services/TrsDataSync/TrsDataSyncHelper.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Core/Services/TrsDataSync/TrsDataSyncHelper.cs @@ -1,11 +1,14 @@ using System.Diagnostics; using System.Reactive.Linq; using System.Reactive.Subjects; +using System.ServiceModel; using System.Text.Json; +using Microsoft.Crm.Sdk.Messages; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.PowerPlatform.Dataverse.Client; using Microsoft.Xrm.Sdk; +using Microsoft.Xrm.Sdk.Messages; using Microsoft.Xrm.Sdk.Query; using Npgsql; using NpgsqlTypes; @@ -26,6 +29,7 @@ public class TrsDataSyncHelper( private const string NowParameterName = "@now"; private const string IdsParameterName = "@ids"; + private const int MaxAuditRequestsPerBatch = 10; private static readonly IReadOnlyDictionary _modelTypeSyncInfo = new Dictionary() { @@ -43,6 +47,46 @@ public static (string EntityLogicalName, string[] AttributeNames) GetEntityInfoF return (modelTypeSyncInfo.EntityLogicalName, modelTypeSyncInfo.AttributeNames); } + public static MandatoryQualification MapMandatoryQualificationFromDqtQualification( + dfeta_qualification qualification, + IEnumerable mqEstablishments, + IEnumerable mqSpecialisms) + { + if (qualification.dfeta_Type != dfeta_qualification_dfeta_Type.MandatoryQualification) + { + throw new ArgumentException("Qualification is not a mandatory qualification.", nameof(qualification)); + } + + MandatoryQualificationProvider.TryMapFromDqtMqEstablishment( + mqEstablishments.SingleOrDefault(e => e.Id == qualification.dfeta_MQ_MQEstablishmentId!?.Id), out var provider); + + MandatoryQualificationSpecialism? specialism = qualification.dfeta_MQ_SpecialismId is not null ? + mqSpecialisms.Single(s => s.Id == qualification.dfeta_MQ_SpecialismId.Id).ToMandatoryQualificationSpecialism() : + null; + + MandatoryQualificationStatus? status = qualification.dfeta_MQ_Status?.ToMandatoryQualificationStatus() ?? + (qualification.dfeta_MQ_Date.HasValue ? MandatoryQualificationStatus.Passed : null); + + return new MandatoryQualification() + { + QualificationId = qualification.Id, + CreatedOn = qualification.CreatedOn.ToUtc()!.Value, + UpdatedOn = qualification.ModifiedOn.ToUtc()!.Value, + PersonId = qualification.dfeta_PersonId.Id, + DqtQualificationId = qualification.Id, + DqtState = (int)qualification.StateCode!, + DqtCreatedOn = qualification.CreatedOn.ToUtc()!.Value, + DqtModifiedOn = qualification.ModifiedOn.ToUtc()!.Value, + ProviderId = provider?.MandatoryQualificationProviderId, + Specialism = specialism, + Status = status, + StartDate = qualification.dfeta_MQStartDate.ToDateOnlyWithDqtBstFix(isLocalTime: true), + EndDate = qualification.dfeta_MQ_Date.ToDateOnlyWithDqtBstFix(isLocalTime: true), + DqtMqEstablishmentId = qualification.dfeta_MQ_MQEstablishmentId?.Id, + DqtSpecialismId = qualification.dfeta_MQ_SpecialismId?.Id + }; + } + public async Task DeleteRecords(string modelType, IReadOnlyCollection ids, CancellationToken cancellationToken = default) { if (ids.Count == 0) @@ -93,7 +137,14 @@ public Task SyncRecords(string modelType, IReadOnlyCollection entities, public async Task SyncPerson(Guid contactId, CancellationToken cancellationToken) { var modelTypeSyncInfo = GetModelTypeSyncInfo(ModelTypes.Person); - var contacts = await GetEntities(Contact.EntityLogicalName, Contact.PrimaryIdAttribute, [contactId], modelTypeSyncInfo.AttributeNames, cancellationToken); + + var contacts = await GetEntities( + Contact.EntityLogicalName, + Contact.PrimaryIdAttribute, + [contactId], + modelTypeSyncInfo.AttributeNames, + cancellationToken); + return await SyncPersons(contacts, ignoreInvalid: false, cancellationToken) == 1; } @@ -194,27 +245,53 @@ public async Task SyncPersons(IReadOnlyCollection entities, bool i public async Task SyncMandatoryQualification(Guid qualificationId, CancellationToken cancellationToken) { var modelTypeSyncInfo = GetModelTypeSyncInfo(ModelTypes.MandatoryQualification); + var qualifications = await GetEntities( dfeta_qualification.EntityLogicalName, dfeta_qualification.PrimaryIdAttribute, [qualificationId], modelTypeSyncInfo.AttributeNames, cancellationToken); - return await SyncMandatoryQualifications(qualifications, ignoreInvalid: false, cancellationToken) == 1; + + var auditDetails = await GetAuditRecords(dfeta_qualification.EntityLogicalName, qualifications.Select(q => q.Id), cancellationToken); + + return await SyncMandatoryQualifications(qualifications, auditDetails, ignoreInvalid: false, cancellationToken) == 1; } - public async Task SyncMandatoryQualification(dfeta_qualification entity, bool ignoreInvalid, CancellationToken cancellationToken = default) => - await SyncMandatoryQualifications(new[] { entity }, ignoreInvalid, cancellationToken) == 1; + public async Task SyncMandatoryQualification( + dfeta_qualification entity, + AuditDetailCollection auditDetails, + bool ignoreInvalid, + CancellationToken cancellationToken = default) + { + var auditDetailsDict = new Dictionary() + { + { entity.Id, auditDetails } + }; + + return await SyncMandatoryQualifications(new[] { entity }, auditDetailsDict, ignoreInvalid, cancellationToken) == 1; + } + + public async Task SyncMandatoryQualifications( + IReadOnlyCollection entities, + bool ignoreInvalid, + CancellationToken cancellationToken) + { + var auditDetails = await GetAuditRecords(dfeta_qualification.EntityLogicalName, entities.Select(q => q.Id), cancellationToken); + + return await SyncMandatoryQualifications(entities, auditDetails, ignoreInvalid, cancellationToken); + } public async Task SyncMandatoryQualifications( IReadOnlyCollection entities, + IReadOnlyDictionary auditDetails, bool ignoreInvalid, CancellationToken cancellationToken = default) { // Not all dfeta_qualification records are MQs.. var toSync = entities.Where(q => q.dfeta_Type == dfeta_qualification_dfeta_Type.MandatoryQualification); - var (mqs, events) = await MapMandatoryQualifications(toSync); + var (mqs, events) = await MapMandatoryQualifications(toSync, auditDetails); if (mqs.Count == 0) { @@ -307,6 +384,135 @@ public async Task SyncMandatoryQualifications( return mqs.Count; } + private EntityVersionInfo[] GetEntityVersions(TEntity latest, IEnumerable auditDetails) + where TEntity : Entity + { + var created = latest.GetAttributeValue("createdon").ToUtc()!.Value; + var createdBy = latest.GetAttributeValue("createdby"); + + var ordered = auditDetails + .OfType() + .Select(a => (AuditDetail: a, AuditRecord: a.AuditRecord.ToEntity())) + .OrderBy(a => a.AuditRecord.CreatedOn) + .ToArray(); + + if (ordered.Length == 0) + { + return [new EntityVersionInfo(latest.Id, latest, ChangedAttributes: Array.Empty(), created, createdBy.Id, createdBy.Name)]; + } + + var versions = new List>(); + + var initialVersion = GetInitialVersion(); + versions.Add(new EntityVersionInfo(initialVersion.Id, initialVersion, ChangedAttributes: Array.Empty(), created, createdBy.Id, createdBy.Name)); + + latest = initialVersion.ShallowClone(); + foreach (var audit in ordered) + { + if (audit.AuditRecord.Action == Audit_Action.Create) + { + if (audit != ordered[0]) + { + throw new InvalidOperationException($"Expected the {Audit_Action.Create} audit to be first."); + } + + continue; + } + + var thisVersion = latest.ShallowClone(); + var changedAttributes = new List(); + + foreach (var attr in audit.AuditDetail.DeletedAttributes) + { + thisVersion.Attributes.Remove(attr.Value); + changedAttributes.Add(attr.Value); + } + + foreach (var attr in audit.AuditDetail.NewValue.Attributes) + { + thisVersion.Attributes[attr.Key] = attr.Value; + changedAttributes.Add(attr.Key); + } + + versions.Add(new EntityVersionInfo( + audit.AuditRecord.Id, + thisVersion, + changedAttributes.ToArray(), + audit.AuditRecord.CreatedOn.ToUtc()!.Value, + audit.AuditRecord.UserId.Id, + audit.AuditRecord.UserId.Name)); + + latest = thisVersion; + } + + return versions.ToArray(); + + TEntity GetInitialVersion() + { + if (ordered[0] is { AuditRecord: { Action: Audit_Action.Create } } createAction) + { + var entity = createAction.AuditDetail.NewValue.ToEntity(); + entity.Id = latest.Id; + entity["createdon"] = created; + entity["createdby"] = createdBy; + entity["modifiedon"] = created; + return entity; + } + + // Starting with `latest`, go through each event in reverse and undo the changes it applied. + // When we're done we end up with the initial version of the record. + var initial = latest.ShallowClone(); + + foreach (var a in ordered.Reverse()) + { + // Check that new new attributes align with what we have in `initial`; + // if they don't, then we've got an incomplete history + foreach (var attr in a.AuditDetail.NewValue.Attributes) + { + if (!AttributeValuesEqual(attr.Value, initial.Attributes[attr.Key])) + { + throw new Exception($"Non-contiguous audit records for {initial.LogicalName} '{initial.Id}':\n" + + $"Expected '{attr.Key}' to be '{attr.Value}' but was '{initial.Attributes[attr.Key]}'."); + } + + if (!a.AuditDetail.OldValue.Attributes.Contains(attr.Key)) + { + initial.Attributes.Remove(attr.Key); + } + } + + foreach (var attr in a.AuditDetail.OldValue.Attributes) + { + initial.Attributes[attr.Key] = attr.Value; + } + } + + return initial; + } + + static bool AttributeValuesEqual(object? first, object? second) + { + if (first is null && second is null) + { + return true; + } + + if (first is null || second is null) + { + return false; + } + + if (first.GetType() != second.GetType()) + { + return false; + } + + return first is EntityReference firstRef && second is EntityReference secondRef ? + firstRef.Name == secondRef.Name && firstRef.Id == secondRef.Id : + first.Equals(second); + } + } + private static ModelTypeSyncInfo GetModelTypeSyncInfo(string modelType) => (ModelTypeSyncInfo)GetModelTypeSyncInfo(modelType); @@ -315,10 +521,47 @@ private static ModelTypeSyncInfo GetModelTypeSyncInfo(string modelType) => modelTypeSyncInfo : throw new ArgumentException($"Unknown data type: '{modelType}.", nameof(modelType)); + private async Task> GetAuditRecords( + string entityLogicalName, + IEnumerable ids, + CancellationToken cancellationToken) + { + return (await Task.WhenAll(ids + .Chunk(MaxAuditRequestsPerBatch) + .Select(async chunk => + { + var request = new ExecuteMultipleRequest() + { + Requests = new(), + Settings = new() + { + ContinueOnError = false, + ReturnResponses = true + } + }; + + request.Requests.AddRange(chunk.Select(e => new RetrieveRecordChangeHistoryRequest() { Target = e.ToEntityReference(entityLogicalName) })); + + var response = (ExecuteMultipleResponse)await organizationService.ExecuteAsync(request, cancellationToken); + + if (response.IsFaulted) + { + var firstFault = response.Responses.First(r => r.Fault is not null).Fault; + throw new FaultException(firstFault, new FaultReason(firstFault.Message)); + } + + return response.Responses.Zip( + chunk, + (r, e) => (Id: e, ((RetrieveRecordChangeHistoryResponse)r.Response).AuditDetailCollection)); + }))) + .SelectMany(b => b) + .ToDictionary(t => t.Id, t => t.AuditDetailCollection); + } + private async Task GetEntities( string entityLogicalName, string idAttributeName, - Guid[] ids, + IEnumerable ids, string[] attributeNames, CancellationToken cancellationToken) where TEntity : Entity @@ -478,6 +721,7 @@ WHERE t.dqt_modified_on < EXCLUDED.dqt_modified_on { dfeta_qualification.Fields.dfeta_qualificationId, dfeta_qualification.Fields.CreatedOn, + dfeta_qualification.Fields.CreatedBy, dfeta_qualification.Fields.ModifiedOn, dfeta_qualification.Fields.dfeta_TrsDeletedEvent, dfeta_qualification.Fields.dfeta_Type, @@ -547,7 +791,8 @@ private static List MapPersons(IEnumerable contacts) => contact .ToList(); private async Task<(List MandatoryQualifications, List Events)> MapMandatoryQualifications( - IEnumerable qualifications) + IEnumerable qualifications, + IReadOnlyDictionary auditDetails) { var mqEstablishments = await referenceDataCache.GetMqEstablishments(); var mqSpecialisms = await referenceDataCache.GetMqSpecialisms(); @@ -557,19 +802,139 @@ private static List MapPersons(IEnumerable contacts) => contact foreach (var q in qualifications) { - var deletedEvent = q.dfeta_TrsDeletedEvent is not null and not "{}" ? - EventInfo.Deserialize(q.dfeta_TrsDeletedEvent).Event : - null; + var mapped = MapMandatoryQualificationFromDqtQualification(q, mqEstablishments, mqSpecialisms); - if (deletedEvent is not null) + var versions = GetEntityVersions(q, auditDetails[q.Id].AuditDetails); + events.Add(MapCreatedEvent(versions.First())); + events.AddRange(versions.Skip(1).Select(MapUpdatedEvent)); + + // If the record is deactivated, the final event should be a MandatoryQualificationDeletedEvent or MandatoryQualificationDqtDeactivatedEvent + if (q.StateCode == dfeta_qualificationState.Inactive) { - events.Add(deletedEvent); + var lastEvent = events.Last(); + + if (lastEvent is MandatoryQualificationDqtDeactivatedEvent or MandatoryQualificationDeletedEvent) + { + mapped.DeletedOn = lastEvent.CreatedUtc; + } + else + { + throw new InvalidOperationException( + $"Expected last event to be a {nameof(MandatoryQualificationDqtDeactivatedEvent)} or {nameof(MandatoryQualificationDeletedEvent)} but was {lastEvent.GetEventName()}."); + } } - mqs.Add(MandatoryQualification.MapFromDqtQualification(q, mqEstablishments, mqSpecialisms)); + mqs.Add(mapped); } return (mqs, events); + + EventBase MapCreatedEvent(EntityVersionInfo snapshot) + { + return new MandatoryQualificationCreatedEvent() + { + EventId = snapshot.Id, + CreatedUtc = snapshot.Timestamp, + RaisedBy = Events.Models.RaisedByUserInfo.FromDqtUser(snapshot.UserId, snapshot.UserName), + PersonId = snapshot.Entity.dfeta_PersonId.Id, + MandatoryQualification = GetEventMandatoryQualification(snapshot.Entity) + }; + } + + EventBase MapUpdatedEvent(EntityVersionInfo snapshot) + { + if (snapshot.Entity.Attributes.ContainsKey(dfeta_qualification.Fields.dfeta_TrsDeletedEvent)) + { + return EventInfo.Deserialize(snapshot.Entity.dfeta_TrsDeletedEvent).Event; + } + + if (snapshot.ChangedAttributes.Contains(dfeta_qualification.Fields.StateCode)) + { + var nonStateAttributes = snapshot.ChangedAttributes + .Where(a => !(a is "statecode" or "statuscode")) + .ToArray(); + + if (nonStateAttributes.Length > 0) + { + throw new InvalidOperationException( + $"Expected state and status attributes to change in isolation but also received: {string.Join(", ", nonStateAttributes)}."); + } + + if (snapshot.Entity.StateCode == dfeta_qualificationState.Inactive) + { + return new MandatoryQualificationDqtDeactivatedEvent() + { + EventId = snapshot.Id, + CreatedUtc = snapshot.Timestamp, + RaisedBy = Events.Models.RaisedByUserInfo.FromDqtUser(snapshot.UserId, snapshot.UserName), + PersonId = snapshot.Entity.dfeta_PersonId.Id, + MandatoryQualification = GetEventMandatoryQualification(snapshot.Entity) + }; + } + else + { + return new MandatoryQualificationDqtReactivatedEvent() + { + EventId = snapshot.Id, + CreatedUtc = snapshot.Timestamp, + RaisedBy = Events.Models.RaisedByUserInfo.FromDqtUser(snapshot.UserId, snapshot.UserName), + PersonId = snapshot.Entity.dfeta_PersonId.Id, + MandatoryQualification = GetEventMandatoryQualification(snapshot.Entity) + }; + } + } + + var changes = MandatoryQualificationUpdatedEventChanges.None | + (snapshot.ChangedAttributes.Contains(dfeta_qualification.Fields.dfeta_HE_EstablishmentId) ? MandatoryQualificationUpdatedEventChanges.Provider : 0) | + (snapshot.ChangedAttributes.Contains(dfeta_qualification.Fields.dfeta_MQ_SpecialismId) ? MandatoryQualificationUpdatedEventChanges.Specialism : 0) | + (snapshot.ChangedAttributes.Contains(dfeta_qualification.Fields.dfeta_MQ_Status) ? MandatoryQualificationUpdatedEventChanges.Status : 0) | + (snapshot.ChangedAttributes.Contains(dfeta_qualification.Fields.dfeta_MQStartDate) ? MandatoryQualificationUpdatedEventChanges.StartDate : 0) | + (snapshot.ChangedAttributes.Contains(dfeta_qualification.Fields.dfeta_MQ_Date) ? MandatoryQualificationUpdatedEventChanges.EndDate : 0); + + if (changes == MandatoryQualificationUpdatedEventChanges.None) + { + throw new InvalidOperationException($"Received an audit event with no changes (id: '{snapshot.Id}')."); + } + + return new MandatoryQualificationUpdatedEvent() + { + EventId = snapshot.Id, + CreatedUtc = snapshot.Timestamp, + RaisedBy = Events.Models.RaisedByUserInfo.FromDqtUser(snapshot.UserId, snapshot.UserName), + PersonId = snapshot.Entity.dfeta_PersonId.Id, + MandatoryQualification = GetEventMandatoryQualification(snapshot.Entity), + Changes = changes + }; + } + + Events.Models.MandatoryQualification GetEventMandatoryQualification(dfeta_qualification snapshot) + { + var mapped = MapMandatoryQualificationFromDqtQualification(snapshot, mqEstablishments, mqSpecialisms); + + var establishment = snapshot.dfeta_MQ_MQEstablishmentId?.Id is Guid establishmentId ? + mqEstablishments.Single(e => e.Id == establishmentId) : + null; + + MandatoryQualificationProvider.TryMapFromDqtMqEstablishment(establishment, out var provider); + + return new() + { + QualificationId = mapped.QualificationId, + Provider = provider is not null || establishment is not null ? + new() + { + MandatoryQualificationProviderId = provider?.MandatoryQualificationProviderId, + Name = provider?.Name, + DqtMqEstablishmentId = establishment?.Id, + DqtMqEstablishmentName = establishment?.dfeta_name + } : + null, + Specialism = mapped.Specialism, + Status = mapped.Status, + StartDate = mapped.StartDate, + EndDate = mapped.EndDate, + }; + } } private async Task SyncEvents(IReadOnlyCollection events, NpgsqlTransaction transaction, CancellationToken cancellationToken) @@ -659,6 +1024,15 @@ private record ModelTypeSyncInfo : ModelTypeSyncInfo public required Action WriteRecord { get; init; } } + private record EntityVersionInfo( + Guid Id, + TEntity Entity, + string[] ChangedAttributes, + DateTime Timestamp, + Guid UserId, + string UserName) + where TEntity : Entity; + public static class ModelTypes { public const string Person = "Person"; @@ -668,6 +1042,20 @@ public static class ModelTypes file static class Extensions { + public static TEntity ShallowClone(this TEntity entity) where TEntity : Entity + { + // N.B. This only clones Attributes + + var cloned = new Entity(entity.LogicalName, entity.Id); + + foreach (var attr in entity.Attributes) + { + cloned.Attributes.Add(attr.Key, attr.Value); + } + + return cloned.ToEntity(); + } + /// /// Returns null if is empty or whitespace. /// diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/ChangeLog.cshtml.cs b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/ChangeLog.cshtml.cs index f7704185a1..e64e0122bd 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/ChangeLog.cshtml.cs +++ b/TeachingRecordSystem/src/TeachingRecordSystem.SupportUi/Pages/Persons/PersonDetail/ChangeLog.cshtml.cs @@ -86,15 +86,15 @@ AND e.event_name in ('MandatoryQualificationDeletedEvent', 'MandatoryQualificati TimelineItems = notesResult .Annotations.Select(n => (TimelineItem)new TimelineItem( TimelineItemType.Annotation, - n.ModifiedOn.WithDqtBstFix(isLocalTime: true)!.Value, + n.ModifiedOn.ToUtc()!.Value, n)) .Concat(notesResult.IncidentResolutions.Select(r => new TimelineItem<(IncidentResolution, Incident)>( TimelineItemType.IncidentResolution, - r.Resolution.ModifiedOn.WithDqtBstFix(isLocalTime: true)!.Value, + r.Resolution.ModifiedOn.ToUtc()!.Value, r))) .Concat(notesResult.Tasks.Select(t => new TimelineItem( TimelineItemType.Task, - t.ModifiedOn.WithDqtBstFix(isLocalTime: true)!.Value, + t.ModifiedOn.ToUtc()!.Value, t))) .Concat(eventsWithUser.Select(MapTimelineEvent)) .OrderByDescending(i => i.Timestamp) diff --git a/TeachingRecordSystem/src/TeachingRecordSystem.Worker/appsettings.Production.json b/TeachingRecordSystem/src/TeachingRecordSystem.Worker/appsettings.Production.json index 59d7780cfd..bc027606a7 100644 --- a/TeachingRecordSystem/src/TeachingRecordSystem.Worker/appsettings.Production.json +++ b/TeachingRecordSystem/src/TeachingRecordSystem.Worker/appsettings.Production.json @@ -11,7 +11,6 @@ "RunService": true }, "TrsSyncService": { - "RegisterServiceClient": true, "RunService": true } } diff --git a/TeachingRecordSystem/tests/TeachingRecordSystem.Core.Tests/Services/TrsDataSync/TrsDataSyncHelperTests.MandatoryQualification.cs b/TeachingRecordSystem/tests/TeachingRecordSystem.Core.Tests/Services/TrsDataSync/TrsDataSyncHelperTests.MandatoryQualification.cs index 5b7b9616d1..ad46140205 100644 --- a/TeachingRecordSystem/tests/TeachingRecordSystem.Core.Tests/Services/TrsDataSync/TrsDataSyncHelperTests.MandatoryQualification.cs +++ b/TeachingRecordSystem/tests/TeachingRecordSystem.Core.Tests/Services/TrsDataSync/TrsDataSyncHelperTests.MandatoryQualification.cs @@ -1,4 +1,5 @@ using FakeXrmEasy.Extensions; +using Microsoft.Crm.Sdk.Messages; using Microsoft.EntityFrameworkCore; using TeachingRecordSystem.Core.Dqt; using TeachingRecordSystem.Core.Dqt.Models; @@ -22,7 +23,7 @@ public async Task SyncMandatoryQualification_NewRecord_WritesNewRowToDb(bool per var entity = await CreateMandatoryQualificationEntity(qualificationId, person.ContactId); // Act - await Helper.SyncMandatoryQualification(entity, ignoreInvalid: false); + await Helper.SyncMandatoryQualification(entity, new AuditDetailCollection(), ignoreInvalid: false); // Assert await AssertDatabaseMandatoryQualificationMatchesEntity(entity); @@ -36,14 +37,14 @@ public async Task SyncMandatoryQualification_ExistingRecord_UpdatesExistingRowIn var qualificationId = Guid.NewGuid(); var existingEntity = await CreateMandatoryQualificationEntity(qualificationId, person.ContactId); - await Helper.SyncMandatoryQualification(existingEntity, ignoreInvalid: false); + await Helper.SyncMandatoryQualification(existingEntity, new AuditDetailCollection(), ignoreInvalid: false); var expectedFirstSync = Clock.UtcNow; Clock.Advance(); var updatedEntity = await CreateMandatoryQualificationEntity(qualificationId, person.ContactId, existingEntity); // Act - await Helper.SyncMandatoryQualification(updatedEntity, ignoreInvalid: false); + await Helper.SyncMandatoryQualification(updatedEntity, new AuditDetailCollection(), ignoreInvalid: false); // Assert await AssertDatabaseMandatoryQualificationMatchesEntity(updatedEntity, expectedFirstSync); @@ -57,7 +58,7 @@ public async Task DeleteRecords_WithMq_RemovesRowFromDb() var qualificationId = Guid.NewGuid(); var existingEntity = await CreateMandatoryQualificationEntity(qualificationId, person.ContactId); - await Helper.SyncMandatoryQualification(existingEntity, ignoreInvalid: false); + await Helper.SyncMandatoryQualification(existingEntity, new AuditDetailCollection(), ignoreInvalid: false); // Act await Helper.DeleteRecords(TrsDataSyncHelper.ModelTypes.MandatoryQualification, new[] { qualificationId }); @@ -116,7 +117,7 @@ public async Task SyncMandatoryQualification_WithDeletedEvent_SetsDeletedOnPrope entity.StateCode = dfeta_qualificationState.Inactive; // Act - await Helper.SyncMandatoryQualification(entity, ignoreInvalid: false); + await Helper.SyncMandatoryQualification(entity, new AuditDetailCollection(), ignoreInvalid: false); // Assert await DbFixture.WithDbContext(async dbContext => @@ -141,12 +142,12 @@ public async Task SyncMandatoryQualification_AlreadyHaveNewerVersion_DoesNotUpda Clock.Advance(); var updatedEntity = await CreateMandatoryQualificationEntity(qualificationId, person.ContactId, initialEntity); - await Helper.SyncMandatoryQualification(updatedEntity, ignoreInvalid: false); + await Helper.SyncMandatoryQualification(updatedEntity, new AuditDetailCollection(), ignoreInvalid: false); var expectedFirstSync = Clock.UtcNow; var expectedLastSync = Clock.UtcNow; // Act - await Helper.SyncMandatoryQualification(initialEntity, ignoreInvalid: false); + await Helper.SyncMandatoryQualification(initialEntity, new AuditDetailCollection(), ignoreInvalid: false); // Assert await AssertDatabaseMandatoryQualificationMatchesEntity(updatedEntity, expectedFirstSync, expectedLastSync); diff --git a/TeachingRecordSystem/tests/TeachingRecordSystem.Core.Tests/Services/TrsDataSync/zzAuditTests.cs b/TeachingRecordSystem/tests/TeachingRecordSystem.Core.Tests/Services/TrsDataSync/zzAuditTests.cs new file mode 100644 index 0000000000..d1ecd028d8 --- /dev/null +++ b/TeachingRecordSystem/tests/TeachingRecordSystem.Core.Tests/Services/TrsDataSync/zzAuditTests.cs @@ -0,0 +1,83 @@ +using Microsoft.Xrm.Sdk; +using TeachingRecordSystem.Core.Dqt; +using TeachingRecordSystem.Core.Dqt.Models; + +namespace TeachingRecordSystem.Core.Tests.Services.TrsDataSync; + +public class zzAuditTests +{ + [Fact] + public void Bst() + { + DateTime? dt = new DateTime(2023, 2, 23, 13, 11, 0, DateTimeKind.Utc); + var r = dt.ToDateOnlyWithDqtBstFix(isLocalTime: true); + } + + [Fact] + public void MandatoryQualificationAuditDetailsWithCreateEvent_AreMappedToExpectedEvents() + { + // Arrange + var qualificationId = Guid.NewGuid(); + var createdOn = new DateTime(2023, 1, 1); + var personId = Guid.NewGuid(); + var initialEstablishment1Id = Guid.NewGuid(); + var currentEstablishmentId = Guid.NewGuid(); + var initialSpecialismId = Guid.NewGuid(); + var currentSpecialismId = Guid.NewGuid(); + var initialStartDate = new DateTime(2020, 4, 1); + var currentStartDate = new DateTime(2020, 6, 1); + var currentEndDate = new DateTime(2020, 11, 30); + + var current = new dfeta_qualification() + { + dfeta_PersonId = personId.ToEntityReference(Contact.EntityLogicalName), + dfeta_Type = dfeta_qualification_dfeta_Type.MandatoryQualification, + dfeta_MQ_MQEstablishmentId = currentEstablishmentId.ToEntityReference(dfeta_mqestablishment.EntityLogicalName), + dfeta_MQ_SpecialismId = currentSpecialismId.ToEntityReference(dfeta_specialism.EntityLogicalName), + dfeta_MQStartDate = currentStartDate, + dfeta_MQ_Date = currentEndDate + }; + + var auditDetails = new[] + { + CreateAudit( + Audit_Action.Create, + new() + { + { dfeta_qualification.PrimaryIdAttribute, qualificationId }, + { dfeta_qualification.Fields.StateCode, dfeta_qualificationState.Active }, + { dfeta_qualification.Fields.dfeta_PersonId, personId.ToEntityReference(Contact.EntityLogicalName) }, + { dfeta_qualification.Fields.dfeta_Type, dfeta_qualification_dfeta_Type.MandatoryQualification }, + { dfeta_qualification.Fields.dfeta_MQ_MQEstablishmentId, initialEstablishment1Id.ToEntityReference(dfeta_mqestablishment.EntityLogicalName) }, + { dfeta_qualification.Fields.dfeta_MQ_SpecialismId, initialSpecialismId.ToEntityReference(dfeta_specialism.EntityLogicalName) }, + { dfeta_qualification.Fields.dfeta_MQStartDate, initialStartDate } + }, + createdOn), + //... + }; + + // Act + // TODO + + // Assert + // TODO + + Audit CreateAudit(Audit_Action action, AttributeCollection attributes, DateTime createdOn) + { + var auditId = Guid.NewGuid(); + + return new Audit() + { + Action = action, + Attributes = attributes, + AuditId = auditId, + CreatedOn = createdOn, + //EntityState = EntityState. ? + Id = auditId, + ObjectId = qualificationId.ToEntityReference(dfeta_qualification.EntityLogicalName), + //ObjectTypeCode = dfeta_qualification.type + //Operation = Audit_Operation. // TODO + }; + } + } +} diff --git a/crm_attributes.json b/crm_attributes.json index ce6d40e364..077f7175ab 100644 --- a/crm_attributes.json +++ b/crm_attributes.json @@ -19,6 +19,16 @@ "subject", "statecode" ], + "audit": [ + "action", + "auditid", + "callinguserid", + "createdon", + "objectid", + "objecttypecode", + "operation", + "userid" + ], "businessunit": [ "businessunitid" ], @@ -217,6 +227,7 @@ "dfeta_mqstartdate", "dfeta_trsdeletedevent", "CreatedOn", + "CreatedBy", "ModifiedOn" ], "dfeta_specialism": [ diff --git a/tools/coretools/CrmSvcUtil.exe.config b/tools/coretools/CrmSvcUtil.exe.config index 3233de1666..b47c182c38 100644 --- a/tools/coretools/CrmSvcUtil.exe.config +++ b/tools/coretools/CrmSvcUtil.exe.config @@ -19,8 +19,8 @@ - - + +