From 8dd4a2bb587cf5d84aab0fe265894166295cefed Mon Sep 17 00:00:00 2001 From: PabloDinella Date: Fri, 23 Aug 2024 12:32:31 -0300 Subject: [PATCH 1/2] Change calculation for one time recurrence to consider the delay as 'full days' --- .../PolicyEvaluationEngine.cs | 6 +- .../PolicyEvaluation/ReferralCalculations.cs | 39 ++-- .../Resources/Policies/IPoliciesResource.cs | 2 +- src/caretogether-pwa/src/GeneratedClient.ts | 172 +++++++----------- swagger.json | 124 +++++-------- ...teMissingMonitoringRequirementInstances.cs | 104 +++++++---- .../CalculateMissingMonitoringRequirements.cs | 12 +- .../ReferralCalculationTests/Helpers.cs | 10 + 8 files changed, 225 insertions(+), 244 deletions(-) diff --git a/src/CareTogether.Core/Engines/PolicyEvaluation/PolicyEvaluationEngine.cs b/src/CareTogether.Core/Engines/PolicyEvaluation/PolicyEvaluationEngine.cs index d14ea124..70626f90 100644 --- a/src/CareTogether.Core/Engines/PolicyEvaluation/PolicyEvaluationEngine.cs +++ b/src/CareTogether.Core/Engines/PolicyEvaluation/PolicyEvaluationEngine.cs @@ -42,9 +42,13 @@ public async Task CalculateReferralStatusAsync( ReferralEntry referralEntry) { var policy = await policiesResource.GetCurrentPolicy(organizationId, locationId); + var config = await policiesResource.GetConfigurationAsync(organizationId); + + var location = config.Locations.Find(item => item.Id == locationId); + TimeZoneInfo locationTimeZone = location?.timeZone ?? TimeZoneInfo.FindSystemTimeZoneById("America/New_York"); return ReferralCalculations.CalculateReferralStatus( - policy.ReferralPolicy, referralEntry, DateTime.UtcNow); + policy.ReferralPolicy, referralEntry, DateTime.UtcNow, locationTimeZone); } } } diff --git a/src/CareTogether.Core/Engines/PolicyEvaluation/ReferralCalculations.cs b/src/CareTogether.Core/Engines/PolicyEvaluation/ReferralCalculations.cs index b5784da2..19fef99f 100644 --- a/src/CareTogether.Core/Engines/PolicyEvaluation/ReferralCalculations.cs +++ b/src/CareTogether.Core/Engines/PolicyEvaluation/ReferralCalculations.cs @@ -12,7 +12,7 @@ namespace CareTogether.Engines.PolicyEvaluation internal static class ReferralCalculations { public static ReferralStatus CalculateReferralStatus( - ReferralPolicy referralPolicy, ReferralEntry referralEntry, DateTime utcNow) + ReferralPolicy referralPolicy, ReferralEntry referralEntry, DateTime utcNow, TimeZoneInfo locationTimeZone) { var missingIntakeRequirements = referralPolicy.RequiredIntakeActionNames.Where(requiredAction => !SharedCalculations.RequirementMetOrExempted(requiredAction, @@ -34,7 +34,7 @@ public static ReferralStatus CalculateReferralStatus( .Single(p => p.ArrangementType == arrangement.Value.ArrangementType); return CalculateArrangementStatus(arrangement.Value, - arrangementPolicy, utcNow); + arrangementPolicy, utcNow, locationTimeZone); }); return new ReferralStatus( @@ -45,12 +45,12 @@ public static ReferralStatus CalculateReferralStatus( internal static ArrangementStatus CalculateArrangementStatus(ArrangementEntry arrangement, ArrangementPolicy arrangementPolicy, - DateTime utcNow) + DateTime utcNow, TimeZoneInfo locationTimeZone) { var missingSetupRequirements = CalculateMissingSetupRequirements( arrangementPolicy, arrangement, utcNow); var missingMonitoringRequirements = CalculateMissingMonitoringRequirements( - arrangementPolicy, arrangement, utcNow); + arrangementPolicy, arrangement, utcNow, locationTimeZone); var missingCloseoutRequirements = CalculateMissingCloseoutRequirements( arrangementPolicy, arrangement, utcNow); var missingFunctionAssignments = CalculateMissingFunctionAssignments(arrangementPolicy.ArrangementFunctions, @@ -160,7 +160,7 @@ internal static ImmutableList CalculateMissingSet } internal static ImmutableList CalculateMissingMonitoringRequirements( - ArrangementPolicy arrangementPolicy, ArrangementEntry arrangement, DateTime utcNow) + ArrangementPolicy arrangementPolicy, ArrangementEntry arrangement, DateTime utcNow, TimeZoneInfo locationTimeZone) { var arrangementLevelResults = arrangementPolicy.RequiredMonitoringActions .SelectMany(monitoringRequirement => @@ -172,7 +172,7 @@ internal static ImmutableList CalculateMissingMon .Where(x => x.RequirementName == monitoringRequirement.ActionName) .Select(x => x.CompletedAtUtc) .OrderBy(x => x).ToImmutableList(), - arrangement.ChildLocationHistory, utcNow) + arrangement.ChildLocationHistory, utcNow, locationTimeZone) : ImmutableList.Empty) .Where(missingDueDate => !arrangement.ExemptedRequirements.Any(exempted => exempted.RequirementName == monitoringRequirement.ActionName && @@ -206,7 +206,7 @@ internal static ImmutableList CalculateMissingMon .Where(x => x.RequirementName == monitoringRequirement.ActionName) .Select(x => x.CompletedAtUtc) .OrderBy(x => x).ToImmutableList(), - arrangement.ChildLocationHistory, utcNow) + arrangement.ChildLocationHistory, utcNow, locationTimeZone) : ImmutableList.Empty) .Where(missingDueDate => !fva.ExemptedRequirements.Any(exempted => exempted.RequirementName == monitoringRequirement.ActionName && @@ -242,7 +242,7 @@ internal static ImmutableList CalculateMissingMon .Where(x => x.RequirementName == monitoringRequirement.ActionName) .Select(x => x.CompletedAtUtc) .OrderBy(x => x).ToImmutableList(), - arrangement.ChildLocationHistory, utcNow) + arrangement.ChildLocationHistory, utcNow, locationTimeZone) : ImmutableList.Empty) .Where(missingDueDate => !iva.ExemptedRequirements.Any(exempted => exempted.RequirementName == monitoringRequirement.ActionName && @@ -264,16 +264,20 @@ internal static ImmutableList CalculateMissingMon .ToImmutableList(); } - internal static ImmutableList CalculateMissingMonitoringRequirementInstances( + // The delay defined in the policies should be considered as "full days" delay. So a delay of 2 days means the + // requirement can be met at any time in the following 2 days after the startedAt date. + // For that reason, we convert the datetimes to the client (location) timezone during calculation, + // to avoid a completion falling in the next day for example. + internal static ImmutableList CalculateMissingMonitoringRequirementInstances( RecurrencePolicy recurrence, Guid? filterToFamilyId, DateTime arrangementStartedAtUtc, DateTime? arrangementEndedAtUtc, ImmutableList completions, ImmutableSortedSet childLocationHistory, - DateTime utcNow) + DateTime utcNow, TimeZoneInfo locationTimeZone) { return recurrence switch { OneTimeRecurrencePolicy oneTime => CalculateMissingMonitoringRequirementInstancesForOneTimeRecurrence( - oneTime, arrangementStartedAtUtc, arrangementEndedAtUtc, completions, utcNow), + oneTime, arrangementStartedAtUtc, arrangementEndedAtUtc, completions, utcNow, locationTimeZone), DurationStagesRecurrencePolicy durationStages => CalculateMissingMonitoringRequirementInstancesForDurationRecurrence( durationStages, arrangementStartedAtUtc, arrangementEndedAtUtc, utcNow, completions), @@ -292,14 +296,19 @@ internal static ImmutableList CalculateMissingMonitoringRequirementIns internal static ImmutableList CalculateMissingMonitoringRequirementInstancesForOneTimeRecurrence( OneTimeRecurrencePolicy recurrence, DateTime arrangementStartedAtUtc, DateTime? arrangementEndedAtUtc, - ImmutableList completions, DateTime utcNow) + ImmutableList completions, DateTime utcNow, TimeZoneInfo locationTimeZone) { if (recurrence.Delay.HasValue) { - var dueDate = arrangementStartedAtUtc + recurrence.Delay.Value; - return completions.Any(completion => completion <= dueDate) + var arrangementStartedAtInClientsTimeZone = TimeZoneInfo.ConvertTimeFromUtc(arrangementStartedAtUtc, locationTimeZone); + + var dueBaseDateInClientsTimeZone = new DateTime(DateOnly.FromDateTime(arrangementStartedAtInClientsTimeZone), new TimeOnly()).AddDays(1); + + var dueDateInClientsTimeZone = (dueBaseDateInClientsTimeZone + recurrence.Delay.Value).AddSeconds(-1); + + return completions.Any(completion => TimeZoneInfo.ConvertTimeFromUtc(completion, locationTimeZone) <= dueDateInClientsTimeZone) ? [] - : [dueDate]; + : [TimeZoneInfo.ConvertTimeToUtc(dueDateInClientsTimeZone, locationTimeZone)]; } else return completions.IsEmpty diff --git a/src/CareTogether.Core/Resources/Policies/IPoliciesResource.cs b/src/CareTogether.Core/Resources/Policies/IPoliciesResource.cs index 8d1b736b..516ab22b 100644 --- a/src/CareTogether.Core/Resources/Policies/IPoliciesResource.cs +++ b/src/CareTogether.Core/Resources/Policies/IPoliciesResource.cs @@ -13,7 +13,7 @@ public sealed record OrganizationConfiguration(string OrganizationName, public sealed record LocationConfiguration(Guid Id, string Name, ImmutableList Ethnicities, ImmutableList AdultFamilyRelationships, ImmutableList? ArrangementReasons, - ImmutableList SmsSourcePhoneNumbers); + ImmutableList SmsSourcePhoneNumbers, TimeZoneInfo? timeZone = null); public sealed record SourcePhoneNumberConfiguration(string SourcePhoneNumber, string Description); diff --git a/src/caretogether-pwa/src/GeneratedClient.ts b/src/caretogether-pwa/src/GeneratedClient.ts index 1779223a..ac91544e 100644 --- a/src/caretogether-pwa/src/GeneratedClient.ts +++ b/src/caretogether-pwa/src/GeneratedClient.ts @@ -906,10 +906,6 @@ export class MetadataClient { this.baseUrl = baseUrl ?? ""; } - /** - * Generates the OData $metadata document. - * @return The IEdmModel representing $metadata. - */ getMetadata(): Promise { let url_ = this.baseUrl + "/api/odata/live/$metadata"; url_ = url_.replace(/[?&]$/, ""); @@ -944,10 +940,6 @@ export class MetadataClient { return Promise.resolve(null as any); } - /** - * Generates the OData service document. - * @return The service document for the service. - */ getServiceDocument(): Promise { let url_ = this.baseUrl + "/api/odata/live"; url_ = url_.replace(/[?&]$/, ""); @@ -1201,6 +1193,7 @@ export class LocationConfiguration implements ILocationConfiguration { adultFamilyRelationships?: string[]; arrangementReasons?: string[] | undefined; smsSourcePhoneNumbers?: SourcePhoneNumberConfiguration[]; + timeZone?: TimeZoneInfo | undefined; constructor(data?: ILocationConfiguration) { if (data) { @@ -1235,6 +1228,7 @@ export class LocationConfiguration implements ILocationConfiguration { for (let item of _data["smsSourcePhoneNumbers"]) this.smsSourcePhoneNumbers!.push(SourcePhoneNumberConfiguration.fromJS(item)); } + this.timeZone = _data["timeZone"] ? TimeZoneInfo.fromJS(_data["timeZone"]) : undefined; } } @@ -1269,6 +1263,7 @@ export class LocationConfiguration implements ILocationConfiguration { for (let item of this.smsSourcePhoneNumbers) data["smsSourcePhoneNumbers"].push(item.toJSON()); } + data["timeZone"] = this.timeZone ? this.timeZone.toJSON() : undefined; return data; } } @@ -1280,6 +1275,7 @@ export interface ILocationConfiguration { adultFamilyRelationships?: string[]; arrangementReasons?: string[] | undefined; smsSourcePhoneNumbers?: SourcePhoneNumberConfiguration[]; + timeZone?: TimeZoneInfo | undefined; } export class SourcePhoneNumberConfiguration implements ISourcePhoneNumberConfiguration { @@ -1322,6 +1318,66 @@ export interface ISourcePhoneNumberConfiguration { description?: string; } +export class TimeZoneInfo implements ITimeZoneInfo { + id?: string; + hasIanaId?: boolean; + displayName?: string; + standardName?: string; + daylightName?: string; + baseUtcOffset?: string; + supportsDaylightSavingTime?: boolean; + + constructor(data?: ITimeZoneInfo) { + if (data) { + for (var property in data) { + if (data.hasOwnProperty(property)) + (this)[property] = (data)[property]; + } + } + } + + init(_data?: any) { + if (_data) { + this.id = _data["Id"]; + this.hasIanaId = _data["HasIanaId"]; + this.displayName = _data["DisplayName"]; + this.standardName = _data["StandardName"]; + this.daylightName = _data["DaylightName"]; + this.baseUtcOffset = _data["BaseUtcOffset"]; + this.supportsDaylightSavingTime = _data["SupportsDaylightSavingTime"]; + } + } + + static fromJS(data: any): TimeZoneInfo { + data = typeof data === 'object' ? data : {}; + let result = new TimeZoneInfo(); + result.init(data); + return result; + } + + toJSON(data?: any) { + data = typeof data === 'object' ? data : {}; + data["Id"] = this.id; + data["HasIanaId"] = this.hasIanaId; + data["DisplayName"] = this.displayName; + data["StandardName"] = this.standardName; + data["DaylightName"] = this.daylightName; + data["BaseUtcOffset"] = this.baseUtcOffset; + data["SupportsDaylightSavingTime"] = this.supportsDaylightSavingTime; + return data; + } +} + +export interface ITimeZoneInfo { + id?: string; + hasIanaId?: boolean; + displayName?: string; + standardName?: string; + daylightName?: string; + baseUtcOffset?: string; + supportsDaylightSavingTime?: boolean; +} + export class RoleDefinition implements IRoleDefinition { roleName?: string; isProtected?: boolean | undefined; @@ -12409,19 +12465,12 @@ export interface IAccountLocationAccess { roles?: string[]; } -/** Semantic representation of an EDM model. */ export abstract class IEdmModel implements IIEdmModel { - /** Gets the collection of schema elements that are contained in this model. */ schemaElements?: IEdmSchemaElement[] | undefined; - /** Gets the collection of vocabulary annotations that are contained in this model. */ vocabularyAnnotations?: IEdmVocabularyAnnotation[] | undefined; - /** Gets the collection of models referred to by this model (mainly by the this.References). */ referencedModels?: IEdmModel[] | undefined; - /** Gets the collection of namespaces that schema elements use contained in this model. */ declaredNamespaces?: string[] | undefined; - /** Gets the model's annotations manager. */ directValueAnnotationsManager?: IEdmDirectValueAnnotationsManager | undefined; - /** Gets the only one entity container of the model. */ entityContainer?: IEdmEntityContainer | undefined; constructor(data?: IIEdmModel) { @@ -12493,27 +12542,17 @@ export abstract class IEdmModel implements IIEdmModel { } } -/** Semantic representation of an EDM model. */ export interface IIEdmModel { - /** Gets the collection of schema elements that are contained in this model. */ schemaElements?: IEdmSchemaElement[] | undefined; - /** Gets the collection of vocabulary annotations that are contained in this model. */ vocabularyAnnotations?: IEdmVocabularyAnnotation[] | undefined; - /** Gets the collection of models referred to by this model (mainly by the this.References). */ referencedModels?: IEdmModel[] | undefined; - /** Gets the collection of namespaces that schema elements use contained in this model. */ declaredNamespaces?: string[] | undefined; - /** Gets the model's annotations manager. */ directValueAnnotationsManager?: IEdmDirectValueAnnotationsManager | undefined; - /** Gets the only one entity container of the model. */ entityContainer?: IEdmEntityContainer | undefined; } -/** Common base interface for all named children of EDM schema. */ export abstract class IEdmSchemaElement implements IIEdmSchemaElement { - /** Gets the kind of this schema element. */ schemaElementKind?: EdmSchemaElementKind; - /** Gets the namespace this schema element belongs to. */ namespace?: string | undefined; constructor(data?: IIEdmSchemaElement) { @@ -12545,15 +12584,11 @@ export abstract class IEdmSchemaElement implements IIEdmSchemaElement { } } -/** Common base interface for all named children of EDM schema. */ export interface IIEdmSchemaElement { - /** Gets the kind of this schema element. */ schemaElementKind?: EdmSchemaElementKind; - /** Gets the namespace this schema element belongs to. */ namespace?: string | undefined; } -/** Defines EDM schema element types. */ export enum EdmSchemaElementKind { None = 0, TypeDefinition = 1, @@ -12563,15 +12598,10 @@ export enum EdmSchemaElementKind { Function = 5, } -/** Represents an EDM vocabulary annotation. */ export abstract class IEdmVocabularyAnnotation implements IIEdmVocabularyAnnotation { - /** Gets the qualifier used to discriminate between multiple bindings of the same property or type. */ qualifier?: string | undefined; - /** Gets the term bound by the annotation. */ term?: IEdmTerm | undefined; - /** Gets the element the annotation applies to. */ target?: IEdmVocabularyAnnotatable | undefined; - /** Gets the expression producing the value of the annotation. */ value?: IEdmExpression | undefined; constructor(data?: IIEdmVocabularyAnnotation) { @@ -12607,25 +12637,16 @@ export abstract class IEdmVocabularyAnnotation implements IIEdmVocabularyAnnotat } } -/** Represents an EDM vocabulary annotation. */ export interface IIEdmVocabularyAnnotation { - /** Gets the qualifier used to discriminate between multiple bindings of the same property or type. */ qualifier?: string | undefined; - /** Gets the term bound by the annotation. */ term?: IEdmTerm | undefined; - /** Gets the element the annotation applies to. */ target?: IEdmVocabularyAnnotatable | undefined; - /** Gets the expression producing the value of the annotation. */ value?: IEdmExpression | undefined; } -/** Represents an EDM term. */ export abstract class IEdmTerm implements IIEdmTerm { - /** Gets the type of this term. */ type?: IEdmTypeReference | undefined; - /** Gets the AppliesTo of this term. */ appliesTo?: string | undefined; - /** Gets the DefaultValue of this term. */ defaultValue?: string | undefined; constructor(data?: IIEdmTerm) { @@ -12659,21 +12680,14 @@ export abstract class IEdmTerm implements IIEdmTerm { } } -/** Represents an EDM term. */ export interface IIEdmTerm { - /** Gets the type of this term. */ type?: IEdmTypeReference | undefined; - /** Gets the AppliesTo of this term. */ appliesTo?: string | undefined; - /** Gets the DefaultValue of this term. */ defaultValue?: string | undefined; } -/** Represents a references to a type. */ export abstract class IEdmTypeReference implements IIEdmTypeReference { - /** Gets a value indicating whether this type is nullable. */ isNullable?: boolean; - /** Gets the definition to which this type refers. */ definition?: IEdmType | undefined; constructor(data?: IIEdmTypeReference) { @@ -12705,17 +12719,12 @@ export abstract class IEdmTypeReference implements IIEdmTypeReference { } } -/** Represents a references to a type. */ export interface IIEdmTypeReference { - /** Gets a value indicating whether this type is nullable. */ isNullable?: boolean; - /** Gets the definition to which this type refers. */ definition?: IEdmType | undefined; } -/** Represents the definition of an EDM type. */ export abstract class IEdmType implements IIEdmType { - /** Gets the kind of this type. */ typeKind?: EdmTypeKind; constructor(data?: IIEdmType) { @@ -12745,13 +12754,10 @@ export abstract class IEdmType implements IIEdmType { } } -/** Represents the definition of an EDM type. */ export interface IIEdmType { - /** Gets the kind of this type. */ typeKind?: EdmTypeKind; } -/** Defines EDM metatypes. */ export enum EdmTypeKind { None = 0, Primitive = 1, @@ -12765,7 +12771,6 @@ export enum EdmTypeKind { Path = 9, } -/** Represents an element that can be targeted by Vocabulary Annotations */ export abstract class IEdmVocabularyAnnotatable implements IIEdmVocabularyAnnotatable { constructor(data?: IIEdmVocabularyAnnotatable) { @@ -12791,13 +12796,10 @@ export abstract class IEdmVocabularyAnnotatable implements IIEdmVocabularyAnnota } } -/** Represents an element that can be targeted by Vocabulary Annotations */ export interface IIEdmVocabularyAnnotatable { } -/** Represents an EDM expression. */ export abstract class IEdmExpression implements IIEdmExpression { - /** Gets the kind of this expression. */ expressionKind?: EdmExpressionKind; constructor(data?: IIEdmExpression) { @@ -12827,13 +12829,10 @@ export abstract class IEdmExpression implements IIEdmExpression { } } -/** Represents an EDM expression. */ export interface IIEdmExpression { - /** Gets the kind of this expression. */ expressionKind?: EdmExpressionKind; } -/** Defines EDM expression kinds. */ export enum EdmExpressionKind { None = 0, BinaryConstant = 1, @@ -12863,7 +12862,6 @@ export enum EdmExpressionKind { AnnotationPath = 25, } -/** Manages getting and setting direct annotations on EDM elements. */ export abstract class IEdmDirectValueAnnotationsManager implements IIEdmDirectValueAnnotationsManager { constructor(data?: IIEdmDirectValueAnnotationsManager) { @@ -12889,13 +12887,10 @@ export abstract class IEdmDirectValueAnnotationsManager implements IIEdmDirectVa } } -/** Manages getting and setting direct annotations on EDM elements. */ export interface IIEdmDirectValueAnnotationsManager { } -/** Represents an EDM entity container. */ export abstract class IEdmEntityContainer implements IIEdmEntityContainer { - /** Gets a collection of the elements of this entity container. */ elements?: IEdmEntityContainerElement[] | undefined; constructor(data?: IIEdmEntityContainer) { @@ -12933,17 +12928,12 @@ export abstract class IEdmEntityContainer implements IIEdmEntityContainer { } } -/** Represents an EDM entity container. */ export interface IIEdmEntityContainer { - /** Gets a collection of the elements of this entity container. */ elements?: IEdmEntityContainerElement[] | undefined; } -/** Represents the common elements of all EDM entity container elements. */ export abstract class IEdmEntityContainerElement implements IIEdmEntityContainerElement { - /** Gets the kind of element of this container element. */ containerElementKind?: EdmContainerElementKind; - /** Gets the container that contains this element. */ container?: IEdmEntityContainer | undefined; constructor(data?: IIEdmEntityContainerElement) { @@ -12975,15 +12965,11 @@ export abstract class IEdmEntityContainerElement implements IIEdmEntityContainer } } -/** Represents the common elements of all EDM entity container elements. */ export interface IIEdmEntityContainerElement { - /** Gets the kind of element of this container element. */ containerElementKind?: EdmContainerElementKind; - /** Gets the container that contains this element. */ container?: IEdmEntityContainer | undefined; } -/** Defines EDM container element types. */ export enum EdmContainerElementKind { None = 0, EntitySet = 1, @@ -12992,9 +12978,7 @@ export enum EdmContainerElementKind { Singleton = 4, } -/** Base class for all annotatable types in OData library. */ export abstract class ODataAnnotatable implements IODataAnnotatable { - /** The annotation for storing @odata.type. */ typeAnnotation?: ODataTypeAnnotation | undefined; constructor(data?: IODataAnnotatable) { @@ -13024,19 +13008,13 @@ export abstract class ODataAnnotatable implements IODataAnnotatable { } } -/** Base class for all annotatable types in OData library. */ export interface IODataAnnotatable { - /** The annotation for storing @odata.type. */ typeAnnotation?: ODataTypeAnnotation | undefined; } -/** Class representing the a service document. */ export class ODataServiceDocument extends ODataAnnotatable implements IODataServiceDocument { - /** Gets or sets the set of entity sets in the service document. */ entitySets?: ODataEntitySetInfo[] | undefined; - /** Gets or sets the set of singletons in the service document. */ singletons?: ODataSingletonInfo[] | undefined; - /** Gets or sets the set of function imports in the service document. */ functionImports?: ODataFunctionImportInfo[] | undefined; constructor(data?: IODataServiceDocument) { @@ -13093,23 +13071,15 @@ export class ODataServiceDocument extends ODataAnnotatable implements IODataServ } } -/** Class representing the a service document. */ export interface IODataServiceDocument extends IODataAnnotatable { - /** Gets or sets the set of entity sets in the service document. */ entitySets?: ODataEntitySetInfo[] | undefined; - /** Gets or sets the set of singletons in the service document. */ singletons?: ODataSingletonInfo[] | undefined; - /** Gets or sets the set of function imports in the service document. */ functionImports?: ODataFunctionImportInfo[] | undefined; } -/** Abstract class representing an element (EntitySet, Singleton) in a service document. */ export abstract class ODataServiceDocumentElement extends ODataAnnotatable implements IODataServiceDocumentElement { - /** Gets or sets the URI representing the Unified Resource Locator (URL) to the element. */ url?: string | undefined; - /** Gets or sets the name of the element; this is the entity set or singleton name in JSON and the HREF in Atom. */ name?: string | undefined; - /** Gets or sets the title of the element; this is the title in JSON. */ title?: string | undefined; constructor(data?: IODataServiceDocumentElement) { @@ -13140,17 +13110,12 @@ export abstract class ODataServiceDocumentElement extends ODataAnnotatable imple } } -/** Abstract class representing an element (EntitySet, Singleton) in a service document. */ export interface IODataServiceDocumentElement extends IODataAnnotatable { - /** Gets or sets the URI representing the Unified Resource Locator (URL) to the element. */ url?: string | undefined; - /** Gets or sets the name of the element; this is the entity set or singleton name in JSON and the HREF in Atom. */ name?: string | undefined; - /** Gets or sets the title of the element; this is the title in JSON. */ title?: string | undefined; } -/** Class representing a entity set in a service document. */ export class ODataEntitySetInfo extends ODataServiceDocumentElement implements IODataEntitySetInfo { constructor(data?: IODataEntitySetInfo) { @@ -13175,13 +13140,10 @@ export class ODataEntitySetInfo extends ODataServiceDocumentElement implements I } } -/** Class representing a entity set in a service document. */ export interface IODataEntitySetInfo extends IODataServiceDocumentElement { } -/** Annotation which stores the EDM type information of a value. */ export class ODataTypeAnnotation implements IODataTypeAnnotation { - /** Gets the type name to serialize, for the annotated item. */ typeName?: string | undefined; constructor(data?: IODataTypeAnnotation) { @@ -13213,13 +13175,10 @@ export class ODataTypeAnnotation implements IODataTypeAnnotation { } } -/** Annotation which stores the EDM type information of a value. */ export interface IODataTypeAnnotation { - /** Gets the type name to serialize, for the annotated item. */ typeName?: string | undefined; } -/** Class representing a singleton in a service document. */ export class ODataSingletonInfo extends ODataServiceDocumentElement implements IODataSingletonInfo { constructor(data?: IODataSingletonInfo) { @@ -13244,11 +13203,9 @@ export class ODataSingletonInfo extends ODataServiceDocumentElement implements I } } -/** Class representing a singleton in a service document. */ export interface IODataSingletonInfo extends IODataServiceDocumentElement { } -/** Class representing a function Import in a service document. */ export class ODataFunctionImportInfo extends ODataServiceDocumentElement implements IODataFunctionImportInfo { constructor(data?: IODataFunctionImportInfo) { @@ -13273,7 +13230,6 @@ export class ODataFunctionImportInfo extends ODataServiceDocumentElement impleme } } -/** Class representing a function Import in a service document. */ export interface IODataFunctionImportInfo extends IODataServiceDocumentElement { } diff --git a/swagger.json b/swagger.json index 4ee957e5..c5c46961 100644 --- a/swagger.json +++ b/swagger.json @@ -943,11 +943,10 @@ "tags": [ "Metadata" ], - "summary": "Generates the OData $metadata document.", "operationId": "Metadata_GetMetadata", "responses": { "200": { - "description": "The IEdmModel representing $metadata.", + "description": "", "content": { "application/json": { "schema": { @@ -964,11 +963,10 @@ "tags": [ "Metadata" ], - "summary": "Generates the OData service document.", "operationId": "Metadata_GetServiceDocument", "responses": { "200": { - "description": "The service document for the service.", + "description": "", "content": { "application/json": { "schema": { @@ -1105,6 +1103,14 @@ "items": { "$ref": "#/components/schemas/SourcePhoneNumberConfiguration" } + }, + "timeZone": { + "nullable": true, + "oneOf": [ + { + "$ref": "#/components/schemas/TimeZoneInfo" + } + ] } } }, @@ -1120,6 +1126,34 @@ } } }, + "TimeZoneInfo": { + "type": "object", + "additionalProperties": false, + "properties": { + "Id": { + "type": "string" + }, + "HasIanaId": { + "type": "boolean" + }, + "DisplayName": { + "type": "string" + }, + "StandardName": { + "type": "string" + }, + "DaylightName": { + "type": "string" + }, + "BaseUtcOffset": { + "type": "string", + "format": "duration" + }, + "SupportsDaylightSavingTime": { + "type": "boolean" + } + } + }, "RoleDefinition": { "type": "object", "additionalProperties": false, @@ -6946,13 +6980,11 @@ }, "IEdmModel": { "type": "object", - "description": "Semantic representation of an EDM model.", "x-abstract": true, "additionalProperties": false, "properties": { "schemaElements": { "type": "array", - "description": "Gets the collection of schema elements that are contained in this model.", "nullable": true, "items": { "$ref": "#/components/schemas/IEdmSchemaElement" @@ -6960,7 +6992,6 @@ }, "vocabularyAnnotations": { "type": "array", - "description": "Gets the collection of vocabulary annotations that are contained in this model.", "nullable": true, "items": { "$ref": "#/components/schemas/IEdmVocabularyAnnotation" @@ -6968,7 +6999,6 @@ }, "referencedModels": { "type": "array", - "description": "Gets the collection of models referred to by this model (mainly by the this.References).", "nullable": true, "items": { "$ref": "#/components/schemas/IEdmModel" @@ -6976,14 +7006,12 @@ }, "declaredNamespaces": { "type": "array", - "description": "Gets the collection of namespaces that schema elements use contained in this model.", "nullable": true, "items": { "type": "string" } }, "directValueAnnotationsManager": { - "description": "Gets the model's annotations manager.", "nullable": true, "oneOf": [ { @@ -6992,7 +7020,6 @@ ] }, "entityContainer": { - "description": "Gets the only one entity container of the model.", "nullable": true, "oneOf": [ { @@ -7004,28 +7031,21 @@ }, "IEdmSchemaElement": { "type": "object", - "description": "Common base interface for all named children of EDM schema.", "x-abstract": true, "additionalProperties": false, "properties": { "schemaElementKind": { - "description": "Gets the kind of this schema element.", - "oneOf": [ - { - "$ref": "#/components/schemas/EdmSchemaElementKind" - } - ] + "$ref": "#/components/schemas/EdmSchemaElementKind" }, "namespace": { "type": "string", - "description": "Gets the namespace this schema element belongs to.", "nullable": true } } }, "EdmSchemaElementKind": { "type": "integer", - "description": "Defines EDM schema element types.", + "description": "", "x-enumNames": [ "None", "TypeDefinition", @@ -7045,17 +7065,14 @@ }, "IEdmVocabularyAnnotation": { "type": "object", - "description": "Represents an EDM vocabulary annotation.", "x-abstract": true, "additionalProperties": false, "properties": { "qualifier": { "type": "string", - "description": "Gets the qualifier used to discriminate between multiple bindings of the same property or type.", "nullable": true }, "term": { - "description": "Gets the term bound by the annotation.", "nullable": true, "oneOf": [ { @@ -7064,7 +7081,6 @@ ] }, "target": { - "description": "Gets the element the annotation applies to.", "nullable": true, "oneOf": [ { @@ -7073,7 +7089,6 @@ ] }, "value": { - "description": "Gets the expression producing the value of the annotation.", "nullable": true, "oneOf": [ { @@ -7085,12 +7100,10 @@ }, "IEdmTerm": { "type": "object", - "description": "Represents an EDM term.", "x-abstract": true, "additionalProperties": false, "properties": { "type": { - "description": "Gets the type of this term.", "nullable": true, "oneOf": [ { @@ -7100,28 +7113,23 @@ }, "appliesTo": { "type": "string", - "description": "Gets the AppliesTo of this term.", "nullable": true }, "defaultValue": { "type": "string", - "description": "Gets the DefaultValue of this term.", "nullable": true } } }, "IEdmTypeReference": { "type": "object", - "description": "Represents a references to a type.", "x-abstract": true, "additionalProperties": false, "properties": { "isNullable": { - "type": "boolean", - "description": "Gets a value indicating whether this type is nullable." + "type": "boolean" }, "definition": { - "description": "Gets the definition to which this type refers.", "nullable": true, "oneOf": [ { @@ -7133,23 +7141,17 @@ }, "IEdmType": { "type": "object", - "description": "Represents the definition of an EDM type.", "x-abstract": true, "additionalProperties": false, "properties": { "typeKind": { - "description": "Gets the kind of this type.", - "oneOf": [ - { - "$ref": "#/components/schemas/EdmTypeKind" - } - ] + "$ref": "#/components/schemas/EdmTypeKind" } } }, "EdmTypeKind": { "type": "integer", - "description": "Defines EDM metatypes.", + "description": "", "x-enumNames": [ "None", "Primitive", @@ -7177,29 +7179,22 @@ }, "IEdmVocabularyAnnotatable": { "type": "object", - "description": "Represents an element that can be targeted by Vocabulary Annotations", "x-abstract": true, "additionalProperties": false }, "IEdmExpression": { "type": "object", - "description": "Represents an EDM expression.", "x-abstract": true, "additionalProperties": false, "properties": { "expressionKind": { - "description": "Gets the kind of this expression.", - "oneOf": [ - { - "$ref": "#/components/schemas/EdmExpressionKind" - } - ] + "$ref": "#/components/schemas/EdmExpressionKind" } } }, "EdmExpressionKind": { "type": "integer", - "description": "Defines EDM expression kinds.", + "description": "", "x-enumNames": [ "None", "BinaryConstant", @@ -7259,19 +7254,16 @@ }, "IEdmDirectValueAnnotationsManager": { "type": "object", - "description": "Manages getting and setting direct annotations on EDM elements.", "x-abstract": true, "additionalProperties": false }, "IEdmEntityContainer": { "type": "object", - "description": "Represents an EDM entity container.", "x-abstract": true, "additionalProperties": false, "properties": { "elements": { "type": "array", - "description": "Gets a collection of the elements of this entity container.", "nullable": true, "items": { "$ref": "#/components/schemas/IEdmEntityContainerElement" @@ -7281,20 +7273,13 @@ }, "IEdmEntityContainerElement": { "type": "object", - "description": "Represents the common elements of all EDM entity container elements.", "x-abstract": true, "additionalProperties": false, "properties": { "containerElementKind": { - "description": "Gets the kind of element of this container element.", - "oneOf": [ - { - "$ref": "#/components/schemas/EdmContainerElementKind" - } - ] + "$ref": "#/components/schemas/EdmContainerElementKind" }, "container": { - "description": "Gets the container that contains this element.", "nullable": true, "oneOf": [ { @@ -7306,7 +7291,7 @@ }, "EdmContainerElementKind": { "type": "integer", - "description": "Defines EDM container element types.", + "description": "", "x-enumNames": [ "None", "EntitySet", @@ -7329,12 +7314,10 @@ }, { "type": "object", - "description": "Class representing the a service document.", "additionalProperties": false, "properties": { "entitySets": { "type": "array", - "description": "Gets or sets the set of entity sets in the service document.", "nullable": true, "items": { "$ref": "#/components/schemas/ODataEntitySetInfo" @@ -7342,7 +7325,6 @@ }, "singletons": { "type": "array", - "description": "Gets or sets the set of singletons in the service document.", "nullable": true, "items": { "$ref": "#/components/schemas/ODataSingletonInfo" @@ -7350,7 +7332,6 @@ }, "functionImports": { "type": "array", - "description": "Gets or sets the set of function imports in the service document.", "nullable": true, "items": { "$ref": "#/components/schemas/ODataFunctionImportInfo" @@ -7367,7 +7348,6 @@ }, { "type": "object", - "description": "Class representing a entity set in a service document.", "additionalProperties": false } ] @@ -7379,24 +7359,20 @@ }, { "type": "object", - "description": "Abstract class representing an element (EntitySet, Singleton) in a service document.", "x-abstract": true, "additionalProperties": false, "properties": { "url": { "type": "string", - "description": "Gets or sets the URI representing the Unified Resource Locator (URL) to the element.", "format": "uri", "nullable": true }, "name": { "type": "string", - "description": "Gets or sets the name of the element; this is the entity set or singleton name in JSON and the HREF in Atom.", "nullable": true }, "title": { "type": "string", - "description": "Gets or sets the title of the element; this is the title in JSON.", "nullable": true } } @@ -7405,12 +7381,10 @@ }, "ODataAnnotatable": { "type": "object", - "description": "Base class for all annotatable types in OData library.", "x-abstract": true, "additionalProperties": false, "properties": { "typeAnnotation": { - "description": "The annotation for storing @odata.type.", "nullable": true, "oneOf": [ { @@ -7422,12 +7396,10 @@ }, "ODataTypeAnnotation": { "type": "object", - "description": "Annotation which stores the EDM type information of a value.", "additionalProperties": false, "properties": { "typeName": { "type": "string", - "description": "Gets the type name to serialize, for the annotated item. ", "nullable": true } } @@ -7439,7 +7411,6 @@ }, { "type": "object", - "description": "Class representing a singleton in a service document.", "additionalProperties": false } ] @@ -7451,7 +7422,6 @@ }, { "type": "object", - "description": "Class representing a function Import in a service document.", "additionalProperties": false } ] diff --git a/test/CareTogether.Core.Test/ReferralCalculationTests/CalculateMissingMonitoringRequirementInstances.cs b/test/CareTogether.Core.Test/ReferralCalculationTests/CalculateMissingMonitoringRequirementInstances.cs index 4eeb32c2..f7593f27 100644 --- a/test/CareTogether.Core.Test/ReferralCalculationTests/CalculateMissingMonitoringRequirementInstances.cs +++ b/test/CareTogether.Core.Test/ReferralCalculationTests/CalculateMissingMonitoringRequirementInstances.cs @@ -25,7 +25,8 @@ public void TestNoCompletions() arrangementEndedAtUtc: null, completions: Helpers.Dates(), childLocationHistory: Helpers.LocationHistoryEntries(), - utcNow: new DateTime(H.YEAR, 2, 14)); + utcNow: new DateTime(H.YEAR, 2, 14), + H.US_EASTERN_TIME_ZONE); AssertEx.SequenceIs(result, Helpers.Dates((1, 3), (1, 10), (1, 17), (1, 24), (1, 31), (2, 14), (2, 28))); } @@ -42,7 +43,8 @@ public void TestNoCompletionsOneTimeRequirementDelayedDurationBased() arrangementEndedAtUtc: null, completions: Helpers.Dates(), childLocationHistory: Helpers.LocationHistoryEntries(), - utcNow: new DateTime(H.YEAR, 2, 14)); + utcNow: new DateTime(H.YEAR, 2, 14), + H.US_EASTERN_TIME_ZONE); AssertEx.SequenceIs(result, Helpers.Dates((1, 3))); } @@ -53,13 +55,14 @@ public void TestNoCompletionsOneTimeRequirementDelayed() var result = ReferralCalculations.CalculateMissingMonitoringRequirementInstances( new OneTimeRecurrencePolicy(TimeSpan.FromDays(2)), filterToFamilyId: null, - arrangementStartedAtUtc: new DateTime(H.YEAR, 1, 1), + arrangementStartedAtUtc: H.DateFromTimeZone(H.US_EASTERN_TIME_ZONE, 1, 1), arrangementEndedAtUtc: null, completions: Helpers.Dates(), childLocationHistory: Helpers.LocationHistoryEntries(), - utcNow: new DateTime(H.YEAR, 2, 14)); + utcNow: new DateTime(H.YEAR, 2, 14), + H.US_EASTERN_TIME_ZONE); - AssertEx.SequenceIs(result, Helpers.Dates((1, 3))); + AssertEx.SequenceIs(result, Helpers.DatesAtLastSecond(H.US_EASTERN_TIME_ZONE, (1, 3))); } [TestMethod] @@ -68,13 +71,14 @@ public void TestNoCompletionsOneTimeRequirementDelayedFuture() var result = ReferralCalculations.CalculateMissingMonitoringRequirementInstances( new OneTimeRecurrencePolicy(TimeSpan.FromDays(2)), filterToFamilyId: null, - arrangementStartedAtUtc: new DateTime(H.YEAR, 1, 1), + arrangementStartedAtUtc: H.DateFromTimeZone(H.US_EASTERN_TIME_ZONE, 1, 1), arrangementEndedAtUtc: null, completions: Helpers.Dates(), childLocationHistory: Helpers.LocationHistoryEntries(), - utcNow: new DateTime(H.YEAR, 1, 2)); + utcNow: H.DateFromTimeZone(H.US_EASTERN_TIME_ZONE, 1, 2), + H.US_EASTERN_TIME_ZONE); - AssertEx.SequenceIs(result, Helpers.Dates((1, 3))); + AssertEx.SequenceIs(result, Helpers.DatesAtLastSecond(H.US_EASTERN_TIME_ZONE, (1, 3))); } [TestMethod] @@ -87,7 +91,8 @@ public void TestOneOnTimeCompletionOneTimeRequirementDelayedFuture() arrangementEndedAtUtc: null, completions: Helpers.Dates((1, 2)), childLocationHistory: Helpers.LocationHistoryEntries(), - utcNow: new DateTime(H.YEAR, 1, 2)); + utcNow: new DateTime(H.YEAR, 1, 2), + H.US_EASTERN_TIME_ZONE); AssertEx.SequenceIs(result, []); } @@ -98,13 +103,14 @@ public void TestOnePastDueCompletionOneTimeRequirementDelayedFuture() var result = ReferralCalculations.CalculateMissingMonitoringRequirementInstances( new OneTimeRecurrencePolicy(TimeSpan.FromDays(2)), filterToFamilyId: null, - arrangementStartedAtUtc: new DateTime(H.YEAR, 1, 1), + arrangementStartedAtUtc: H.DateFromTimeZone(H.US_EASTERN_TIME_ZONE, 1, 1), arrangementEndedAtUtc: null, - completions: Helpers.Dates((1, 4)), + completions: Helpers.DatesFromTimeZone(H.US_EASTERN_TIME_ZONE, (1, 4)), childLocationHistory: Helpers.LocationHistoryEntries(), - utcNow: new DateTime(H.YEAR, 1, 2)); + utcNow: H.DateFromTimeZone(H.US_EASTERN_TIME_ZONE, 1, 2), + H.US_EASTERN_TIME_ZONE); - AssertEx.SequenceIs(result, Helpers.Dates((1, 3))); + AssertEx.SequenceIs(result, Helpers.DatesAtLastSecond(H.US_EASTERN_TIME_ZONE, (1, 3))); } [TestMethod] @@ -117,7 +123,8 @@ public void TestNoCompletionsOneTimeRequirementImmediate() arrangementEndedAtUtc: null, completions: Helpers.Dates(), childLocationHistory: Helpers.LocationHistoryEntries(), - utcNow: new DateTime(H.YEAR, 2, 14)); + utcNow: new DateTime(H.YEAR, 2, 14), + H.US_EASTERN_TIME_ZONE); //TODO: This could instead be "max date" -- need to decide what's more intuitive for users. AssertEx.SequenceIs(result, Helpers.Dates((1, 1))); @@ -130,13 +137,14 @@ public void TestOneCompletionOneTimeRequirementImmediate() var result = ReferralCalculations.CalculateMissingMonitoringRequirementInstances( new OneTimeRecurrencePolicy(TimeSpan.Zero), filterToFamilyId: null, - arrangementStartedAtUtc: new DateTime(H.YEAR, 1, 1), + arrangementStartedAtUtc: H.DateFromTimeZone(H.US_EASTERN_TIME_ZONE, 1, 1), arrangementEndedAtUtc: null, - completions: Helpers.Dates((1, 5)), + completions: Helpers.DatesFromTimeZone(H.US_EASTERN_TIME_ZONE, (1, 5)), childLocationHistory: Helpers.LocationHistoryEntries(), - utcNow: new DateTime(H.YEAR, 2, 14)); + utcNow: new DateTime(H.YEAR, 2, 14), + H.US_EASTERN_TIME_ZONE); - AssertEx.SequenceIs(result, Helpers.Dates((1, 1))); + AssertEx.SequenceIs(result, Helpers.DatesAtLastSecond(H.US_EASTERN_TIME_ZONE, (1, 1))); } [TestMethod] @@ -152,7 +160,8 @@ public void TestNoCompletionsEnded() arrangementEndedAtUtc: new DateTime(H.YEAR, 2, 1), completions: Helpers.Dates(), childLocationHistory: Helpers.LocationHistoryEntries(), - utcNow: new DateTime(H.YEAR, 2, 14)); + utcNow: new DateTime(H.YEAR, 2, 14), + H.US_EASTERN_TIME_ZONE); AssertEx.SequenceIs(result, Helpers.Dates((1, 3), (1, 10), (1, 17), (1, 24), (1, 31))); } @@ -183,7 +192,8 @@ public void TestNoCompletionsOccurrenceBasedNoSkip() (ChildLocationPlan.WithParent, 2, 18), (ChildLocationPlan.DaytimeChildCare, 2, 22), (ChildLocationPlan.WithParent, 2, 25)), - utcNow: new DateTime(H.YEAR, 2, 28)); + utcNow: new DateTime(H.YEAR, 2, 28), + H.US_EASTERN_TIME_ZONE); AssertEx.SequenceIs(result, Helpers.Dates((1, 3), (1, 24), (2, 17))); } @@ -214,7 +224,8 @@ public void TestNoCompletionsOccurrenceBasedWithSkip() (ChildLocationPlan.WithParent, 2, 18), (ChildLocationPlan.DaytimeChildCare, 2, 22), (ChildLocationPlan.WithParent, 2, 25)), - utcNow: new DateTime(H.YEAR, 2, 28)); + utcNow: new DateTime(H.YEAR, 2, 28), + H.US_EASTERN_TIME_ZONE); AssertEx.SequenceIs(result, Helpers.Dates((1, 17), (2, 10))); } @@ -236,7 +247,8 @@ public void TestNoCompletionsOccurrenceBasedNotYetReturnedPastDue() (ChildLocationPlan.DaytimeChildCare, 1, 15), (ChildLocationPlan.WithParent, 1, 18), (ChildLocationPlan.DaytimeChildCare, 1, 22)), - utcNow: new DateTime(H.YEAR, 2, 28)); + utcNow: new DateTime(H.YEAR, 2, 28), + H.US_EASTERN_TIME_ZONE); AssertEx.SequenceIs(result, Helpers.Dates((1, 3), (1, 24))); } @@ -258,7 +270,8 @@ public void TestNoCompletionsOccurrenceBasedNotYetReturnedDueInFuture() (ChildLocationPlan.DaytimeChildCare, 1, 15), (ChildLocationPlan.WithParent, 1, 18), (ChildLocationPlan.DaytimeChildCare, 1, 22)), - utcNow: new DateTime(H.YEAR, 2, 23)); + utcNow: new DateTime(H.YEAR, 2, 23), + H.US_EASTERN_TIME_ZONE); AssertEx.SequenceIs(result, Helpers.Dates((1, 3), (1, 24))); } @@ -276,7 +289,8 @@ public void TestOneCompletion() arrangementEndedAtUtc: null, completions: Helpers.Dates((1, 2)), childLocationHistory: Helpers.LocationHistoryEntries(), - utcNow: new DateTime(H.YEAR, 2, 14)); + utcNow: new DateTime(H.YEAR, 2, 14), + H.US_EASTERN_TIME_ZONE); AssertEx.SequenceIs(result, Helpers.Dates((1, 9), (1, 16), (1, 23), (1, 30), (2, 13), (2, 27))); } @@ -294,7 +308,8 @@ public void TestOneCompletionEnded() arrangementEndedAtUtc: new DateTime(H.YEAR, 2, 1), completions: Helpers.Dates((1, 2)), childLocationHistory: Helpers.LocationHistoryEntries(), - utcNow: new DateTime(H.YEAR, 2, 14)); + utcNow: new DateTime(H.YEAR, 2, 14), + H.US_EASTERN_TIME_ZONE); AssertEx.SequenceIs(result, Helpers.Dates((1, 9), (1, 16), (1, 23), (1, 30))); } @@ -310,7 +325,8 @@ public void TestEdgeCaseOneCompletionBeforeStarted() arrangementEndedAtUtc: new DateTime(H.YEAR, 1, 10), completions: Helpers.Dates((1, 5)), childLocationHistory: Helpers.LocationHistoryEntries(), - utcNow: new DateTime(H.YEAR, 2, 1)); + utcNow: new DateTime(H.YEAR, 2, 1), + H.US_EASTERN_TIME_ZONE); AssertEx.SequenceIs(result, Helpers.Dates()); } @@ -326,7 +342,8 @@ public void TestEdgeCaseOneCompletionAfterEnded() arrangementEndedAtUtc: new DateTime(H.YEAR, 1, 8), completions: Helpers.Dates((1, 9)), childLocationHistory: Helpers.LocationHistoryEntries(), - utcNow: new DateTime(H.YEAR, 2, 1)); + utcNow: new DateTime(H.YEAR, 2, 1), + H.US_EASTERN_TIME_ZONE); AssertEx.SequenceIs(result, Helpers.Dates()); } @@ -344,7 +361,8 @@ public void TestTwoCompletionsInFirstStage() arrangementEndedAtUtc: null, completions: Helpers.Dates((1, 2), (1, 2)), childLocationHistory: Helpers.LocationHistoryEntries(), - utcNow: new DateTime(H.YEAR, 2, 14)); + utcNow: new DateTime(H.YEAR, 2, 14), + H.US_EASTERN_TIME_ZONE); AssertEx.SequenceIs(result, Helpers.Dates((1, 9), (1, 16), (1, 23), (1, 30), (2, 13), (2, 27))); } @@ -362,7 +380,8 @@ public void TestTwoCompletionsInFirstStageOnStageEndDate() arrangementEndedAtUtc: null, completions: Helpers.Dates((1, 1), (1, 2)), childLocationHistory: Helpers.LocationHistoryEntries(), - utcNow: new DateTime(H.YEAR, 2, 14)); + utcNow: new DateTime(H.YEAR, 2, 14), + H.US_EASTERN_TIME_ZONE); AssertEx.SequenceIs(result, Helpers.Dates((1, 9), (1, 16), (1, 23), (1, 30), (2, 13), (2, 27))); } @@ -380,7 +399,8 @@ public void TestTwoCompletionsInFirstAndSecondStages() arrangementEndedAtUtc: null, completions: Helpers.Dates((1, 2), (1, 9)), childLocationHistory: Helpers.LocationHistoryEntries(), - utcNow: new DateTime(H.YEAR, 2, 14)); + utcNow: new DateTime(H.YEAR, 2, 14), + H.US_EASTERN_TIME_ZONE); AssertEx.SequenceIs(result, Helpers.Dates((1, 16), (1, 23), (1, 30), (2, 13), (2, 27))); } @@ -398,7 +418,8 @@ public void TestTwoCompletionsInFirstAndSecondStagesOneMissedDate() arrangementEndedAtUtc: null, completions: Helpers.Dates((1, 2), (1, 10)), childLocationHistory: Helpers.LocationHistoryEntries(), - utcNow: new DateTime(H.YEAR, 2, 14)); + utcNow: new DateTime(H.YEAR, 2, 14), + H.US_EASTERN_TIME_ZONE); AssertEx.SequenceIs(result, Helpers.Dates((1, 9), (1, 17), (1, 24), (1, 31), (2, 14), (2, 28))); } @@ -416,7 +437,8 @@ public void TestTwoCompletionsInFirstAndSecondStagesTwoMissedDates() arrangementEndedAtUtc: null, completions: Helpers.Dates((1, 2), (1, 20)), childLocationHistory: Helpers.LocationHistoryEntries(), - utcNow: new DateTime(H.YEAR, 2, 14)); + utcNow: new DateTime(H.YEAR, 2, 14), + H.US_EASTERN_TIME_ZONE); AssertEx.SequenceIs(result, Helpers.Dates((1, 9), (1, 16), (1, 27), (2, 10), (2, 24))); } @@ -434,7 +456,8 @@ public void TestTwoCompletionsInFirstAndSecondStagesTwoGapsOfMissedDates() arrangementEndedAtUtc: null, completions: Helpers.Dates((1, 2), (1, 20), (2, 9)), childLocationHistory: Helpers.LocationHistoryEntries(), - utcNow: new DateTime(H.YEAR, 2, 14)); + utcNow: new DateTime(H.YEAR, 2, 14), + H.US_EASTERN_TIME_ZONE); AssertEx.SequenceIs(result, Helpers.Dates((1, 9), (1, 16), (1, 27), (2, 23))); } @@ -452,7 +475,8 @@ public void TestNoCompletionsWithPerChildLocationDurationStagesNoLocations() arrangementEndedAtUtc: null, completions: Helpers.Dates(), childLocationHistory: Helpers.LocationHistoryEntries(), - utcNow: new DateTime(H.YEAR, 2, 14)); + utcNow: new DateTime(H.YEAR, 2, 14), + H.US_EASTERN_TIME_ZONE); AssertEx.SequenceIs(result, Helpers.Dates()); } @@ -471,7 +495,8 @@ public void TestNoCompletionsWithPerChildLocationDurationStagesOneLocation() completions: Helpers.Dates(), childLocationHistory: Helpers.LocationHistoryEntries( (ChildLocationPlan.DaytimeChildCare, 1, 1)), - utcNow: new DateTime(H.YEAR, 2, 14)); + utcNow: new DateTime(H.YEAR, 2, 14), + H.US_EASTERN_TIME_ZONE); AssertEx.SequenceIs(result, Helpers.Dates((1, 3), (1, 10), (1, 17), (1, 24), (1, 31), (2, 14), (2, 28))); } @@ -492,7 +517,8 @@ public void TestNoCompletionsWithPerChildLocationDurationStagesOneLocationWithRe (ChildLocationPlan.DaytimeChildCare, 1, 1), (ChildLocationPlan.WithParent, 1, 12), (ChildLocationPlan.DaytimeChildCare, 1, 15)), - utcNow: new DateTime(H.YEAR, 2, 14)); + utcNow: new DateTime(H.YEAR, 2, 14), + H.US_EASTERN_TIME_ZONE); AssertEx.SequenceIs(result, Helpers.Dates((1, 3), (1, 10), (1, 20), (1, 27), (2, 3), (2, 17))); } @@ -512,7 +538,8 @@ public void TestNoCompletionsWithPerChildLocationDurationStagesOneLocationWithRe childLocationHistory: Helpers.LocationHistoryEntries( (ChildLocationPlan.DaytimeChildCare, 1, 1), (ChildLocationPlan.WithParent, 2, 7)), - utcNow: new DateTime(H.YEAR, 2, 14)); + utcNow: new DateTime(H.YEAR, 2, 14), + H.US_EASTERN_TIME_ZONE); AssertEx.SequenceIs(result, Helpers.Dates((1, 3), (1, 10), (1, 17), (1, 24), (1, 31))); } @@ -530,7 +557,8 @@ public void TestNoCompletionsWithPerChildLocationDurationStagesTwoLocations() arrangementEndedAtUtc: null, completions: Helpers.Dates(), childLocationHistory: Helpers.LocationHistoryEntries(), - utcNow: new DateTime(H.YEAR, 2, 14)); + utcNow: new DateTime(H.YEAR, 2, 14), + H.US_EASTERN_TIME_ZONE); Assert.Inconclusive("Test not updated for multiple locations"); AssertEx.SequenceIs(result, Helpers.Dates(/*(1, 3), (1, 10), (1, 17), (1, 24), (1, 31), (2, 14), (2, 28)*/)); diff --git a/test/CareTogether.Core.Test/ReferralCalculationTests/CalculateMissingMonitoringRequirements.cs b/test/CareTogether.Core.Test/ReferralCalculationTests/CalculateMissingMonitoringRequirements.cs index ec55b1ff..bec4c669 100644 --- a/test/CareTogether.Core.Test/ReferralCalculationTests/CalculateMissingMonitoringRequirements.cs +++ b/test/CareTogether.Core.Test/ReferralCalculationTests/CalculateMissingMonitoringRequirements.cs @@ -39,7 +39,8 @@ public void TestNotStarted() Helpers.LocationHistoryEntries(), Helpers.LocationHistoryEntries(), Comments: null, Reason: null), - utcNow: new DateTime(H.YEAR, 1, 31)); + utcNow: new DateTime(H.YEAR, 1, 31), + H.US_EASTERN_TIME_ZONE); AssertEx.SequenceIs(result); } @@ -65,7 +66,8 @@ public void TestStartedNoCompletions() Helpers.LocationHistoryEntries(), Helpers.LocationHistoryEntries(), Comments: null, Reason: null), - utcNow: new DateTime(H.YEAR, 1, 31)); + utcNow: new DateTime(H.YEAR, 1, 31), + H.US_EASTERN_TIME_ZONE); AssertEx.SequenceIs(result, new MissingArrangementRequirement(null, null, null, null, "A", DueBy: null, PastDueSince: new DateTime(H.YEAR, 1, 3)), @@ -102,7 +104,8 @@ public void TestStartedSomeCompletions() Helpers.LocationHistoryEntries(), Helpers.LocationHistoryEntries(), Comments: null, Reason: null), - utcNow: new DateTime(H.YEAR, 1, 31)); + utcNow: new DateTime(H.YEAR, 1, 31), + H.US_EASTERN_TIME_ZONE); AssertEx.SequenceIs(result, new MissingArrangementRequirement(null, null, null, null, "A", DueBy: null, PastDueSince: new DateTime(H.YEAR, 1, 10)), @@ -137,7 +140,8 @@ public void TestStartedUpToDate() Helpers.LocationHistoryEntries(), Helpers.LocationHistoryEntries(), Comments: null, Reason: null), - utcNow: new DateTime(H.YEAR, 1, 8)); + utcNow: new DateTime(H.YEAR, 1, 8), + H.US_EASTERN_TIME_ZONE); AssertEx.SequenceIs(result, new MissingArrangementRequirement(null, null, null, null, "A", DueBy: new DateTime(H.YEAR, 1, 10), PastDueSince: null), diff --git a/test/CareTogether.Core.Test/ReferralCalculationTests/Helpers.cs b/test/CareTogether.Core.Test/ReferralCalculationTests/Helpers.cs index 3bbed7b7..0b0b1b10 100644 --- a/test/CareTogether.Core.Test/ReferralCalculationTests/Helpers.cs +++ b/test/CareTogether.Core.Test/ReferralCalculationTests/Helpers.cs @@ -10,6 +10,7 @@ namespace CareTogether.Core.Test.ReferralCalculationTests internal class Helpers { public const int YEAR = 2024; + public static TimeZoneInfo US_EASTERN_TIME_ZONE = TimeZoneInfo.FindSystemTimeZoneById("America/New_York"); public static ImmutableList Completed(params (string, int)[] completionsWithDates) => completionsWithDates.Select(completion => @@ -36,6 +37,15 @@ public static ImmutableList From(params string[] values) => public static ImmutableList Dates(params (int month, int day)[] values) => values.Select(value => new DateTime(YEAR, value.month, value.day)).ToImmutableList(); + public static ImmutableList DatesFromTimeZone(TimeZoneInfo tz, params (int month, int day)[] values) => + values.Select(value => TimeZoneInfo.ConvertTimeToUtc(new DateTime(YEAR, value.month, value.day), tz)).ToImmutableList(); + + public static DateTime DateFromTimeZone(TimeZoneInfo tz, int month, int day) => + TimeZoneInfo.ConvertTimeToUtc(new DateTime(YEAR, month, day), tz); + + public static ImmutableList DatesAtLastSecond(TimeZoneInfo tz, params (int month, int day)[] values) => + values.Select(value => TimeZoneInfo.ConvertTimeToUtc(new DateTime(YEAR, value.month, value.day, 23, 59, 59), tz)).ToImmutableList(); + public static ImmutableSortedSet LocationHistoryEntries( params (ChildLocationPlan plan, int month, int day)[] values) => values From 71dbe4a1cd785b3ff3041ddde9ed909d98cbd04a Mon Sep 17 00:00:00 2001 From: PabloDinella Date: Fri, 30 Aug 2024 17:43:46 -0300 Subject: [PATCH 2/2] Refactor CalculateMissingMonitoringRequirementInstancesForOneTimeRecurrence --- .../PolicyEvaluation/ReferralCalculations.cs | 88 ++++++++++++++----- ...teMissingMonitoringRequirementInstances.cs | 12 +-- 2 files changed, 72 insertions(+), 28 deletions(-) diff --git a/src/CareTogether.Core/Engines/PolicyEvaluation/ReferralCalculations.cs b/src/CareTogether.Core/Engines/PolicyEvaluation/ReferralCalculations.cs index 19fef99f..a50e254e 100644 --- a/src/CareTogether.Core/Engines/PolicyEvaluation/ReferralCalculations.cs +++ b/src/CareTogether.Core/Engines/PolicyEvaluation/ReferralCalculations.cs @@ -268,16 +268,51 @@ internal static ImmutableList CalculateMissingMon // requirement can be met at any time in the following 2 days after the startedAt date. // For that reason, we convert the datetimes to the client (location) timezone during calculation, // to avoid a completion falling in the next day for example. - internal static ImmutableList CalculateMissingMonitoringRequirementInstances( - RecurrencePolicy recurrence, Guid? filterToFamilyId, - DateTime arrangementStartedAtUtc, DateTime? arrangementEndedAtUtc, - ImmutableList completions, ImmutableSortedSet childLocationHistory, - DateTime utcNow, TimeZoneInfo locationTimeZone) + internal static ImmutableList CalculateMissingMonitoringRequirementInstances( + RecurrencePolicy recurrence, Guid? filterToFamilyId, + DateTime arrangementStartedAtUtc, DateTime? arrangementEndedAtUtc, + ImmutableList completions, ImmutableSortedSet childLocationHistory, + DateTime utcNow, TimeZoneInfo locationTimeZone) { + ////////////////////////////////////////////////// + //TODO: Move these timezone conversions out of the ReferralCalculations class and into the policy evaluation engine, + // where they can be performed by a helper class. + + // INPUTS: Given in UTC with time --> convert to location time --> extract date-only. + // OUTPUTS: Calculated in date-only --> convert to location time @ midnight --> return as UTC. + + var currentLocationTime = TimeZoneInfo.ConvertTimeFromUtc(utcNow, locationTimeZone); + + var arrangementStartedAtInLocationTime = TimeZoneInfo.ConvertTimeFromUtc(arrangementStartedAtUtc, locationTimeZone); + var arrangementStartedDate = DateOnly.FromDateTime(arrangementStartedAtInLocationTime); + + var completionDates = completions + .Select(completionWithTime => + { + var completionInLocationTime = TimeZoneInfo.ConvertTimeFromUtc(completionWithTime, locationTimeZone); + var completionDate = DateOnly.FromDateTime(completionInLocationTime); + return completionDate; + }) + .ToImmutableList(); + + var today = DateOnly.FromDateTime(utcNow); + + ImmutableList WrapWithTimeInUtc(ImmutableList dates) + { + return dates.Select(date => + { + var dateInLocationTime = new DateTime(date, TimeOnly.MinValue); + var dateTimeInUtc = TimeZoneInfo.ConvertTimeToUtc(dateInLocationTime, locationTimeZone); + return dateTimeInUtc; + }).ToImmutableList(); + } + ////////////////////////////////////////////////// + return recurrence switch { - OneTimeRecurrencePolicy oneTime => CalculateMissingMonitoringRequirementInstancesForOneTimeRecurrence( - oneTime, arrangementStartedAtUtc, arrangementEndedAtUtc, completions, utcNow, locationTimeZone), + OneTimeRecurrencePolicy oneTime => WrapWithTimeInUtc( + CalculateMissingMonitoringRequirementInstancesForOneTimeRecurrence( + oneTime, arrangementStartedDate, completionDates, today)), DurationStagesRecurrencePolicy durationStages => CalculateMissingMonitoringRequirementInstancesForDurationRecurrence( durationStages, arrangementStartedAtUtc, arrangementEndedAtUtc, utcNow, completions), @@ -294,28 +329,37 @@ internal static ImmutableList CalculateMissingMonitoringRequirementIns }; } - internal static ImmutableList CalculateMissingMonitoringRequirementInstancesForOneTimeRecurrence( - OneTimeRecurrencePolicy recurrence, DateTime arrangementStartedAtUtc, DateTime? arrangementEndedAtUtc, - ImmutableList completions, DateTime utcNow, TimeZoneInfo locationTimeZone) + internal static ImmutableList CalculateMissingMonitoringRequirementInstancesForOneTimeRecurrence( + OneTimeRecurrencePolicy recurrence, DateOnly arrangementStartedDate, + ImmutableList completions, DateOnly today) { if (recurrence.Delay.HasValue) { - var arrangementStartedAtInClientsTimeZone = TimeZoneInfo.ConvertTimeFromUtc(arrangementStartedAtUtc, locationTimeZone); + var dueDate = arrangementStartedDate.AddDays(recurrence.Delay.Value.Days); - var dueBaseDateInClientsTimeZone = new DateTime(DateOnly.FromDateTime(arrangementStartedAtInClientsTimeZone), new TimeOnly()).AddDays(1); - - var dueDateInClientsTimeZone = (dueBaseDateInClientsTimeZone + recurrence.Delay.Value).AddSeconds(-1); - - return completions.Any(completion => TimeZoneInfo.ConvertTimeFromUtc(completion, locationTimeZone) <= dueDateInClientsTimeZone) - ? [] - : [TimeZoneInfo.ConvertTimeToUtc(dueDateInClientsTimeZone, locationTimeZone)]; + if (completions.Any(completion => completion <= dueDate)) + { + return []; + } + else + { + return [dueDate]; + } + } else - return completions.IsEmpty - ? [arrangementStartedAtUtc] - : []; + { + if (completions.IsEmpty) + { + return [arrangementStartedDate]; + } + else + { + return []; + } + } } - + internal static ImmutableList CalculateMissingMonitoringRequirementInstancesForDurationRecurrence( DurationStagesRecurrencePolicy recurrence, DateTime arrangementStartedAtUtc, DateTime? arrangementEndedAtUtc, DateTime utcNow, diff --git a/test/CareTogether.Core.Test/ReferralCalculationTests/CalculateMissingMonitoringRequirementInstances.cs b/test/CareTogether.Core.Test/ReferralCalculationTests/CalculateMissingMonitoringRequirementInstances.cs index f7593f27..72076bf9 100644 --- a/test/CareTogether.Core.Test/ReferralCalculationTests/CalculateMissingMonitoringRequirementInstances.cs +++ b/test/CareTogether.Core.Test/ReferralCalculationTests/CalculateMissingMonitoringRequirementInstances.cs @@ -62,7 +62,7 @@ public void TestNoCompletionsOneTimeRequirementDelayed() utcNow: new DateTime(H.YEAR, 2, 14), H.US_EASTERN_TIME_ZONE); - AssertEx.SequenceIs(result, Helpers.DatesAtLastSecond(H.US_EASTERN_TIME_ZONE, (1, 3))); + AssertEx.SequenceIs(result, Helpers.DatesFromTimeZone(H.US_EASTERN_TIME_ZONE, (1, 3))); } [TestMethod] @@ -78,7 +78,7 @@ public void TestNoCompletionsOneTimeRequirementDelayedFuture() utcNow: H.DateFromTimeZone(H.US_EASTERN_TIME_ZONE, 1, 2), H.US_EASTERN_TIME_ZONE); - AssertEx.SequenceIs(result, Helpers.DatesAtLastSecond(H.US_EASTERN_TIME_ZONE, (1, 3))); + AssertEx.SequenceIs(result, Helpers.DatesFromTimeZone(H.US_EASTERN_TIME_ZONE, (1, 3))); } [TestMethod] @@ -110,7 +110,7 @@ public void TestOnePastDueCompletionOneTimeRequirementDelayedFuture() utcNow: H.DateFromTimeZone(H.US_EASTERN_TIME_ZONE, 1, 2), H.US_EASTERN_TIME_ZONE); - AssertEx.SequenceIs(result, Helpers.DatesAtLastSecond(H.US_EASTERN_TIME_ZONE, (1, 3))); + AssertEx.SequenceIs(result, Helpers.DatesFromTimeZone(H.US_EASTERN_TIME_ZONE, (1, 3))); } [TestMethod] @@ -119,7 +119,7 @@ public void TestNoCompletionsOneTimeRequirementImmediate() var result = ReferralCalculations.CalculateMissingMonitoringRequirementInstances( new OneTimeRecurrencePolicy(null), filterToFamilyId: null, - arrangementStartedAtUtc: new DateTime(H.YEAR, 1, 1), + arrangementStartedAtUtc: H.DateFromTimeZone(H.US_EASTERN_TIME_ZONE, 1, 1), arrangementEndedAtUtc: null, completions: Helpers.Dates(), childLocationHistory: Helpers.LocationHistoryEntries(), @@ -127,7 +127,7 @@ public void TestNoCompletionsOneTimeRequirementImmediate() H.US_EASTERN_TIME_ZONE); //TODO: This could instead be "max date" -- need to decide what's more intuitive for users. - AssertEx.SequenceIs(result, Helpers.Dates((1, 1))); + AssertEx.SequenceIs(result, Helpers.DatesFromTimeZone(H.US_EASTERN_TIME_ZONE, (1, 1))); } [TestMethod] @@ -144,7 +144,7 @@ public void TestOneCompletionOneTimeRequirementImmediate() utcNow: new DateTime(H.YEAR, 2, 14), H.US_EASTERN_TIME_ZONE); - AssertEx.SequenceIs(result, Helpers.DatesAtLastSecond(H.US_EASTERN_TIME_ZONE, (1, 1))); + AssertEx.SequenceIs(result, Helpers.DatesFromTimeZone(H.US_EASTERN_TIME_ZONE, (1, 1))); } [TestMethod]