diff --git a/GetIntoTeachingApi/Jobs/UpsertCandidateJob.cs b/GetIntoTeachingApi/Jobs/UpsertCandidateJob.cs index 526b71105..00ac12ca4 100644 --- a/GetIntoTeachingApi/Jobs/UpsertCandidateJob.cs +++ b/GetIntoTeachingApi/Jobs/UpsertCandidateJob.cs @@ -15,7 +15,7 @@ public class UpsertCandidateJob : BaseJob private readonly ICandidateUpserter _upserter; private readonly INotifyService _notifyService; private readonly IPerformContextAdapter _contextAdapter; - private readonly IMetricService _metrics; + private readonly IMetricService _metrics; private readonly IAppSettings _appSettings; private readonly ILogger _logger; @@ -38,23 +38,23 @@ public UpsertCandidateJob( _appSettings = appSettings; } - public void Run(string json, PerformContext context) - { - var candidate = json.DeserializeChangeTracked(); - - if (Deduplicate(Signature(candidate), context, _contextAdapter)) - { - _logger.LogInformation("UpsertCandidateJob - Deduplicating"); - return; - } - - if (_appSettings.IsCrmIntegrationPaused) - { - throw new InvalidOperationException("UpsertCandidateJob - Aborting (CRM integration paused)."); - } - - _logger.LogInformation("UpsertCandidateJob - Started ({Attempt})", AttemptInfo(context, _contextAdapter)); - _logger.LogInformation("UpsertCandidateJob - Payload {Payload}", Redactor.RedactJson(json)); + public void Run(string json, PerformContext context) + { + var candidate = json.DeserializeChangeTracked(); + + if (Deduplicate(Signature(candidate), context, _contextAdapter)) + { + _logger.LogInformation("UpsertCandidateJob - Deduplicating"); + return; + } + + if (_appSettings.IsCrmIntegrationPaused) + { + throw new InvalidOperationException("UpsertCandidateJob - Aborting (CRM integration paused)."); + } + + _logger.LogInformation("UpsertCandidateJob - Started ({Attempt})", AttemptInfo(context, _contextAdapter)); + _logger.LogInformation("UpsertCandidateJob - Payload {Payload}", Redactor.RedactJson(json)); if (IsLastAttempt(context, _contextAdapter)) { @@ -68,19 +68,19 @@ public void Run(string json, PerformContext context) _logger.LogInformation("UpsertCandidateJob - Deleted"); } else - { + { _upserter.Upsert(candidate); _logger.LogInformation("UpsertCandidateJob - Succeeded - {Id}", candidate.Id); } var duration = (DateTime.UtcNow - _contextAdapter.GetJobCreatedAt(context)).TotalSeconds; - _metrics.HangfireJobQueueDuration.WithLabels("UpsertCandidateJob").Observe(duration); + _metrics.HangfireJobQueueDuration.WithLabels("UpsertCandidateJob").Observe(duration); } - private static string Signature(Candidate candidate) - { - return $"{candidate.Id}-{candidate.Email}-{string.Join("", candidate.ChangedPropertyNames)}"; + private static string Signature(Candidate candidate) + { + return $"{candidate.Id}-{candidate.Email}-{string.Join("", candidate.ChangedPropertyNames)}"; } } } diff --git a/GetIntoTeachingApi/Models/SchoolsExperience/SchoolsExperienceSignUp.cs b/GetIntoTeachingApi/Models/SchoolsExperience/SchoolsExperienceSignUp.cs index 7ba0b1587..a5a0cee74 100644 --- a/GetIntoTeachingApi/Models/SchoolsExperience/SchoolsExperienceSignUp.cs +++ b/GetIntoTeachingApi/Models/SchoolsExperience/SchoolsExperienceSignUp.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Text.Json.Serialization; using GetIntoTeachingApi.Models.Crm; using GetIntoTeachingApi.Services; @@ -33,6 +34,11 @@ public class SchoolsExperienceSignUp public string Telephone { get; set; } public bool? HasDbsCertificate { get; set; } public DateTime? DbsCertificateIssuedAt { get; set; } + public Guid? QualificationId { get; set; } + public int? DegreeStatusId { get; set; } + public int? DegreeTypeId { get; set; } + public string DegreeSubject { get; set; } + public int? UkDegreeGradeId { get; set; } [JsonIgnore] public Candidate Candidate => CreateCandidate(); @@ -75,6 +81,17 @@ private void PopulateWithCandidate(Candidate candidate) HasDbsCertificate = candidate.HasDbsCertificate; DbsCertificateIssuedAt = candidate.DbsCertificateIssuedAt; + + var latestQualification = candidate.Qualifications.OrderByDescending(q => q.CreatedAt).FirstOrDefault(); + + if (latestQualification != null) + { + QualificationId = latestQualification.Id; + DegreeSubject = latestQualification.DegreeSubject; + UkDegreeGradeId = latestQualification.UkDegreeGradeId; + DegreeStatusId = latestQualification.DegreeStatusId; + DegreeTypeId = latestQualification.TypeId; + } } private Candidate CreateCandidate() @@ -109,6 +126,7 @@ private Candidate CreateCandidate() ConfigureChannel(candidate); AcceptPrivacyPolicy(candidate); + AddQualification(candidate); return candidate; } @@ -132,5 +150,25 @@ private void AcceptPrivacyPolicy(Candidate candidate) }; } } + + private void AddQualification(Candidate candidate) + { + if (ContainsQualification() && !candidate.Qualifications.Any()) + { + candidate.Qualifications.Add(new CandidateQualification() + { + Id = QualificationId, + UkDegreeGradeId = UkDegreeGradeId, + DegreeStatusId = DegreeStatusId, + DegreeSubject = DegreeSubject, + TypeId = DegreeTypeId ?? (int)CandidateQualification.DegreeType.Degree + }); + } + } + + private bool ContainsQualification() + { + return UkDegreeGradeId != null || DegreeStatusId != null || DegreeSubject != null || DegreeTypeId != null; + } } } diff --git a/GetIntoTeachingApi/Services/CandidateUpserter.cs b/GetIntoTeachingApi/Services/CandidateUpserter.cs index 25adff90e..8aacebfec 100644 --- a/GetIntoTeachingApi/Services/CandidateUpserter.cs +++ b/GetIntoTeachingApi/Services/CandidateUpserter.cs @@ -1,7 +1,7 @@ -using System; -using System.Collections.Generic; +using System; +using System.Collections.Generic; using GetIntoTeachingApi.Jobs; -using GetIntoTeachingApi.Models.Crm; +using GetIntoTeachingApi.Models.Crm; using GetIntoTeachingApi.Utils; using Hangfire; using static GetIntoTeachingApi.Models.Crm.Candidate; @@ -43,8 +43,8 @@ public void Upsert(Candidate candidate) SaveSchoolExperiences(schoolExperiences, candidate); IncrementCallbackBookingQuotaNumberOfBookings(phoneCall); - } - + } + private static IEnumerable ClearTeachingEventRegistrations(Candidate candidate) { // Due to reasons unknown the event registrations relationship can't be deep-inserted @@ -74,15 +74,20 @@ private static IEnumerable ClearQualifications(Candidate var qualifications = new List(candidate.Qualifications); candidate.Qualifications.Clear(); return qualifications; - } - - private static IEnumerable ClearSchoolExperiences(Candidate candidate) - { + } + + private static void AddQualifications(IEnumerable candidateQualifications, Candidate candidate) + { + candidate.Qualifications.AddRange(candidateQualifications); + } + + private static IEnumerable ClearSchoolExperiences(Candidate candidate) + { var schoolExperiences = new List(candidate.SchoolExperiences); candidate.SchoolExperiences.Clear(); - return schoolExperiences; - } - + return schoolExperiences; + } + private static PhoneCall ClearPhoneCall(Candidate candidate) { if (candidate.PhoneCall == null) @@ -165,9 +170,14 @@ private void SaveQualifications(IEnumerable qualificatio { foreach (var qualification in qualifications) { - qualification.CandidateId = (Guid)candidate.Id; - string json = qualification.SerializeChangeTracked(); - _jobClient.Enqueue>((x) => x.Run(json, null)); + if (!_crm.CandidateHasDegreeQualification((Guid)candidate.Id, CandidateQualification.DegreeType.Degree, + qualification.DegreeSubject)) + { + qualification.CandidateId = (Guid)candidate.Id; + string json = qualification.SerializeChangeTracked(); + + _jobClient.Enqueue>((x) => x.Run(json, null)); + } } } @@ -189,8 +199,8 @@ private void SavePastTeachingPositions(IEnumerable>((x) => x.Run(json, null)); } - } - + } + private void SaveSchoolExperiences(IEnumerable schoolExperiences, Candidate candidate) { foreach (var schoolExperience in schoolExperiences) diff --git a/GetIntoTeachingApi/Services/CrmService.cs b/GetIntoTeachingApi/Services/CrmService.cs index ebcc74644..114bffa36 100644 --- a/GetIntoTeachingApi/Services/CrmService.cs +++ b/GetIntoTeachingApi/Services/CrmService.cs @@ -1,16 +1,18 @@ using System; using System.Collections.Generic; -using System.Globalization; +using System.Globalization; using System.Linq; -using FluentValidation; +using FluentValidation; +using Flurl.Util; using GetIntoTeachingApi.Adapters; using GetIntoTeachingApi.Models; using GetIntoTeachingApi.Models.Crm; -using GetIntoTeachingApi.Utils; -using Microsoft.Extensions.Logging; +using GetIntoTeachingApi.Utils; +using Microsoft.Extensions.Logging; using Microsoft.Xrm.Sdk; using Microsoft.Xrm.Sdk.Client; using Microsoft.Xrm.Sdk.Query; +using NuGet.Protocol; namespace GetIntoTeachingApi.Services { @@ -21,10 +23,10 @@ public class CrmService : ICrmService private const int MaximumCallbackBookingQuotaDaysInAdvance = 14; private readonly IOrganizationServiceAdapter _service; private readonly IServiceProvider _serviceProvider; - private readonly IDateTimeProvider _dateTime; + private readonly IDateTimeProvider _dateTime; private readonly IAppSettings _appSettings; - private readonly ILogger _logger; - private readonly IEnv _env; + private readonly ILogger _logger; + private readonly IEnv _env; private readonly TimeSpan _statusCheckInterval = TimeSpan.FromMinutes(1); private DateTime _previousStatusCheckAt = DateTime.UtcNow; private string _previousStatus; @@ -47,35 +49,35 @@ public CrmService( public string CheckStatus() { - if (_appSettings.IsCrmIntegrationPaused) - { + if (_appSettings.IsCrmIntegrationPaused) + { return HealthCheckResponse.StatusIntegrationPaused; } - if (_previousStatus != null && _dateTime.UtcNow.Subtract(_previousStatusCheckAt) < _statusCheckInterval) - { - return _previousStatus; + if (_previousStatus != null && _dateTime.UtcNow.Subtract(_previousStatusCheckAt) < _statusCheckInterval) + { + return _previousStatus; } - - _previousStatusCheckAt = _dateTime.UtcNow; - - return _previousStatus = _service.CheckStatus(); + + _previousStatusCheckAt = _dateTime.UtcNow; + + return _previousStatus = _service.CheckStatus(); } public IEnumerable GetCountries() { return _service.CreateQuery("dfe_country", Context()).AsEnumerable().Select((entity) => new Country(entity)); - } - + } + public IEnumerable GetTeachingSubjects() { return _service.CreateQuery("dfe_teachingsubjectlist", Context()).AsEnumerable().Select((entity) => new TeachingSubject(entity)); } public IEnumerable GetPickListItems(string entityName, string attributeName) - { - return _service.GetPickListItemsForAttribute(entityName, attributeName) + { + return _service.GetPickListItemsForAttribute(entityName, attributeName) .Select((pickListItem) => new PickListItem(pickListItem, entityName, attributeName)); } @@ -96,26 +98,26 @@ public CallbackBookingQuota GetCallbackBookingQuota(DateTime scheduledAt) .Where((entity) => entity.GetAttributeValue("dfe_starttime") == scheduledAt) .Select((entity) => new CallbackBookingQuota(entity, this, _serviceProvider)) .FirstOrDefault(); - } - - public IEnumerable GetApplyModels(IEnumerable applyIds) where T : BaseModel, IHasApplyId - { - if (!applyIds.Any()) - { - return Array.Empty(); - } - - var query = new QueryExpression(BaseModel.LogicalName(typeof(T))); - query.ColumnSet.AddColumns(BaseModel.EntityFieldAttributeNames(typeof(T))); - - var property = typeof(T).GetProperty("ApplyId"); - var attribute = BaseModel.EntityFieldAttribute(property); - query.Criteria.AddCondition(new ConditionExpression(attribute.Name, ConditionOperator.In, applyIds.ToArray())); - - var entities = _service.RetrieveMultiple(query); - - - return entities.Select(e => (T)Activator.CreateInstance(typeof(T), e, this, _serviceProvider)); + } + + public IEnumerable GetApplyModels(IEnumerable applyIds) where T : BaseModel, IHasApplyId + { + if (!applyIds.Any()) + { + return Array.Empty(); + } + + var query = new QueryExpression(BaseModel.LogicalName(typeof(T))); + query.ColumnSet.AddColumns(BaseModel.EntityFieldAttributeNames(typeof(T))); + + var property = typeof(T).GetProperty("ApplyId"); + var attribute = BaseModel.EntityFieldAttribute(property); + query.Criteria.AddCondition(new ConditionExpression(attribute.Name, ConditionOperator.In, applyIds.ToArray())); + + var entities = _service.RetrieveMultiple(query); + + + return entities.Select(e => (T)Activator.CreateInstance(typeof(T), e, this, _serviceProvider)); } public IEnumerable GetPrivacyPolicies() @@ -130,19 +132,19 @@ public IEnumerable GetPrivacyPolicies() } public Candidate MatchCandidate(ExistingCandidateRequest request) - { - var query = MatchBackQuery(request.Email); - query.TopCount = MaximumNumberOfCandidatesToMatch; + { + var query = MatchBackQuery(request.Email); + query.TopCount = MaximumNumberOfCandidatesToMatch; var entities = _service.RetrieveMultiple(query); - var entity = entities.FirstOrDefault(); - - var status = entity == null ? "Miss" : "Hit"; + var entity = entities.FirstOrDefault(); + + var status = entity == null ? "Miss" : "Hit"; _logger.LogInformation("MatchCandidate - EmailMatch - {Status}", status); - if (entity == null) - { - return null; + if (entity == null) + { + return null; } var context = Context(); @@ -151,17 +153,17 @@ public Candidate MatchCandidate(ExistingCandidateRequest request) LoadCandidateRelationships(entity, context); return new Candidate(entity, this, _serviceProvider); - } - + } + public Candidate MatchCandidate(string email, string applyId = null) - { - var query = MatchBackQuery(email, applyId); - query.TopCount = 1; + { + var query = MatchBackQuery(email, applyId); + query.TopCount = 1; var entities = _service.RetrieveMultiple(query); - var entity = entities.FirstOrDefault(); - - var status = entity == null ? "Miss" : "Hit"; + var entity = entities.FirstOrDefault(); + + var status = entity == null ? "Miss" : "Hit"; _logger.LogInformation("MatchCandidate - EmailMatch (Apply) - {Status}", status); if (entity == null) @@ -175,59 +177,59 @@ public Candidate MatchCandidate(string email, string applyId = null) public IEnumerable MatchCandidates(string magicLinkToken) { // Avoids a potentially very expensive query. - if (string.IsNullOrEmpty(magicLinkToken)) - { - return Array.Empty(); - } - + if (string.IsNullOrEmpty(magicLinkToken)) + { + return Array.Empty(); + } + var query = new QueryExpression("contact"); query.ColumnSet.AddColumns(BaseModel.EntityFieldAttributeNames(typeof(Candidate))); - query.Criteria.AddCondition(new ConditionExpression("dfe_websitemltoken", ConditionOperator.Equal, magicLinkToken)); + query.Criteria.AddCondition(new ConditionExpression("dfe_websitemltoken", ConditionOperator.Equal, magicLinkToken)); var entities = _service.RetrieveMultiple(query); var context = Context(); - foreach (var entity in entities) - { - context.Attach(entity); - LoadCandidateRelationships(entity, context); + foreach (var entity in entities) + { + context.Attach(entity); + LoadCandidateRelationships(entity, context); } return entities.Select(e => new Candidate(e, this, _serviceProvider)); } public Candidate GetCandidate(Guid id) - { - return GetCandidates(new Guid[] { id }).FirstOrDefault(); - } - + { + return GetCandidates(new Guid[] { id }).FirstOrDefault(); + } + public IEnumerable GetCandidates(IEnumerable ids) - { - if (!ids.Any()) - { - return Array.Empty(); - } - + { + if (!ids.Any()) + { + return Array.Empty(); + } + var query = new QueryExpression("contact"); query.ColumnSet.AddColumns(BaseModel.EntityFieldAttributeNames(typeof(Candidate))); - query.Criteria.AddCondition(new ConditionExpression("contactid", ConditionOperator.In, ids.ToArray())); + query.Criteria.AddCondition(new ConditionExpression("contactid", ConditionOperator.In, ids.ToArray())); var entities = _service.RetrieveMultiple(query); - return entities.Select((entity) => new Candidate(entity, this, _serviceProvider)); - } + return entities.Select((entity) => new Candidate(entity, this, _serviceProvider)); + } - public IEnumerable GetCandidatesPendingMagicLinkTokenGeneration(int limit = 10) - { + public IEnumerable GetCandidatesPendingMagicLinkTokenGeneration(int limit = 10) + { var query = new QueryExpression("contact"); query.ColumnSet.AddColumns(BaseModel.EntityFieldAttributeNames(typeof(Candidate))); - query.Criteria.AddCondition(new ConditionExpression("dfe_websitemltokenstatus", ConditionOperator.Equal, (int)Candidate.MagicLinkTokenStatus.Pending)); - query.TopCount = limit; - + query.Criteria.AddCondition(new ConditionExpression("dfe_websitemltokenstatus", ConditionOperator.Equal, (int)Candidate.MagicLinkTokenStatus.Pending)); + query.TopCount = limit; + var entities = _service.RetrieveMultiple(query); - - return entities.Select(e => new Candidate(e, this, _serviceProvider)); + + return entities.Select(e => new Candidate(e, this, _serviceProvider)); } public bool CandidateAlreadyHasLocalEventSubscriptionType(Guid candidateId) @@ -249,6 +251,14 @@ public bool CandidateYetToRegisterForTeachingEvent(Guid candidateId, Guid teachi entity.GetAttributeValue("msevtmgt_eventid").Id == teachingEventId) == null; } + public bool CandidateHasDegreeQualification(Guid candidateId, CandidateQualification.DegreeType degreeType, string degreeSubject) + { + // this check helps prevent duplicate qualification records from being created + return !(_service.CreateQuery("dfe_candidatequalification", Context()).FirstOrDefault(entity => + entity.GetAttributeValue("dfe_contactid").Id == candidateId && + entity.GetAttributeValue("dfe_type") == (int)CandidateQualification.DegreeType.Degree) == null); + } + public void AddLink(Entity source, Relationship relationship, Entity target, OrganizationServiceContext context) { _service.AddLink(source, relationship, target, context); @@ -310,9 +320,9 @@ public void Save(BaseModel model) public IEnumerable GetTeachingEvents(DateTime? startAfter = null) { - if (startAfter == null) - { - startAfter = _dateTime.UtcNow; + if (startAfter == null) + { + startAfter = _dateTime.UtcNow; } var query = new QueryExpression("msevtmgt_event"); @@ -358,47 +368,47 @@ public IEnumerable GetTeachingEventBuildings() { return _service.CreateQuery("msevtmgt_building", Context()) .Select((entity) => new TeachingEventBuilding(entity, this, _serviceProvider)).ToList(); - } - - private static QueryExpression MatchBackQuery(string email, string applyId = null) - { - // The ToList() is important or Dynamics throws an error. - var emails = EmailReconciler.EquivalentEmails(email).ToList(); + } + + private static QueryExpression MatchBackQuery(string email, string applyId = null) + { + // The ToList() is important or Dynamics throws an error. + var emails = EmailReconciler.EquivalentEmails(email).ToList(); var query = new QueryExpression("contact"); query.ColumnSet.AddColumns(BaseModel.EntityFieldAttributeNames(typeof(Candidate))); - - var mainFilter = new FilterExpression(LogicalOperator.And); - - var filter = new FilterExpression(LogicalOperator.Or); - filter.AddCondition(new ConditionExpression("emailaddress1", ConditionOperator.In, emails)); - filter.AddCondition(new ConditionExpression("emailaddress2", ConditionOperator.In, emails)); - - if (applyId != null) - { - // We match records on email or apply id. - filter.AddCondition(new ConditionExpression("dfe_applyid", ConditionOperator.Equal, applyId)); - - // Ensure apply id takes presedence over email and duplicate score/modified on. - query.Orders.Add(new OrderExpression("dfe_applyid", OrderType.Descending)); - } - - mainFilter.AddFilter(filter); - - mainFilter.AddCondition(new ConditionExpression("statecode", ConditionOperator.Equal, (int)Candidate.Status.Active)); - - query.Criteria = mainFilter; - - query.Orders.Add(new OrderExpression("dfe_duplicatescorecalculated", OrderType.Descending)); - query.Orders.Add(new OrderExpression("modifiedon", OrderType.Descending)); - - return query; + + var mainFilter = new FilterExpression(LogicalOperator.And); + + var filter = new FilterExpression(LogicalOperator.Or); + filter.AddCondition(new ConditionExpression("emailaddress1", ConditionOperator.In, emails)); + filter.AddCondition(new ConditionExpression("emailaddress2", ConditionOperator.In, emails)); + + if (applyId != null) + { + // We match records on email or apply id. + filter.AddCondition(new ConditionExpression("dfe_applyid", ConditionOperator.Equal, applyId)); + + // Ensure apply id takes presedence over email and duplicate score/modified on. + query.Orders.Add(new OrderExpression("dfe_applyid", OrderType.Descending)); + } + + mainFilter.AddFilter(filter); + + mainFilter.AddCondition(new ConditionExpression("statecode", ConditionOperator.Equal, (int)Candidate.Status.Active)); + + query.Criteria = mainFilter; + + query.Orders.Add(new OrderExpression("dfe_duplicatescorecalculated", OrderType.Descending)); + query.Orders.Add(new OrderExpression("modifiedon", OrderType.Descending)); + + return query; } - private void LoadCandidateRelationships(Entity entity, OrganizationServiceContext context) - { + private void LoadCandidateRelationships(Entity entity, OrganizationServiceContext context) + { _service.LoadProperty(entity, new Relationship("dfe_contact_dfe_candidatequalification_ContactId"), context); _service.LoadProperty(entity, new Relationship("dfe_contact_dfe_candidatepastteachingposition_ContactId"), context); - _service.LoadProperty(entity, new Relationship("msevtmgt_contact_msevtmgt_eventregistration_Contact"), context); + _service.LoadProperty(entity, new Relationship("msevtmgt_contact_msevtmgt_eventregistration_Contact"), context); } private OrganizationServiceContext Context() diff --git a/GetIntoTeachingApi/Services/ICrmService.cs b/GetIntoTeachingApi/Services/ICrmService.cs index 4496818df..52431607f 100644 --- a/GetIntoTeachingApi/Services/ICrmService.cs +++ b/GetIntoTeachingApi/Services/ICrmService.cs @@ -28,6 +28,9 @@ public interface ICrmService bool CandidateAlreadyHasLocalEventSubscriptionType(Guid candidateId); bool CandidateYetToAcceptPrivacyPolicy(Guid candidateId, Guid privacyPolicyId); bool CandidateYetToRegisterForTeachingEvent(Guid candidateId, Guid teachingEventId); + + bool CandidateHasDegreeQualification(Guid candidateId, CandidateQualification.DegreeType degreeType, + string degreeSubject); void Save(BaseModel model); void AddLink(Entity source, Relationship relationship, Entity target, OrganizationServiceContext context); void DeleteLink(Entity source, Relationship relationship, Entity target, OrganizationServiceContext context); diff --git a/GetIntoTeachingApiTests/Helpers/ContractCrmService.cs b/GetIntoTeachingApiTests/Helpers/ContractCrmService.cs index 784ff9b24..bc568831f 100644 --- a/GetIntoTeachingApiTests/Helpers/ContractCrmService.cs +++ b/GetIntoTeachingApiTests/Helpers/ContractCrmService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -85,6 +85,11 @@ public bool CandidateYetToRegisterForTeachingEvent(Guid candidateId, Guid teachi return _crmService.CandidateYetToRegisterForTeachingEvent(candidateId, teachingEventId); } + public bool CandidateHasDegreeQualification(Guid candidateId, CandidateQualification.DegreeType degreeType, string degreeSubject) + { + return _crmService.CandidateHasDegreeQualification(candidateId, degreeType, degreeSubject); + } + public string CheckStatus() { return _crmService.CheckStatus(); @@ -95,7 +100,7 @@ public void DeleteLink(Entity source, Relationship relationship, Entity target, _crmService.DeleteLink(source, relationship, target, context); } - public IEnumerable GetApplyModels(IEnumerable applyIds) where T : BaseModel, IHasApplyId + public IEnumerable GetApplyModels(IEnumerable applyIds) where T : BaseModel, IHasApplyId { switch (typeof(T).ToString()) {