Skip to content
Open
4 changes: 3 additions & 1 deletion .AL-Go/settings.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
{
"$schema": "https://raw.githubusercontent.com/Freddy-D4P/AL-Go/main/Actions/.Modules/settings.schema.json",
"appFolders": [],
"testFolders": [],
"testFolders": [
"CCMSTests"
],
"bcptTestFolders": [],
"enableCodeCop": true,
"enableUICop": true,
Expand Down
7 changes: 7 additions & 0 deletions CCMS/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,12 @@
"features": [
"NoImplicitWith",
"TranslationFile"
],
"internalsVisibleTo": [
{
"id": "f10bf877-caa5-49c6-882e-22922fca331c",
"name": "Cloud Customer Management Solution Tests",
"publisher": "Directions for partners"
}
]
}
7 changes: 3 additions & 4 deletions CCMS/src/Environment/D4PBCEnvironmentCard.page.al
Original file line number Diff line number Diff line change
Expand Up @@ -364,20 +364,19 @@ page 62004 "D4P BC Environment Card"
SelectedDate: Date;
ExpectedMonth: Integer;
ExpectedYear: Integer;
IgnoreUpdateWindow: Boolean;
NoUpdatesAvailableErr: Label 'No updates available for the environment %1.', Comment = '%1 = Environment Name';
TargetVersion: Text[100];
begin
// Get available updates from API
EnvironmentManagement.GetAvailableUpdates(Rec, TempAvailableUpdate);

if TempAvailableUpdate.IsEmpty() then
Error(NoUpdatesAvailableErr, Rec.Name);

// Pass data to selection dialog and show it
UpdateSelectionDialog.SetData(TempAvailableUpdate);
if UpdateSelectionDialog.RunModal() = Action::OK then begin
UpdateSelectionDialog.GetSelectedVersion(TargetVersion, SelectedDate, ExpectedMonth, ExpectedYear);
EnvironmentManagement.SelectTargetVersion(Rec, TargetVersion, SelectedDate, ExpectedMonth, ExpectedYear);
UpdateSelectionDialog.GetSelectedVersion(TargetVersion, SelectedDate, ExpectedMonth, ExpectedYear, IgnoreUpdateWindow);
EnvironmentManagement.SelectTargetVersion(Rec, TargetVersion, SelectedDate, ExpectedMonth, ExpectedYear, IgnoreUpdateWindow);
CurrPage.Update(false);
end;
end;
Expand Down
542 changes: 234 additions & 308 deletions CCMS/src/Environment/D4PBCEnvironmentMgt.codeunit.al

Large diffs are not rendered by default.

32 changes: 16 additions & 16 deletions CCMS/src/Environment/D4PUpdateSelectionDialog.page.al
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,21 @@ page 62025 "D4P Update Selection Dialog"
Editable = DateFieldEditable;
Visible = DateFieldsVisible;
StyleExpr = RowStyleExpr;
ToolTip = 'Specifies the date for the update. Select a date between today and the latest selectable date.';
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new tooltip says the date must be between today and the latest selectable date, but this dialog explicitly supports cases where Latest Selectable Date is 0D (no upper bound) and defaults to Today(). Consider rewording the tooltip to reflect that the upper bound applies only when a latest selectable date is provided.

Suggested change
ToolTip = 'Specifies the date for the update. Select a date between today and the latest selectable date.';
ToolTip = 'Specifies the date for the update. Select a date from today onward, and no later than the latest selectable date when one is provided.';

Copilot uses AI. Check for mistakes.

trigger OnValidate()
begin
ValidateSelectedDate();
end;
}
field("Ignore Update Window"; Rec."Ignore Update Window")
{
Caption = 'Ignore Update Window';
Editable = DateFieldEditable;
Visible = DateFieldsVisible;
StyleExpr = RowStyleExpr;
ToolTip = 'Specifies whether to bypass the configured update window and schedule the update for any time on the selected date.';
}
field("Rollout Status"; Rec."Rollout Status")
{
Editable = false;
Expand All @@ -71,11 +80,6 @@ page 62025 "D4P Update Selection Dialog"
StyleExpr = RowStyleExpr;
}
}
group(SelectionGroup)
{
Caption = 'Schedule Details';
Visible = false; // Hidden since Selected Date is now in repeater
}
}
}

Expand Down Expand Up @@ -114,31 +118,28 @@ page 62025 "D4P Update Selection Dialog"

local procedure UpdateFieldVisibility()
begin
// Show date fields if version is available
DateFieldsVisible := Rec.Available;
DateFieldEditable := Rec.Available and (Rec."Latest Selectable Date" <> 0D);

// Show expected fields if version is not available
DateFieldEditable := Rec.Available;
ExpectedFieldsVisible := not Rec.Available;
end;

local procedure SetDefaultDateTime()
begin
// Set default date to Latest Selectable Date if available
if (Rec."User Selected Date" = 0D) and DateFieldEditable then
Rec."User Selected Date" := Rec."Latest Selectable Date";
if Rec."Latest Selectable Date" <> 0D then
Rec."User Selected Date" := Rec."Latest Selectable Date"
else
Rec."User Selected Date" := Today();
end;

local procedure ValidateSelectedDate()
begin
if Rec."User Selected Date" = 0D then
exit;

// Validate that selected date is not in the past
if Rec."User Selected Date" < Today() then
Error(DateTooEarlyErr);

// Validate that selected date is not later than Latest Selectable Date
if (Rec."Latest Selectable Date" <> 0D) and (Rec."User Selected Date" > Rec."Latest Selectable Date") then
Error(DateTooLateErr, Rec."Latest Selectable Date");
end;
Expand All @@ -150,7 +151,6 @@ page 62025 "D4P Update Selection Dialog"
begin
CurrentEntryNo := Rec."Entry No.";

// If this record is now selected, unselect all others
if Rec.Selected then begin
TempUpdate.Copy(Rec, true);
TempUpdate.Reset();
Expand All @@ -164,17 +164,17 @@ page 62025 "D4P Update Selection Dialog"
end;
end;

procedure GetSelectedVersion(var TargetVersion: Text[100]; var SelectedDate: Date; var ExpectedMonth: Integer; var ExpectedYear: Integer)
procedure GetSelectedVersion(var TargetVersion: Text[100]; var SelectedDate: Date; var ExpectedMonth: Integer; var ExpectedYear: Integer; var IgnoreUpdateWindow: Boolean)
begin
TargetVersion := Rec."Target Version";
SelectedDate := Rec."User Selected Date";
ExpectedMonth := Rec."Expected Month";
ExpectedYear := Rec."Expected Year";
IgnoreUpdateWindow := Rec."Ignore Update Window";
end;
Comment on lines +167 to 174
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GetSelectedVersion returns values from the current Rec, not from the row where Selected = true. If the user selects one version and then moves focus to a different row before pressing OK, this can return (and schedule) the wrong target version/date/window. Consider locating the selected record (e.g., filter Selected = true, FindFirst) and erroring if none is selected; also enforce that an available update has a non-0D selected date before returning.

Copilot uses AI. Check for mistakes.

procedure SetData(var TempSourceUpdate: Record "D4P BC Available Update" temporary)
begin
// Copy all records from source temporary table to page's temporary table
TempSourceUpdate.Reset();
if TempSourceUpdate.FindSet() then
repeat
Expand Down
40 changes: 40 additions & 0 deletions CCMSTests/app.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"id": "f10bf877-caa5-49c6-882e-22922fca331c",
"name": "Cloud Customer Management Solution Tests",
"publisher": "Directions for partners",
"version": "0.0.2.0",
"brief": "Test app for CCMS.",
"description": "Automated tests for the Cloud Customer Management Solution.",
"privacyStatement": "",
"EULA": "",
"help": "",
"url": "",
"logo": "",
"dependencies": [
{
"id": "e33746ef-a796-46ca-8a1a-fb3840a42c48",
"name": "Cloud Customer Management Solution",
"publisher": "Directions for partners",
"version": "0.0.2.0"
}
],
"screenshots": [],
"platform": "1.0.0.0",
"application": "27.0.0.0",
"idRanges": [
{
"from": 62050,
"to": 62099
}
Comment thread
jeffreybulanadi marked this conversation as resolved.
],
Comment thread
jeffreybulanadi marked this conversation as resolved.
"resourceExposurePolicy": {
"allowDebugging": true,
"allowDownloadingSource": true,
"includeSourceInSymbolFile": true
},
"runtime": "16.0",
"features": [
"NoImplicitWith",
"TranslationFile"
]
}
153 changes: 153 additions & 0 deletions CCMSTests/src/D4PJsonHelperTests.codeunit.al
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
namespace D4P.CCMS.Environment.Tests;

using D4P.CCMS.Environment;

codeunit 62051 "D4P JSON Helper Tests"
{
Subtype = Test;

var
EnvironmentMgt: Codeunit "D4P BC Environment Mgt";

[Test]
procedure TryGetJsonDate_DateOnlyString_ParsesCorrectly()
var
JsonObj: JsonObject;
ResultDate: Date;
Found: Boolean;
begin
// Arrange: date-only ISO string (root cause of Bug 1)
JsonObj.Add('latestSelectableDate', '2025-06-15');

// Act
Found := EnvironmentMgt.TryGetJsonDate(JsonObj, 'latestSelectableDate', ResultDate);

// Assert
VerifyIsTrue(Found, 'TryGetJsonDate should return true for date-only string');
VerifyDateEqual(ResultDate, DMY2Date(15, 6, 2025), 'Parsed date');
end;

[Test]
procedure TryGetJsonDate_DateTimeString_ParsesDatePortion()
var
JsonObj: JsonObject;
ResultDate: Date;
Found: Boolean;
begin
// Arrange: datetime string — the root cause scenario (AsDateTime() returned 0DT for this)
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment says the datetime string case is “the root cause scenario (AsDateTime() returned 0DT for this)”, but the PR description attributes the 0DT behavior to date-only strings like YYYY-MM-DD. Consider adjusting the comment so it matches the actual root cause being tested (e.g., “datetime fallback parsing”) to avoid misleading future readers.

Suggested change
// Arrange: datetime string — the root cause scenario (AsDateTime() returned 0DT for this)
// Arrange: datetime string — validates datetime fallback parsing when extracting the date portion

Copilot uses AI. Check for mistakes.
JsonObj.Add('latestSelectableDate', '2025-06-15T00:00:00Z');

// Act
Found := EnvironmentMgt.TryGetJsonDate(JsonObj, 'latestSelectableDate', ResultDate);

// Assert
VerifyIsTrue(Found, 'TryGetJsonDate should return true for datetime string');
VerifyDateEqual(ResultDate, DMY2Date(15, 6, 2025), 'Parsed date from datetime string');
end;

[Test]
procedure TryGetJsonDate_NullValue_ReturnsFalse()
var
JsonObj: JsonObject;
NullToken: JsonToken;
ResultDate: Date;
Found: Boolean;
begin
// Arrange: JSON null value
NullToken.ReadFrom('null');
JsonObj.Add('latestSelectableDate', NullToken);

// Act
Found := EnvironmentMgt.TryGetJsonDate(JsonObj, 'latestSelectableDate', ResultDate);

// Assert
VerifyIsFalse(Found, 'TryGetJsonDate should return false for null');
VerifyDateEqual(ResultDate, 0D, 'ResultDate should be 0D for null');
end;

[Test]
procedure TryGetJsonDate_MissingField_ReturnsFalse()
var
JsonObj: JsonObject;
ResultDate: Date;
Found: Boolean;
begin
// Arrange: empty object
// Act
Found := EnvironmentMgt.TryGetJsonDate(JsonObj, 'latestSelectableDate', ResultDate);

// Assert
VerifyIsFalse(Found, 'TryGetJsonDate should return false for missing field');
VerifyDateEqual(ResultDate, 0D, 'ResultDate should be 0D for missing field');
end;

[Test]
procedure TryGetJsonDateTime_ValidDateTimeString_ParsesCorrectly()
var
JsonObj: JsonObject;
ResultDateTime: DateTime;
Found: Boolean;
begin
// Arrange
JsonObj.Add('selectedDateTime', '2025-06-15T10:30:00');

// Act
Found := EnvironmentMgt.TryGetJsonDateTime(JsonObj, 'selectedDateTime', ResultDateTime);

// Assert
VerifyIsTrue(Found, 'TryGetJsonDateTime should return true for valid string');
VerifyIsTrue(ResultDateTime <> 0DT, 'ResultDateTime should not be 0DT');
end;

[Test]
procedure TryGetJsonDateTime_NullValue_ReturnsFalse()
var
JsonObj: JsonObject;
NullToken: JsonToken;
ResultDateTime: DateTime;
Found: Boolean;
begin
// Arrange
NullToken.ReadFrom('null');
JsonObj.Add('selectedDateTime', NullToken);

// Act
Found := EnvironmentMgt.TryGetJsonDateTime(JsonObj, 'selectedDateTime', ResultDateTime);

// Assert
VerifyIsFalse(Found, 'TryGetJsonDateTime should return false for null');
VerifyIsTrue(ResultDateTime = 0DT, 'ResultDateTime should be 0DT for null');
end;

[Test]
procedure TryGetJsonDateTime_MissingField_ReturnsFalse()
var
JsonObj: JsonObject;
ResultDateTime: DateTime;
Found: Boolean;
begin
// Act
Found := EnvironmentMgt.TryGetJsonDateTime(JsonObj, 'selectedDateTime', ResultDateTime);

// Assert
VerifyIsFalse(Found, 'TryGetJsonDateTime should return false for missing field');
end;

local procedure VerifyIsTrue(Value: Boolean; Msg: Text)
begin
if not Value then
Error('Assert failed (expected true): %1', Msg);
end;

local procedure VerifyIsFalse(Value: Boolean; Msg: Text)
begin
if Value then
Error('Assert failed (expected false): %1', Msg);
end;

local procedure VerifyDateEqual(Actual: Date; Expected: Date; FieldName: Text)
begin
if Actual <> Expected then
Error('Assert failed (%1): expected %2, got %3', FieldName, Expected, Actual);
end;
}
Loading
Loading