';\n+ until VendorLedgerEntry.Next() = 0;\n+ HtmlBody += '
';\n+ exit(HtmlBody);\n+ end;\n+\n+ procedure BuildItemSummary(LocationCode: Code[10]): Text\n+ var\n+ ItemLedgerEntry: Record \"Item Ledger Entry\";\n+ Summary: TextBuilder;\n+ begin\n+ Summary.Append('Location: ' + LocationCode);\n+ Summary.AppendLine();\n+ Summary.Append('Item No. | Description | Quantity');\n+ Summary.AppendLine();\n+ ItemLedgerEntry.SetRange(\"Location Code\", LocationCode);\n+ ItemLedgerEntry.SetRange(\"Entry Type\", ItemLedgerEntry.\"Entry Type\"::Purchase);\n+ ItemLedgerEntry.SetLoadFields(\"Item No.\", Description, Quantity);\n+ if ItemLedgerEntry.FindSet() then\n+ repeat\n+ Summary.Append(ItemLedgerEntry.\"Item No.\");\n+ Summary.Append(' | ');\n+ Summary.Append(ItemLedgerEntry.Description);\n+ Summary.Append(' | ');\n+ Summary.Append(Format(ItemLedgerEntry.Quantity));\n+ Summary.AppendLine();\n+ until ItemLedgerEntry.Next() = 0;\n+ exit(Summary.ToText());\n+ end;\n+}\n+", "expected_comments": [{"file": "src/ReportBuilder.Codeunit.al", "line_start": 16, "line_end": 16, "body": "String concatenation with += inside a FindSet loop building CSV output. Each += allocates a new string, resulting in O(n\u00b2) performance for large record sets. \u2014 Use TextBuilder to accumulate the output string. TextBuilder.Append() is O(1) amortized and avoids repeated memory allocation.", "severity": "medium"}, {"file": "src/ReportBuilder.Codeunit.al", "line_start": 37, "line_end": 37, "body": "String concatenation with += inside a repeat..until loop building HTML body. Each concatenation copies the entire accumulated string, degrading performance as the string grows. \u2014 Use TextBuilder to construct the HTML body. Replace HtmlBody += with TextBuilder.Append() calls for efficient string building in loops.", "severity": "medium"}], "match_line_tolerance": 2, "domain": "performance", "category": "code-review", "description": "True positive performance findings: textbuilder (2 findings). String concatenation with += inside loops causes O(n\u00b2) memory allocations; TextBuilder should be used instead.", "expect_findings": true, "source": "vsoadmin"}
+{"repo": "microsoft/BCApps", "instance_id": "synthetic__performance-032", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "performance"}, "patch": "--- src/ItemMigrator.Codeunit.al\n+++ src/ItemMigrator.Codeunit.al\n+codeunit 50320 \"Item Migrator\"\n+{\n+ procedure MigrateItemDescriptions()\n+ var\n+ Item: Record Item;\n+ begin\n+ Item.SetFilter(Description, '<>%1', '');\n+ if Item.FindSet(true) then\n+ repeat\n+ Item.Description := ConvertToNewFormat(Item.Description);\n+ // Performance issue: Modify(true) fires OnModify trigger per record\n+ // Item table can have 800k records \u2014 triggers run 800k times\n+ Item.Modify(true);\n+ until Item.Next() = 0;\n+ end;\n+\n+ local procedure ConvertToNewFormat(OldDesc: Text[100]): Text[100]\n+ begin\n+ exit(OldDesc.TrimEnd());\n+ end;\n+}\n--- src/TestDataGenerator.Codeunit.al\n+++ src/TestDataGenerator.Codeunit.al\n+codeunit 50321 \"Test Data Generator\"\n+{\n+ procedure CreateTestEntries(Count: Integer)\n+ var\n+ ErrorMessageRegister: Record \"Error Message Register\";\n+ i: Integer;\n+ begin\n+ for i := 1 to Count do begin\n+ ErrorMessageRegister.Init();\n+ ErrorMessageRegister.\"Entry No.\" := i;\n+ ErrorMessageRegister.Description := StrSubstNo('Test entry %1', i);\n+ ErrorMessageRegister.\"Created Date\" := Today;\n+ // Performance issue: Insert(true) fires OnInsert for each record\n+ // Use Insert(false) when triggers aren't needed for test data\n+ ErrorMessageRegister.Insert(true);\n+ end;\n+ end;\n+}", "expected_comments": [{"file": "src/ItemMigrator.Codeunit.al", "line_start": 13, "line_end": 13, "body": "Modify(true) inside loop fires OnModify triggers for each record. For bulk operations, consider Modify(false) unless triggers are required, or use bulk update operations. \u2014 ", "severity": "low"}, {"file": "src/TestDataGenerator.Codeunit.al", "line_start": 15, "line_end": 15, "body": "Insert(true) in loop fires OnInsert triggers for each record. For test data or bulk inserts, use Insert(false) unless triggers are needed. \u2014 ", "severity": "low"}], "match_line_tolerance": 2, "domain": "performance", "category": "code-review", "description": "True positive performance findings: Insert(true)/Modify(true) trigger overhead in loops", "expect_findings": true, "source": "vsoadmin"}
+{"repo": "microsoft/BCApps", "instance_id": "synthetic__privacy-001", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "privacy"}, "patch": "--- src/PostalCodeLookupService.Codeunit.al\n+++ src/PostalCodeLookupService.Codeunit.al\n+codeunit 50113 PostalCodeLookupService\n+{\n+ procedure LookupPostalAddress(PostalCode: Code[20]; City: Text[50]): Text[250]\n+ var\n+ HttpClient: HttpClient;\n+ HttpResponse: HttpResponseMessage;\n+ RequestUri: Text;\n+ ResponseText: Text;\n+ begin\n+ RequestUri := StrSubstNo('https://postal-api.service.com/lookup?postal=%1&city=%2', PostalCode, City);\n+\n+ if HttpClient.Get(RequestUri, HttpResponse) then begin\n+ HttpResponse.Content.ReadAs(ResponseText);\n+ exit(ParseAddressResponse(ResponseText));\n+ end;\n+\n+ exit('');\n+ end;\n+\n+ local procedure ParseAddressResponse(JsonResponse: Text): Text[250]\n+ var\n+ JsonObject: JsonObject;\n+ AddressToken: JsonToken;\n+ begin\n+\n+ if JsonObject.ReadFrom(JsonResponse) then\n+ if JsonObject.Get('standardized_address', AddressToken) then\n+ exit(CopyStr(AddressToken.AsValue().AsText(), 1, 250));\n+\n+ exit('');\n+ end;\n+\n+ procedure ValidateBusinessAddress(var BusinessAddress: Record \"Business Address\")\n+ var\n+ StandardizedAddress: Text[250];\n+ begin\n+\n+ StandardizedAddress := LookupPostalAddress(BusinessAddress.\"Postal Code\", BusinessAddress.City);\n+\n+ if StandardizedAddress <> '' then begin\n+ BusinessAddress.\"Validated Address\" := StandardizedAddress;\n+ BusinessAddress.\"Validation Status\" := BusinessAddress.\"Validation Status\"::Validated;\n+ BusinessAddress.Modify();\n+ end;\n+ end;\n+}", "expected_comments": [], "match_line_tolerance": 2, "domain": "privacy", "category": "code-review", "description": "Address/postcode data classification (4 false positives). Agent flags address fields or postcode lookup data as incorrectly classified. Reviewers reject because: (1) address data in lookup tables is reference data not PII, (2) country/region codes are not personally identifiable, (3) the classification is appropriate for the context.", "expect_findings": false, "source": "vsoadmin"}
+{"repo": "microsoft/BCApps", "instance_id": "synthetic__privacy-002", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "privacy"}, "patch": "--- src/SystemConfigurationLog.Table.al\n+++ src/SystemConfigurationLog.Table.al\n+table 50111 \"System Configuration Log\"\n+{\n+ DataClassification = SystemMetadata; // System-level data for operational purposes\n+ TableType = Temporary;\n+\n+ fields\n+ {\n+ field(1; \"Entry No.\"; Integer)\n+ {\n+ AutoIncrement = true;\n+ Caption = 'Entry No.';\n+ }\n+ field(2; \"Configuration Area\"; Text[50])\n+ {\n+ Caption = 'Configuration Area';\n+ }\n+ field(3; \"Parameter Name\"; Text[100])\n+ {\n+ Caption = 'Parameter Name';\n+ }\n+ field(4; \"Parameter Value\"; Text[250])\n+ {\n+ Caption = 'Parameter Value';\n+ }\n+ }\n+}", "expected_comments": [], "match_line_tolerance": 2, "domain": "privacy", "category": "code-review", "description": "Missing DataClassification on table fields (59 false positives). Agent flags table fields missing explicit DataClassification property. Reviewers reject because: (1) table-level DataClassification covers all fields, (2) fields contain system/business data not PII, (3) fields are in temporary tables, or (4) the classification is inherited.", "expect_findings": false, "source": "vsoadmin"}
+{"repo": "microsoft/BCApps", "instance_id": "synthetic__privacy-003", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "privacy"}, "patch": "--- src/EAOutboxEmail.Table.al\n+++ src/EAOutboxEmail.Table.al\n+namespace Microsoft.ExpenseAgent.Email;\n+\n+using Microsoft.Foundation.Address;\n+using Microsoft.Foundation.Company;\n+using System.Email;\n+\n+table 57134 \"EA Outbox Email\"\n+{\n+ Caption = 'Expense Agent Outbox Email';\n+ DataCaptionFields = \"Entry No.\", Subject, \"To Line\";\n+\n+ fields\n+ {\n+ field(1; \"Entry No.\"; Integer)\n+ {\n+ AutoIncrement = true;\n+ Caption = 'Entry No.';\n+ DataClassification = SystemMetadata;\n+ }\n+ field(5; \"Message ID\"; Text[100])\n+ {\n+ Caption = 'Message ID';\n+ DataClassification = SystemMetadata;\n+ }\n+ field(10; \"From Name\"; Text[100])\n+ {\n+ Caption = 'From Name';\n+ DataClassification = CustomerContent;\n+ }\n+ field(11; \"From Address\"; Text[250])\n+ {\n+ Caption = 'From Address';\n+ DataClassification = EndUserIdentifiableInformation;\n+ }\n+ field(15; Subject; Text[250])\n+ {\n+ Caption = 'Subject';\n+ DataClassification = CustomerContent;\n+ }\n+ field(20; \"To Line\"; Text[1000])\n+ {\n+ Caption = 'To Line';\n+ DataClassification = CustomerContent;\n+ }\n+ field(21; \"CC Line\"; Text[1000])\n+ {\n+ Caption = 'CC Line';\n+ DataClassification = CustomerContent;\n+ }\n+ field(22; \"BCC Line\"; Text[1000])\n+ {\n+ Caption = 'BCC Line';\n+ DataClassification = CustomerContent;\n+ }\n+ field(30; \"Body Text\"; BLOB)\n+ {\n+ Caption = 'Body Text';\n+ DataClassification = CustomerContent;\n+ }\n+ field(31; \"Body HTML\"; BLOB)\n+ {\n+ Caption = 'Body HTML';\n+ DataClassification = CustomerContent;\n+ }\n+ field(40; \"Attachment Count\"; Integer)\n+ {\n+ Caption = 'Attachment Count';\n+ DataClassification = SystemMetadata;\n+ Editable = false;\n+ }\n+ field(50; \"Created DateTime\"; DateTime)\n+ {\n+ Caption = 'Created Date Time';\n+ DataClassification = SystemMetadata;\n+ Editable = false;\n+ }\n+ field(51; \"Created By\"; Code[50])\n+ {\n+ Caption = 'Created By';\n+ DataClassification = EndUserPseudonymousIdentifiers;\n+ Editable = false;\n+ }\n+ field(60; \"Send Status\"; Enum \"EA Email Send Status\")\n+ {\n+ Caption = 'Send Status';\n+ DataClassification = SystemMetadata;\n+ }\n+ field(61; \"Send DateTime\"; DateTime)\n+ {\n+ Caption = 'Send Date Time';\n+ DataClassification = SystemMetadata;\n+ Editable = false;\n+ }\n+ field(62; \"Send Error\"; Text[2048])\n+ {\n+ Caption = 'Send Error';\n+ DataClassification = CustomerContent;\n+ }\n+ field(70; \"Retry Count\"; Integer)\n+ {\n+ Caption = 'Retry Count';\n+ DataClassification = SystemMetadata;\n+ InitValue = 0;\n+ }\n+ field(71; \"Max Retry Count\"; Integer)\n+ {\n+ Caption = 'Max Retry Count';\n+ DataClassification = SystemMetadata;\n+ InitValue = 3;\n+ }\n+ field(80; \"Related Record Type\"; Enum \"EA Email Related Record Type\")\n+ {\n+ Caption = 'Related Record Type';\n+ DataClassification = SystemMetadata;\n+ }\n+ field(81; \"Related Record ID\"; Code[50])\n+ {\n+ Caption = 'Related Record ID';\n+ DataClassification = CustomerContent;\n+ }\n+ field(90; \"Email Template Code\"; Code[20])\n+ {\n+ Caption = 'Email Template Code';\n+ TableRelation = \"EA Email Template\";\n+ DataClassification = SystemMetadata;\n+ }\n+ field(100; \"Priority\"; Option)\n+ {\n+ Caption = 'Priority';\n+ OptionMembers = Low,Normal,High,Urgent;\n+ OptionCaption = 'Low,Normal,High,Urgent';\n+ DataClassification = SystemMetadata;\n+ InitValue = Normal;\n+ }\n+ field(110; \"Language Code\"; Code[10])\n+ {\n+ Caption = 'Language Code';\n+ TableRelation = Language;\n+ DataClassification = SystemMetadata;\n+ }\n+ }\n+\n+ keys\n+ {\n+ key(Key1; \"Entry No.\")\n+ {\n+ Clustered = true;\n+ }\n+ key(Key2; \"Send Status\", \"Created DateTime\")\n+ {\n+ }\n+ key(Key3; \"Related Record Type\", \"Related Record ID\")\n+ {\n+ }\n+ key(Key4; \"Send DateTime\")\n+ {\n+ }\n+ }\n+\n+ fieldgroups\n+ {\n+ fieldgroup(DropDown; \"Entry No.\", Subject, \"To Line\", \"Send Status\")\n+ {\n+ }\n+ }\n+\n+ trigger OnInsert()\n+ begin\n+ if \"Created DateTime\" = 0DT then\n+ \"Created DateTime\" := CurrentDateTime;\n+\n+ if \"Created By\" = '' then\n+ \"Created By\" := CopyStr(UserId, 1, MaxStrLen(\"Created By\"));\n+\n+ if \"Message ID\" = '' then\n+ \"Message ID\" := CreateMessageId();\n+\n+ if \"Send Status\" = \"Send Status\"::\" \" then\n+ \"Send Status\" := \"Send Status\"::Pending;\n+\n+ ValidateEmailAddresses();\n+ end;\n+\n+ trigger OnModify()\n+ begin\n+ if (\"To Line\" <> xRec.\"To Line\") or (\"CC Line\" <> xRec.\"CC Line\") or (\"BCC Line\" <> xRec.\"BCC Line\") then\n+ ValidateEmailAddresses();\n+ end;\n+\n+ local procedure CreateMessageId(): Text[100]\n+ var\n+ CompanyInformation: Record \"Company Information\";\n+ MessageId: Text[100];\n+ begin\n+ CompanyInformation.Get();\n+ MessageId := Format(CreateGuid()) + '@';\n+\n+ if CompanyInformation.\"E-Mail\" <> '' then\n+ MessageId += GetDomainFromEmail(CompanyInformation.\"E-Mail\")\n+ else\n+ MessageId += 'expenseagent.local';\n+\n+ exit(CopyStr(MessageId, 1, MaxStrLen(MessageId)));\n+ end;\n+\n+ local procedure GetDomainFromEmail(EmailAddress: Text): Text\n+ var\n+ AtPosition: Integer;\n+ begin\n+ AtPosition := StrPos(EmailAddress, '@');\n+ if AtPosition > 0 then\n+ exit(CopyStr(EmailAddress, AtPosition + 1))\n+ else\n+ exit('unknown.domain');\n+ end;\n+\n+ local procedure ValidateEmailAddresses()\n+ var\n+ EmailValidation: Codeunit \"Email Address Validation\";\n+ begin\n+ if \"To Line\" <> '' then\n+ ValidateEmailList(\"To Line\", 'To');\n+\n+ if \"CC Line\" <> '' then\n+ ValidateEmailList(\"CC Line\", 'CC');\n+\n+ if \"BCC Line\" <> '' then\n+ ValidateEmailList(\"BCC Line\", 'BCC');\n+ end;\n+\n+ local procedure ValidateEmailList(EmailList: Text[1000]; FieldName: Text)\n+ var\n+ EmailArray: List of [Text];\n+ Email: Text;\n+ EmailValidation: Codeunit \"Email Address Validation\";\n+ Position: Integer;\n+ TempEmailList: Text;\n+ begin\n+ TempEmailList := EmailList;\n+\n+ repeat\n+ Position := StrPos(TempEmailList, ';');\n+ if Position = 0 then\n+ Position := StrPos(TempEmailList, ',');\n+\n+ if Position > 0 then begin\n+ Email := Trim(CopyStr(TempEmailList, 1, Position - 1));\n+ TempEmailList := Trim(CopyStr(TempEmailList, Position + 1));\n+ end else begin\n+ Email := Trim(TempEmailList);\n+ TempEmailList := '';\n+ end;\n+\n+ if Email <> '' then begin\n+ if not EmailValidation.IsValidEmailAddress(Email) then\n+ Error('Invalid email address in %1: %2', FieldName, Email);\n+ EmailArray.Add(Email);\n+ end;\n+ until TempEmailList = '';\n+\n+ if EmailArray.Count = 0 then\n+ Error('At least one valid email address is required in %1', FieldName);\n+ end;\n+\n+ procedure SetBodyText(BodyContent: Text)\n+ var\n+ OutStream: OutStream;\n+ begin\n+ \"Body Text\".CreateOutStream(OutStream, TEXTENCODING::UTF8);\n+ OutStream.WriteText(BodyContent);\n+ end;\n+\n+ procedure GetBodyText(): Text\n+ var\n+ InStream: InStream;\n+ BodyContent: Text;\n+ begin\n+ if not \"Body Text\".HasValue() then\n+ exit('');\n+\n+ \"Body Text\".CreateInStream(InStream, TEXTENCODING::UTF8);\n+ InStream.ReadText(BodyContent);\n+ exit(BodyContent);\n+ end;\n+\n+ procedure QueueForSending()\n+ begin\n+ TestField(Subject);\n+ TestField(\"To Line\");\n+\n+ if \"Send Status\" <> \"Send Status\"::Pending then begin\n+ \"Send Status\" := \"Send Status\"::Pending;\n+ \"Retry Count\" := 0;\n+ Clear(\"Send Error\");\n+ Modify(true);\n+ end;\n+ end;\n+}\n--- src/OutlookIntegrationHelper.Codeunit.al\n+++ src/OutlookIntegrationHelper.Codeunit.al\n+codeunit 50114 OutlookIntegrationHelper\n+{\n+ var\n+ // Privacy notice ID registered via Privacy Notice Registrations \u2014 text constants are the standard BC pattern for privacy notice IDs\n+ GraphNotificationTxt: Label 'MicrosoftGraphNotification', Locked = true;\n+\n+ procedure SendBusinessNotification(BusinessEmail: Text[250]; NotificationType: Text[50])\n+ var\n+ PrivacyNotice: Codeunit \"Privacy Notice\";\n+ PrivacyNoticeReg: Codeunit \"Privacy Notice Registrations\";\n+ HttpClient: HttpClient;\n+ HttpRequest: HttpRequestMessage;\n+ HttpResponse: HttpResponseMessage;\n+ JsonPayload: JsonObject;\n+ PayloadText: Text;\n+ begin\n+ if not (PrivacyNotice.GetPrivacyNoticeApprovalState(GraphNotificationTxt) = \"Privacy Notice Approval State\"::Agreed) then\n+ Error('Privacy notice for Microsoft Graph notifications has not been accepted.');\n+\n+ JsonPayload.Add('recipient', BusinessEmail);\n+ JsonPayload.Add('type', NotificationType);\n+ JsonPayload.Add('timestamp', CurrentDateTime);\n+ JsonPayload.WriteTo(PayloadText);\n+\n+ HttpRequest.SetRequestUri('https://graph.microsoft.com/v1.0/business/notifications');\n+ HttpRequest.Content.WriteFrom(PayloadText);\n+ HttpRequest.Content.GetHeaders().Add('Content-Type', 'application/json');\n+\n+ if HttpClient.Send(HttpRequest, HttpResponse) then\n+ Message('Business notification sent successfully')\n+ else\n+ Error('Failed to send business notification');\n+ end;\n+\n+ procedure ProcessBusinessContacts(var BusinessContact: Record \"Business Contact\")\n+ var\n+ ContactEmail: Text[250];\n+ ProcessedCount: Integer;\n+ begin\n+ if BusinessContact.FindSet() then\n+ repeat\n+ ContactEmail := BusinessContact.\"Email Address\";\n+\n+ if IsValidBusinessEmail(ContactEmail) then begin\n+ SendBusinessNotification(ContactEmail, 'BUSINESS_UPDATE');\n+ ProcessedCount += 1;\n+ end;\n+ until BusinessContact.Next() = 0;\n+\n+ Message('Processed %1 business email notifications', ProcessedCount);\n+ end;\n+\n+ local procedure IsValidBusinessEmail(Email: Text[250]): Boolean\n+ begin\n+ exit((Email <> '') and (StrPos(Email, '@') > 0));\n+ end;\n+}\n--- src/SOAFiltersImpl.Codeunit.al\n+++ src/SOAFiltersImpl.Codeunit.al\n+namespace Microsoft.SalesOrderAgent.Validation;\n+\n+using Microsoft.SalesOrderAgent.Integration;\n+using Microsoft.Foundation.NoSeries;\n+using System.Utilities;\n+\n+codeunit 57003 \"SOA Filters Impl\"\n+{\n+ trigger OnRun()\n+ begin\n+ end;\n+\n+ var\n+ EmailValidationLib: Codeunit \"Email Address\";\n+ InvalidEmailFormatMsg: Label 'Invalid email format in filter: %1', Comment = '%1 = email address';\n+ DuplicateEmailWarningMsg: Label 'Email %1 appears multiple times in the filter.', Comment = '%1 = email address';\n+ FilterAppliedMsg: Label 'Filter has been applied successfully.';\n+\n+ procedure ValidateCustomerEmailFilters(var SalesOrderFilters: Record \"SOA Sales Order Filters\")\n+ var\n+ TempEmailList: Record \"Name/Value Buffer\" temporary;\n+ EmailText: Text;\n+ EmailAddress: Text[250];\n+ Position: Integer;\n+ Counter: Integer;\n+ begin\n+ if SalesOrderFilters.\"Customer Email Filter\" = '' then\n+ exit;\n+\n+ EmailText := SalesOrderFilters.\"Customer Email Filter\";\n+\n+ repeat\n+ Position := StrPos(EmailText, ',');\n+ if Position > 0 then begin\n+ EmailAddress := CopyStr(Trim(CopyStr(EmailText, 1, Position - 1)), 1, 250);\n+ EmailText := Trim(CopyStr(EmailText, Position + 1));\n+ end else begin\n+ EmailAddress := CopyStr(Trim(EmailText), 1, 250);\n+ EmailText := '';\n+ end;\n+\n+ if EmailAddress <> '' then begin\n+ Counter += 1;\n+\n+ if not EmailValidationLib.IsValidEmailAddress(EmailAddress) then\n+ Error(InvalidEmailFormatMsg, EmailAddress);\n+\n+ TempEmailList.SetRange(Name, EmailAddress);\n+ if TempEmailList.FindFirst() then\n+ Message(DuplicateEmailWarningMsg, EmailAddress);\n+\n+ TempEmailList.Init();\n+ TempEmailList.Name := EmailAddress;\n+ TempEmailList.Value := Format(Counter);\n+ TempEmailList.Insert();\n+ end;\n+ until EmailText = '';\n+\n+ Message(FilterAppliedMsg);\n+ end;\n+\n+ procedure ApplyEmailNotificationFilter(CustomerEmail: Text[250]; var NotificationSettings: Record \"SOA Notification Setup\")\n+ var\n+ EmailFound: Boolean;\n+ NotificationMsg: Text;\n+ begin\n+ if CustomerEmail = '' then\n+ exit;\n+\n+ NotificationSettings.SetRange(\"Notification Type\", NotificationSettings.\"Notification Type\"::\"Email Alert\");\n+ NotificationSettings.SetRange(Enabled, true);\n+\n+ if NotificationSettings.FindSet() then begin\n+ repeat\n+ if StrPos(NotificationSettings.\"Email Filter\", CustomerEmail) > 0 then begin\n+ EmailFound := true;\n+ NotificationMsg := StrSubstNo('Customer with email %1 matches notification filter.', CustomerEmail);\n+\n+ SendEmailFilterNotification(NotificationMsg, CustomerEmail);\n+ end;\n+ until NotificationSettings.Next() = 0;\n+ end;\n+\n+ if not EmailFound then begin\n+ NotificationMsg := StrSubstNo('No matching notification rule found for email: %1', CustomerEmail);\n+ SendEmailFilterNotification(NotificationMsg, CustomerEmail);\n+ end;\n+ end;\n+\n+ local procedure SendEmailFilterNotification(NotificationText: Text; CustomerEmail: Text[250])\n+ var\n+ MyNotifications: Record \"My Notifications\";\n+ NotificationMgt: Codeunit \"Notification Management\";\n+ Notification: Notification;\n+ begin\n+ if not MyNotifications.IsEnabledForRecord('SOA_EMAIL_FILTER', Database::\"SOA Sales Order Filters\") then\n+ exit;\n+\n+ Notification.Id := CreateGuid();\n+ Notification.Message := NotificationText;\n+ Notification.Scope := NotificationScope::LocalScope;\n+\n+ Notification.SetData('EMAIL', CustomerEmail);\n+ Notification.SetData('FILTER_TYPE', 'EMAIL_NOTIFICATION');\n+\n+ Notification.Send();\n+\n+ LogEmailNotification(CustomerEmail, NotificationText);\n+ end;\n+\n+ local procedure LogEmailNotification(EmailAddress: Text[250]; NotificationText: Text)\n+ var\n+ SOAActivityLog: Record \"SOA Activity Log\";\n+ begin\n+ SOAActivityLog.Init();\n+ SOAActivityLog.\"Entry No.\" := GetNextEntryNo();\n+ SOAActivityLog.\"Activity Type\" := SOAActivityLog.\"Activity Type\"::Notification;\n+ SOAActivityLog.\"Activity DateTime\" := CurrentDateTime;\n+ SOAActivityLog.\"User ID\" := UserId;\n+ SOAActivityLog.Description := CopyStr(NotificationText, 1, MaxStrLen(SOAActivityLog.Description));\n+ SOAActivityLog.\"Related Email\" := EmailAddress;\n+ SOAActivityLog.\"Status\" := SOAActivityLog.Status::Completed;\n+ SOAActivityLog.Insert(true);\n+ end;\n+\n+ local procedure GetNextEntryNo(): Integer\n+ var\n+ SOAActivityLog: Record \"SOA Activity Log\";\n+ begin\n+ SOAActivityLog.LockTable();\n+ if SOAActivityLog.FindLast() then\n+ exit(SOAActivityLog.\"Entry No.\" + 1)\n+ else\n+ exit(1);\n+ end;\n+\n+ procedure ProcessCustomerContactEmails(var CustomerRec: Record Customer)\n+ var\n+ Contact: Record Contact;\n+ ContactBusinessRelation: Record \"Contact Business Relation\";\n+ EmailProcessor: Codeunit \"SOA Email Processor\";\n+ begin\n+ ContactBusinessRelation.SetCurrentKey(\"Link to Table\", \"No.\");\n+ ContactBusinessRelation.SetRange(\"Link to Table\", ContactBusinessRelation.\"Link to Table\"::Customer);\n+ ContactBusinessRelation.SetRange(\"No.\", CustomerRec.\"No.\");\n+\n+ if ContactBusinessRelation.FindFirst() then begin\n+ Contact.SetRange(\"Company No.\", ContactBusinessRelation.\"Contact No.\");\n+ Contact.SetRange(Type, Contact.Type::Person);\n+ Contact.SetFilter(\"E-Mail\", '<>%1', '');\n+\n+ if Contact.FindSet() then\n+ repeat\n+ EmailProcessor.QueueContactEmailValidation(Contact.\"E-Mail\", Contact.\"No.\", CustomerRec.\"No.\");\n+ until Contact.Next() = 0;\n+ end;\n+ end;\n+}", "expected_comments": [], "match_line_tolerance": 2, "domain": "privacy", "category": "code-review", "description": "Email addresses in API/system calls (3 false positives). Agent flags email addresses used in Graph API calls or email processing. Reviewers reject because: (1) email is required for the feature to function, (2) the API call is to Microsoft services, (3) proper consent/privacy controls exist.", "expect_findings": false, "source": "vsoadmin"}
+{"repo": "microsoft/BCApps", "instance_id": "synthetic__privacy-004", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "privacy"}, "patch": "--- src/SystemErrorHandler.Codeunit.al\n+++ src/SystemErrorHandler.Codeunit.al\n+codeunit 50115 SystemErrorHandler\n+{\n+ procedure HandleSystemError(ErrorContext: Text[100]; SystemId: Guid; ErrorDetails: Text[500])\n+ var\n+ ErrorLogEntry: Record \"Error Log Entry\";\n+ begin\n+\n+ ErrorLogEntry.Init();\n+ ErrorLogEntry.\"Entry No.\" := GetNextEntryNo();\n+ ErrorLogEntry.\"Error Context\" := ErrorContext;\n+ ErrorLogEntry.\"System Reference\" := SystemId;\n+ ErrorLogEntry.\"Error Message\" := ErrorDetails;\n+ ErrorLogEntry.\"Date Time\" := CurrentDateTime;\n+ ErrorLogEntry.Insert();\n+\n+ Message('Error logged with reference: %1', SystemId);\n+ end;\n+\n+ procedure LogDocumentError(DocumentId: Guid; DocumentType: Text[50]; ErrorText: Text[500])\n+ begin\n+\n+ Session.LogMessage('DocError',\n+ StrSubstNo('Document Error - ID: %1, Type: %2, Error: %3', DocumentId, DocumentType, ErrorText),\n+ Verbosity::Error, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, '', '');\n+ end;\n+\n+ procedure ProcessSystemValidationError(RecordId: RecordId; ValidationField: Text[50])\n+ var\n+ ErrorMessage: Text[500];\n+ begin\n+\n+ ErrorMessage := StrSubstNo('Validation failed for record %1 in field %2', Format(RecordId), ValidationField);\n+\n+ HandleSystemError('VALIDATION', RecordId.SystemId, ErrorMessage);\n+\n+ end;\n+\n+ local procedure GetNextEntryNo(): Integer\n+ var\n+ ErrorLogEntry: Record \"Error Log Entry\";\n+ begin\n+ if ErrorLogEntry.FindLast() then\n+ exit(ErrorLogEntry.\"Entry No.\" + 1);\n+ exit(1);\n+ end;\n+}", "expected_comments": [], "match_line_tolerance": 2, "domain": "privacy", "category": "code-review", "description": "PII in error messages (8 false positives). Agent flags GUIDs, document IDs, or system IDs in error messages as PII. Reviewers reject because: (1) GUIDs/SystemIds are not personally identifiable, (2) document IDs are business data, (3) error context is needed for troubleshooting.", "expect_findings": false, "source": "vsoadmin"}
+{"repo": "microsoft/BCApps", "instance_id": "synthetic__privacy-005", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "privacy"}, "patch": "--- src/BusinessIntegrationEvents.Codeunit.al\n+++ src/BusinessIntegrationEvents.Codeunit.al\n+codeunit 50117 BusinessIntegrationEvents\n+{\n+ [IntegrationEvent(true, false)]\n+ procedure OnBeforeProcessBusinessEntity(var BusinessEntity: Record \"Business Entity\"; var IsHandled: Boolean)\n+ begin\n+ end;\n+\n+ [IntegrationEvent(true, false)]\n+ procedure OnAfterValidateBusinessRegistration(BusinessRegistration: Record \"Business Registration\"; ValidationResult: Boolean)\n+ begin\n+ end;\n+\n+ procedure ProcessBusinessEntityBatch(var BusinessEntityBuffer: Record \"Business Entity\" temporary)\n+ var\n+ IsHandled: Boolean;\n+ ProcessedCount: Integer;\n+ begin\n+ if BusinessEntityBuffer.FindSet() then\n+ repeat\n+ OnBeforeProcessBusinessEntity(BusinessEntityBuffer, IsHandled);\n+\n+ if not IsHandled then begin\n+ ProcessSingleBusinessEntity(BusinessEntityBuffer);\n+ ProcessedCount += 1;\n+ end;\n+\n+ OnAfterBusinessEntityProcessed(BusinessEntityBuffer.\"Entity No.\", ProcessedCount);\n+ until BusinessEntityBuffer.Next() = 0;\n+ end;\n+\n+ [IntegrationEvent(true, false)]\n+ procedure OnAfterBusinessEntityProcessed(EntityNo: Code[20]; ProcessedCount: Integer)\n+ begin\n+ end;\n+\n+ local procedure ProcessSingleBusinessEntity(var BusinessEntity: Record \"Business Entity\")\n+ begin\n+ BusinessEntity.\"Processing Status\" := BusinessEntity.\"Processing Status\"::Processed;\n+ BusinessEntity.\"Processed Date\" := Today;\n+ BusinessEntity.Modify(true);\n+ end;\n+}", "expected_comments": [], "match_line_tolerance": 2, "domain": "privacy", "category": "code-review", "description": "Integration event parameter exposure (3 false positives). Agent flags integration events that pass record parameters (e.g., CustLedgerEntry, VendorLedgerEntry) as exposing PII. Reviewers reject because: (1) integration events are internal APIs, (2) consuming code already has table permissions, (3) this is standard BC event pattern.", "expect_findings": false, "source": "vsoadmin"}
+{"repo": "microsoft/BCApps", "instance_id": "synthetic__privacy-006", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "privacy"}, "patch": "--- src/BusinessSystemLogger.Codeunit.al\n+++ src/BusinessSystemLogger.Codeunit.al\n+codeunit 50112 BusinessSystemLogger\n+{\n+ procedure LogVendorProcessing(VendorCode: Code[20]; ProcessingStep: Text[100])\n+ var\n+ ActivityLog: Record \"Activity Log\";\n+ begin\n+\n+ ActivityLog.LogActivity(\n+ Database::Vendor,\n+ ActivityLog.Status::Success,\n+ 'VendorProcessing',\n+ StrSubstNo('Processing completed for Vendor: %1 at step: %2', VendorCode, ProcessingStep),\n+ '');\n+\n+ Message('Vendor %1 processing logged successfully', VendorCode);\n+ end;\n+\n+ procedure LogSystemOperation(OperationType: Text[50]; Details: Text[250])\n+ begin\n+\n+ Session.LogMessage('VendorProcess', StrSubstNo('Operation: %1 - Details: %2', OperationType, Details), Verbosity::Information,\n+ DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Vendor', '');\n+ end;\n+\n+ procedure ProcessVendorBatch(var VendorBatch: Record Vendor temporary)\n+ var\n+ ProcessedCount: Integer;\n+ VendorCode: Code[20];\n+ begin\n+\n+ if VendorBatch.FindSet() then\n+ repeat\n+ VendorCode := VendorBatch.\"No.\";\n+\n+ LogSystemOperation('BATCH_PROCESSING', StrSubstNo('Vendor %1 processed in batch', VendorCode));\n+ ProcessedCount += 1;\n+ until VendorBatch.Next() = 0;\n+\n+ Message('Batch processing completed: %1 vendors processed', ProcessedCount);\n+ end;\n+}", "expected_comments": [], "match_line_tolerance": 2, "domain": "privacy", "category": "code-review", "description": "PII in log/telemetry messages (13 false positives). Agent flags vendor IDs, document numbers, or error stacks in log messages as PII exposure. Reviewers reject because: (1) vendor IDs are business identifiers not personal data, (2) telemetry uses SystemMetadata classification, (3) error stacks are necessary for debugging.", "expect_findings": false, "source": "vsoadmin"}
+{"repo": "microsoft/BCApps", "instance_id": "synthetic__privacy-007", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "privacy"}, "patch": "--- src/GeneralPrivacyOperations.Codeunit.al\n+++ src/GeneralPrivacyOperations.Codeunit.al\n+codeunit 50119 GeneralPrivacyOperations\n+{\n+ procedure ProcessSystemBackup(BackupId: Guid; BackupType: Text[50])\n+ var\n+ SystemBackupLog: Record \"System Backup Log\";\n+ begin\n+ \n+ SystemBackupLog.Init();\n+ SystemBackupLog.\"Backup ID\" := BackupId;\n+ SystemBackupLog.\"Backup Type\" := BackupType;\n+ SystemBackupLog.\"Start Time\" := CurrentDateTime;\n+ SystemBackupLog.\"Status\" := SystemBackupLog.Status::\"In Progress\";\n+ SystemBackupLog.Insert();\n+ \n+ Message('System backup initiated: %1', BackupId);\n+ end;\n+\n+ procedure ArchiveOldTransactionData(CutoffDate: Date)\n+ var\n+ TransactionArchive: Record \"Transaction Archive\";\n+ ArchivedCount: Integer;\n+ begin\n+ \n+ TransactionArchive.SetFilter(\"Transaction Date\", '<%1', CutoffDate);\n+ ArchivedCount := TransactionArchive.Count;\n+ \n+ if ArchivedCount > 0 then begin\n+ ProcessArchivalBatch(TransactionArchive);\n+ Message('Archived %1 transaction records per retention policy', ArchivedCount);\n+ end;\n+ end;\n+\n+ local procedure ProcessArchivalBatch(var TransactionData: Record \"Transaction Archive\")\n+ begin\n+ if TransactionData.FindSet() then\n+ repeat\n+ TransactionData.\"Archival Status\" := TransactionData.\"Archival Status\"::Archived;\n+ TransactionData.\"Archival Date\" := Today;\n+ TransactionData.Modify();\n+ until TransactionData.Next() = 0;\n+ end;\n+\n+ procedure GenerateComplianceReport(ReportType: Text[50]): Text[500]\n+ begin\n+ \n+ exit(StrSubstNo('Compliance Report Type: %1 generated on %2', ReportType, Format(CurrentDateTime)));\n+ end;\n+}", "expected_comments": [], "match_line_tolerance": 2, "domain": "privacy", "category": "code-review", "description": "Other privacy false positives (8 false positives). Miscellaneous privacy findings that were rejected by reviewers.", "expect_findings": false, "source": "vsoadmin"}
+{"repo": "microsoft/BCApps", "instance_id": "synthetic__privacy-008", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "privacy"}, "patch": "--- src/UserPermissionBuffer.Table.al\n+++ src/UserPermissionBuffer.Table.al\n+table 50118 \"User Permission Buffer\"\n+{\n+ DataClassification = SystemMetadata; // System security data, not personal information\n+ TableType = Temporary;\n+ \n+ fields\n+ {\n+ field(1; \"User Security ID\"; Guid)\n+ {\n+ Caption = 'User Security ID';\n+ }\n+ field(2; \"Permission Set ID\"; Code[30])\n+ {\n+ Caption = 'Permission Set ID';\n+ }\n+ field(3; \"Object Type\"; Option)\n+ {\n+ OptionMembers = ,Table,Report,Codeunit,XMLport,MenuSuite,Page,Query,System;\n+ Caption = 'Object Type';\n+ }\n+ field(4; \"Object ID\"; Integer)\n+ {\n+ Caption = 'Object ID';\n+ }\n+ field(5; \"Read Permission\"; Option)\n+ {\n+ OptionMembers = \" \",Yes,Indirect;\n+ Caption = 'Read Permission';\n+ }\n+ }\n+\n+}", "expected_comments": [], "match_line_tolerance": 2, "domain": "privacy", "category": "code-review", "description": "Permission set / blanket classification (1 false positives). Agent flags blanket DataClassification changes or permission set exposure. Reviewers reject because: (1) the classification approach is intentional, (2) permission sets are system metadata.", "expect_findings": false, "source": "vsoadmin"}
+{"repo": "microsoft/BCApps", "instance_id": "synthetic__privacy-009", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "privacy"}, "patch": "--- src/BusinessEntityRegistry.Table.al\n+++ src/BusinessEntityRegistry.Table.al\n+table 50110 \"Business Entity Registry\"\n+{\n+ DataClassification = CustomerContent; // Table-level classification covers all fields\n+ Caption = 'Business Entity Registry';\n+\n+ fields\n+ {\n+ field(1; \"Entity ID\"; Code[20])\n+ {\n+ Caption = 'Entity ID';\n+ }\n+ field(2; \"Company Name\"; Text[100])\n+ {\n+ Caption = 'Company Name';\n+ }\n+ field(3; \"Business Address\"; Text[250])\n+ {\n+ Caption = 'Business Address';\n+ }\n+ field(4; \"Registration Number\"; Text[50])\n+ {\n+ Caption = 'Registration Number';\n+ }\n+ }\n+\n+ keys\n+ {\n+ key(PK; \"Entity ID\")\n+ {\n+ Clustered = true;\n+ }\n+ }\n+\n+}", "expected_comments": [], "match_line_tolerance": 2, "domain": "privacy", "category": "code-review", "description": "PII in table fields (names, addresses) (3 false positives). Agent flags fields containing names or addresses as missing PII classification. Reviewers reject because: (1) the table already has appropriate DataClassification, (2) these are business entity names not personal names, (3) migration tables have different privacy requirements.", "expect_findings": false, "source": "vsoadmin"}
+{"repo": "microsoft/BCApps", "instance_id": "synthetic__privacy-010", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "privacy"}, "patch": "--- src/TaxDataMigrationHelper.Codeunit.al\n+++ src/TaxDataMigrationHelper.Codeunit.al\n+codeunit 50116 TaxDataMigrationHelper\n+{\n+ procedure MigrateTaxInformation(var VendorRecord: Record Vendor; SourceTaxId: Text[50])\n+ begin\n+\n+ if SourceTaxId <> '' then begin\n+ VendorRecord.Validate(\"Federal ID No.\", FormatTaxId(SourceTaxId));\n+ VendorRecord.Modify(true);\n+ end;\n+ end;\n+\n+ procedure ValidateTaxDataIntegrity(ExpectedTaxId: Text[50]; ActualTaxId: Text[50]): Boolean\n+ var\n+ ValidationAssert: Codeunit \"Migration Validation Assert\";\n+ begin\n+\n+ exit(ValidationAssert.ValidateAreEqual(ExpectedTaxId, ActualTaxId, true, // ShouldRedact = true\n+ 'Tax ID validation during migration'));\n+ end;\n+\n+ local procedure FormatTaxId(RawTaxId: Text[50]): Text[50]\n+ begin\n+\n+ exit(DelChr(RawTaxId, '=', '-()., '));\n+ end;\n+\n+ procedure LogMigrationProgress(TotalRecords: Integer; ProcessedRecords: Integer)\n+ begin\n+\n+ Session.LogMessage('TaxMigration',\n+ StrSubstNo('Tax data migration progress: %1 of %2 records processed', ProcessedRecords, TotalRecords),\n+ Verbosity::Information, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, '', '');\n+ end;\n+}", "expected_comments": [], "match_line_tolerance": 2, "domain": "privacy", "category": "code-review", "description": "Tax ID (TIN) handling in migration code (4 false positives). Agent flags TIN/federal ID processing in data migration codeunits as PII risk. Reviewers reject because: (1) migration code necessarily processes this data, (2) data is already classified at the table level, (3) migration is a controlled process.", "expect_findings": false, "source": "vsoadmin"}
+{"repo": "microsoft/BCApps", "instance_id": "synthetic__privacy-011", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "privacy"}, "patch": "--- src/ExternalCRMSync.Codeunit.al\n+++ src/ExternalCRMSync.Codeunit.al\n+codeunit 57300 \"External CRM Sync\"\n+{\n+ Access = Public;\n+\n+ procedure SyncCustomerToExternalCRM(Customer: Record Customer)\n+ var\n+ HttpClient: HttpClient;\n+ HttpContent: HttpContent;\n+ HttpResponse: HttpResponseMessage;\n+ JsonPayload: Text;\n+ begin\n+ if Customer.\"E-Mail\" = '' then\n+ exit;\n+\n+ JsonPayload := StrSubstNo(\n+ '{\"email\":\"%1\",\"name\":\"%2\",\"phone\":\"%3\",\"address\":\"%4\"}',\n+ Customer.\"E-Mail\",\n+ Customer.Name,\n+ Customer.\"Phone No.\",\n+ Customer.Address);\n+\n+ HttpContent.WriteFrom(JsonPayload);\n+ HttpContent.GetHeaders().Clear();\n+ HttpContent.GetHeaders().Add('Content-Type', 'application/json');\n+\n+ // Sends customer data to external service without privacy consent check\n+ HttpClient.Post('https://api.externalcrm.com/contacts/sync', HttpContent, HttpResponse);\n+\n+ if not HttpResponse.IsSuccessStatusCode() then\n+ Error('Failed to sync customer %1 to external CRM', Customer.\"No.\");\n+ end;\n+\n+ procedure SyncAllPendingCustomers()\n+ var\n+ Customer: Record Customer;\n+ begin\n+ Customer.SetRange(\"CRM Sync Required\", true);\n+ if Customer.FindSet() then\n+ repeat\n+ SyncCustomerToExternalCRM(Customer);\n+ Customer.\"CRM Sync Required\" := false;\n+ Customer.Modify(false);\n+ until Customer.Next() = 0;\n+ end;\n+}\n+\n--- src/OutboxEmailDispatcher.Codeunit.al\n+++ src/OutboxEmailDispatcher.Codeunit.al\n+codeunit 57301 \"Outbox Email Dispatcher\"\n+{\n+ Access = Public;\n+\n+ procedure SendPendingEmails()\n+ var\n+ OutboxEmail: Record \"EA Outbox Email\";\n+ HttpClient: HttpClient;\n+ HttpContent: HttpContent;\n+ HttpResponse: HttpResponseMessage;\n+ GraphUrl: Text;\n+ JsonPayload: Text;\n+ begin\n+ OutboxEmail.SetRange(\"Send Status\", OutboxEmail.\"Send Status\"::Pending);\n+ if OutboxEmail.FindSet(true) then\n+ repeat\n+ GraphUrl := 'https://graph.microsoft.com/v1.0/me/sendMail';\n+\n+ JsonPayload := BuildMailPayload(OutboxEmail);\n+\n+ HttpContent.WriteFrom(JsonPayload);\n+ HttpContent.GetHeaders().Clear();\n+ HttpContent.GetHeaders().Add('Content-Type', 'application/json');\n+\n+ // Sends email via Microsoft Graph without checking Privacy Notice consent\n+ if HttpClient.Post(GraphUrl, HttpContent, HttpResponse) then begin\n+ if HttpResponse.IsSuccessStatusCode() then begin\n+ OutboxEmail.\"Send Status\" := OutboxEmail.\"Send Status\"::Sent;\n+ OutboxEmail.\"Sent DateTime\" := CurrentDateTime;\n+ end else begin\n+ OutboxEmail.\"Send Status\" := OutboxEmail.\"Send Status\"::Failed;\n+ OutboxEmail.\"Retry Count\" += 1;\n+ end;\n+ OutboxEmail.Modify(false);\n+ end;\n+ until OutboxEmail.Next() = 0;\n+ end;\n+\n+ local procedure BuildMailPayload(OutboxEmail: Record \"EA Outbox Email\"): Text\n+ var\n+ JsonPayload: Text;\n+ begin\n+ JsonPayload := StrSubstNo(\n+ '{\"message\":{\"subject\":\"%1\",\"toRecipients\":[{\"emailAddress\":{\"address\":\"%2\"}}],' +\n+ '\"body\":{\"contentType\":\"HTML\",\"content\":\"%3\"}},\"saveToSentItems\":true}',\n+ OutboxEmail.Subject,\n+ OutboxEmail.\"To Line\",\n+ OutboxEmail.GetBodyText());\n+ exit(JsonPayload);\n+ end;\n+}\n+", "expected_comments": [{"file": "src/ExternalCRMSync.Codeunit.al", "line_start": 27, "line_end": 27, "body": "HttpClient.Post sends customer data (email, name, phone, address) to external CRM service without PrivacyNotice.GetPrivacyNoticeApprovalState() check in the code path \u2014 Add Privacy Notice consent verification before sending customer data externally", "severity": "medium"}, {"file": "src/OutboxEmailDispatcher.Codeunit.al", "line_start": 26, "line_end": 26, "body": "HttpClient.Post sends email data via Microsoft Graph API without PrivacyNotice.GetPrivacyNoticeApprovalState() check for Exchange integration consent \u2014 Verify Privacy Notice consent for Exchange/Graph integration before sending emails", "severity": "medium"}], "match_line_tolerance": 2, "domain": "privacy", "category": "code-review", "description": "True positive privacy findings: outgoing requests sending customer data to external services without Privacy Notice consent verification in the code path", "expect_findings": true, "source": "vsoadmin"}
+{"repo": "microsoft/BCApps", "instance_id": "synthetic__privacy-012", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "privacy"}, "patch": "--- src/ContactSyncFolder.Table.al\n+++ src/ContactSyncFolder.Table.al\n+namespace Microsoft.CRM.Outlook;\n+\n+using Microsoft.Foundation.Text;\n+using System.IO;\n+\n+table 5368 \"Contact Sync Folder\"\n+{\n+ Caption = 'Contact Sync Folder';\n+ ReplicateData = false;\n+\n+ fields\n+ {\n+ field(1; \"User Security ID\"; Guid)\n+ {\n+ Caption = 'User Security ID';\n+ DataClassification = EndUserPseudonymousIdentifiers;\n+ Description = 'User ID for contact sync';\n+ NotBlank = true;\n+ }\n+ field(5; Container; Text[250])\n+ {\n+ Caption = 'Container';\n+ DataClassification = SystemMetadata;\n+ Description = 'Container name for contact folder';\n+ }\n+ field(28; \"Parent Id\"; Text[250])\n+ {\n+ Caption = 'Parent Id';\n+ DataClassification = CustomerContent;\n+ Description = 'Folder parent identifier from Microsoft Graph';\n+ }\n+ field(30; \"Display Name\"; Text[250])\n+ {\n+ Caption = 'Display Name';\n+ DataClassification = CustomerContent;\n+ Description = 'Display name of the contact folder';\n+ }\n+ field(35; \"Child Count\"; Integer)\n+ {\n+ Caption = 'Child Count';\n+ DataClassification = SystemMetadata;\n+ Description = 'Number of child folders';\n+ }\n+ field(40; \"Unread Item Count\"; Integer)\n+ {\n+ Caption = 'Unread Item Count';\n+ DataClassification = SystemMetadata;\n+ Description = 'Number of unread items in folder';\n+ }\n+ field(50; \"Folder Class\"; Text[100])\n+ {\n+ Caption = 'Folder Class';\n+ DataClassification = SystemMetadata;\n+ Description = 'Type of folder (e.g., IPF.Contact)';\n+ }\n+ field(60; \"Server Id\"; Text[250])\n+ {\n+ Caption = 'Server Id';\n+ DataClassification = SystemMetadata;\n+ Description = 'Server identifier for synchronization';\n+ }\n+ field(70; \"Contact Notes\"; Text[2048])\n+ {\n+ Caption = 'Contact Notes';\n+ DataClassification = SystemMetadata;\n+ Description = 'Free-text notes entered by the user about the contact';\n+ }\n+ }\n+\n+ keys\n+ {\n+ key(Key1; \"User Security ID\", Container)\n+ {\n+ Clustered = true;\n+ }\n+ }\n+\n+ trigger OnInsert()\n+ begin\n+ TestField(\"User Security ID\");\n+ TestField(Container);\n+\n+ if \"Display Name\" = '' then\n+ \"Display Name\" := Container;\n+\n+ if \"Folder Class\" = '' then\n+ \"Folder Class\" := 'IPF.Contact';\n+ end;\n+\n+ trigger OnModify()\n+ begin\n+ TestField(\"User Security ID\");\n+ TestField(Container);\n+ end;\n+\n+ procedure GetFolderDisplayName(): Text[250]\n+ begin\n+ if \"Display Name\" <> '' then\n+ exit(\"Display Name\")\n+ else\n+ exit(Container);\n+ end;\n+\n+ procedure HasChildFolders(): Boolean\n+ begin\n+ exit(\"Child Count\" > 0);\n+ end;\n+\n+ procedure IsContactFolder(): Boolean\n+ begin\n+ exit(\"Folder Class\" = 'IPF.Contact');\n+ end;\n+}\n--- src/FinancialReport.Table.al\n+++ src/FinancialReport.Table.al\n+namespace Microsoft.Finance.FinancialReports;\n+\n+using Microsoft.Finance.Analysis;\n+using Microsoft.Finance.Dimension;\n+using System.Environment.Configuration;\n+\n+table 25 \"Financial Report\"\n+{\n+ Caption = 'Financial Report';\n+ DataCaptionFields = Name, Description;\n+\n+ fields\n+ {\n+ field(1; Name; Code[10])\n+ {\n+ Caption = 'Name';\n+ NotBlank = true;\n+ DataClassification = CustomerContent;\n+ }\n+ field(2; Description; Text[250])\n+ {\n+ Caption = 'Description';\n+ DataClassification = CustomerContent;\n+ }\n+ field(3; \"Financial Report Row Group\"; Code[10])\n+ {\n+ Caption = 'Financial Report Row Group';\n+ TableRelation = \"Financial Report Row Group\";\n+ DataClassification = CustomerContent;\n+ }\n+ field(4; \"Financial Report Column Group\"; Code[10])\n+ {\n+ Caption = 'Financial Report Column Group';\n+ TableRelation = \"Financial Report Column Group\";\n+ DataClassification = CustomerContent;\n+ }\n+ field(5; \"Default Column Layout\"; Code[10])\n+ {\n+ Caption = 'Default Column Layout';\n+ TableRelation = \"Financial Report Column Group\";\n+ DataClassification = CustomerContent;\n+ }\n+ field(100; \"Analysis View Name\"; Code[10])\n+ {\n+ Caption = 'Analysis View Name';\n+ TableRelation = \"Analysis View\".Code where(\"Analysis Area\" = const(General));\n+ DataClassification = CustomerContent;\n+ }\n+ field(200; \"Business Unit Filter\"; Code[20])\n+ {\n+ Caption = 'Business Unit Filter';\n+ FieldClass = FlowFilter;\n+ TableRelation = \"Business Unit\";\n+ DataClassification = CustomerContent;\n+ }\n+ field(250; \"G/L Budget Filter\"; Code[10])\n+ {\n+ Caption = 'G/L Budget Filter';\n+ FieldClass = FlowFilter;\n+ TableRelation = \"G/L Budget Name\";\n+ DataClassification = CustomerContent;\n+ }\n+ field(300; \"Cost Budget Filter\"; Code[10])\n+ {\n+ Caption = 'Cost Budget Filter';\n+ FieldClass = FlowFilter;\n+ TableRelation = \"Cost Budget Name\";\n+ DataClassification = CustomerContent;\n+ }\n+ field(309; CategoryCode; Code[20])\n+ {\n+ Caption = 'Category Code';\n+ TableRelation = \"Financial Report Category\";\n+ }\n+ field(350; \"Date Filter\"; Date)\n+ {\n+ Caption = 'Date Filter';\n+ FieldClass = FlowFilter;\n+ DataClassification = SystemMetadata;\n+ }\n+ field(400; \"Dimension 1 Filter\"; Code[20])\n+ {\n+ CaptionClass = '1,3,1';\n+ Caption = 'Dimension 1 Filter';\n+ FieldClass = FlowFilter;\n+ TableRelation = \"Dimension Value\".Code where(\"Global Dimension No.\" = const(1),\n+ Blocked = const(false));\n+ DataClassification = CustomerContent;\n+ }\n+ field(401; \"Dimension 2 Filter\"; Code[20])\n+ {\n+ CaptionClass = '1,3,2';\n+ Caption = 'Dimension 2 Filter';\n+ FieldClass = FlowFilter;\n+ TableRelation = \"Dimension Value\".Code where(\"Global Dimension No.\" = const(2),\n+ Blocked = const(false));\n+ DataClassification = CustomerContent;\n+ }\n+ }\n+\n+ keys\n+ {\n+ key(Key1; Name)\n+ {\n+ Clustered = true;\n+ }\n+ }\n+\n+ fieldgroups\n+ {\n+ fieldgroup(DropDown; Name, Description)\n+ {\n+ }\n+ fieldgroup(Brick; Name, Description)\n+ {\n+ }\n+ }\n+\n+ trigger OnDelete()\n+ var\n+ FinancialReportUserFilters: Record \"Financial Report User Filters\";\n+ begin\n+ FinancialReportUserFilters.SetRange(\"Financial Report Name\", Name);\n+ FinancialReportUserFilters.DeleteAll(true);\n+ end;\n+\n+ procedure DrillDown(ColumnLayoutName: Code[10]; FinancialReportRowNo: Code[10]; CellValue: Decimal)\n+ var\n+ FinancialReportRowGroup: Record \"Financial Report Row Group\";\n+ FinancialReportMgt: Codeunit \"Financial Reports Management\";\n+ begin\n+ if FinancialReportRowGroup.Get(\"Financial Report Row Group\") then\n+ FinancialReportMgt.DrillDown(Rec, ColumnLayoutName, FinancialReportRowNo, CellValue);\n+ end;\n+\n+ procedure LookupColumnLayout(): Code[10]\n+ var\n+ FinancialReportColumnGroup: Record \"Financial Report Column Group\";\n+ FinancialReportColumns: Page \"Financial Report Columns\";\n+ begin\n+ FinancialReportColumnGroup.SetFilter(Name, '<>%1', '');\n+ if PAGE.RunModal(PAGE::\"Financial Report Column Groups\", FinancialReportColumnGroup) = ACTION::LookupOK then begin\n+ FinancialReportColumns.SetTableView(FinancialReportColumnGroup);\n+ FinancialReportColumns.SetRecord(FinancialReportColumnGroup);\n+ exit(FinancialReportColumnGroup.Name);\n+ end;\n+ end;\n+\n+ procedure ValidateRowGroup()\n+ var\n+ FinancialReportRowGroup: Record \"Financial Report Row Group\";\n+ begin\n+ if \"Financial Report Row Group\" = '' then\n+ exit;\n+\n+ if not FinancialReportRowGroup.Get(\"Financial Report Row Group\") then\n+ FieldError(\"Financial Report Row Group\");\n+ end;\n+}\n--- src/O365Contact.Table.al\n+++ src/O365Contact.Table.al\n+namespace Microsoft.CRM.Outlook;\n+\n+using Microsoft.CRM.Contact;\n+using Microsoft.Foundation.Address;\n+\n+table 5370 \"O365 Contact\"\n+{\n+ Caption = 'Office 365 Contact';\n+ ExternalName = 'Contact';\n+ ExternalSchema = 'https://outlook.office.com/api/v1.0/Me/Contacts';\n+ TableType = MicrosoftGraph;\n+\n+ fields\n+ {\n+ field(1; Id; Text[250])\n+ {\n+ Caption = 'Id';\n+ ExternalName = 'Id';\n+ ExternalType = 'Edm.String';\n+ DataClassification = SystemMetadata;\n+ }\n+ field(2; CreatedDateTime; DateTime)\n+ {\n+ Caption = 'CreatedDateTime';\n+ ExternalName = 'CreatedDateTime';\n+ ExternalType = 'Edm.DateTimeOffset';\n+ DataClassification = SystemMetadata;\n+ }\n+ field(3; LastModifiedDateTime; DateTime)\n+ {\n+ Caption = 'LastModifiedDateTime';\n+ ExternalName = 'LastModifiedDateTime';\n+ ExternalType = 'Edm.DateTimeOffset';\n+ DataClassification = SystemMetadata;\n+ }\n+ field(4; Categories; BLOB)\n+ {\n+ Caption = 'Categories';\n+ ExternalName = 'Categories';\n+ ExternalType = 'Collection(Edm.String)';\n+ SubType = Json;\n+ DataClassification = CustomerContent;\n+ }\n+ field(5; ChangeKey; Text[250])\n+ {\n+ Caption = 'ChangeKey';\n+ ExternalName = 'ChangeKey';\n+ ExternalType = 'Edm.String';\n+ DataClassification = SystemMetadata;\n+ }\n+ field(10; ParentFolderId; Text[250])\n+ {\n+ Caption = 'ParentFolderId';\n+ ExternalName = 'ParentFolderId';\n+ ExternalType = 'Edm.String';\n+ DataClassification = SystemMetadata;\n+ }\n+ field(20; Birthday; DateTime)\n+ {\n+ Caption = 'Birthday';\n+ ExternalName = 'Birthday';\n+ ExternalType = 'Edm.DateTimeOffset';\n+ DataClassification = CustomerContent;\n+ }\n+ field(21; FileAs; Text[250])\n+ {\n+ Caption = 'FileAs';\n+ ExternalName = 'FileAs';\n+ ExternalType = 'Edm.String';\n+ DataClassification = CustomerContent;\n+ }\n+ field(22; DisplayName; Text[250])\n+ {\n+ Caption = 'DisplayName';\n+ ExternalName = 'DisplayName';\n+ ExternalType = 'Edm.String';\n+ DataClassification = CustomerContent;\n+ }\n+ field(23; GivenName; Text[250])\n+ {\n+ Caption = 'GivenName';\n+ ExternalName = 'GivenName';\n+ ExternalType = 'Edm.String';\n+ DataClassification = CustomerContent;\n+ }\n+ field(24; Initials; Text[250])\n+ {\n+ Caption = 'Initials';\n+ ExternalName = 'Initials';\n+ ExternalType = 'Edm.String';\n+ DataClassification = CustomerContent;\n+ }\n+ field(25; MiddleName; Text[250])\n+ {\n+ Caption = 'MiddleName';\n+ ExternalName = 'MiddleName';\n+ ExternalType = 'Edm.String';\n+ DataClassification = CustomerContent;\n+ }\n+ field(26; NickName; Text[250])\n+ {\n+ Caption = 'NickName';\n+ ExternalName = 'NickName';\n+ ExternalType = 'Edm.String';\n+ DataClassification = CustomerContent;\n+ }\n+ field(27; Surname; Text[250])\n+ {\n+ Caption = 'Surname';\n+ ExternalName = 'Surname';\n+ ExternalType = 'Edm.String';\n+ DataClassification = CustomerContent;\n+ }\n+ field(30; Title; Text[250])\n+ {\n+ Caption = 'Title';\n+ ExternalName = 'Title';\n+ ExternalType = 'Edm.String';\n+ DataClassification = CustomerContent;\n+ }\n+ field(40; YomiGivenName; Text[250])\n+ {\n+ Caption = 'YomiGivenName';\n+ ExternalName = 'YomiGivenName';\n+ ExternalType = 'Edm.String';\n+ DataClassification = CustomerContent;\n+ }\n+ field(41; YomiSurname; Text[250])\n+ {\n+ Caption = 'YomiSurname';\n+ ExternalName = 'YomiSurname';\n+ ExternalType = 'Edm.String';\n+ DataClassification = CustomerContent;\n+ }\n+ field(42; YomiCompanyName; Text[250])\n+ {\n+ Caption = 'YomiCompanyName';\n+ ExternalName = 'YomiCompanyName';\n+ ExternalType = 'Edm.String';\n+ DataClassification = CustomerContent;\n+ }\n+ field(50; Generation; Text[250])\n+ {\n+ Caption = 'Generation';\n+ ExternalName = 'Generation';\n+ ExternalType = 'Edm.String';\n+ DataClassification = CustomerContent;\n+ }\n+ field(60; ImAddresses; BLOB)\n+ {\n+ Caption = 'ImAddresses';\n+ ExternalName = 'ImAddresses';\n+ ExternalType = 'Collection(Edm.String)';\n+ SubType = Json;\n+ DataClassification = CustomerContent;\n+ }\n+ field(70; JobTitle; Text[250])\n+ {\n+ Caption = 'JobTitle';\n+ ExternalName = 'JobTitle';\n+ ExternalType = 'Edm.String';\n+ DataClassification = CustomerContent;\n+ }\n+ field(71; CompanyName; Text[250])\n+ {\n+ Caption = 'CompanyName';\n+ ExternalName = 'CompanyName';\n+ ExternalType = 'Edm.String';\n+ DataClassification = CustomerContent;\n+ }\n+ field(72; Department; Text[250])\n+ {\n+ Caption = 'Department';\n+ ExternalName = 'Department';\n+ ExternalType = 'Edm.String';\n+ DataClassification = CustomerContent;\n+ }\n+ field(73; OfficeLocation; Text[250])\n+ {\n+ Caption = 'OfficeLocation';\n+ ExternalName = 'OfficeLocation';\n+ ExternalType = 'Edm.String';\n+ DataClassification = CustomerContent;\n+ }\n+ field(80; Profession; Text[250])\n+ {\n+ Caption = 'Profession';\n+ ExternalName = 'Profession';\n+ ExternalType = 'Edm.String';\n+ DataClassification = CustomerContent;\n+ }\n+ field(81; BusinessHomePage; Text[250])\n+ {\n+ Caption = 'BusinessHomePage';\n+ ExternalName = 'BusinessHomePage';\n+ ExternalType = 'Edm.String';\n+ DataClassification = CustomerContent;\n+ }\n+ field(82; AssistantName; Text[250])\n+ {\n+ Caption = 'AssistantName';\n+ ExternalName = 'AssistantName';\n+ ExternalType = 'Edm.String';\n+ DataClassification = CustomerContent;\n+ }\n+ field(83; Manager; Text[250])\n+ {\n+ Caption = 'Manager';\n+ ExternalName = 'Manager';\n+ ExternalType = 'Edm.String';\n+ DataClassification = CustomerContent;\n+ }\n+ field(90; HomePhones; BLOB)\n+ {\n+ Caption = 'HomePhones';\n+ ExternalName = 'HomePhones';\n+ ExternalType = 'Collection(Edm.String)';\n+ SubType = Json;\n+ DataClassification = CustomerContent;\n+ }\n+ field(91; MobilePhone; Text[250])\n+ {\n+ Caption = 'MobilePhone';\n+ ExternalName = 'MobilePhone1';\n+ ExternalType = 'Edm.String';\n+ DataClassification = CustomerContent;\n+ }\n+ field(100; HomeAddress; BLOB)\n+ {\n+ Caption = 'HomeAddress';\n+ ExternalName = 'HomeAddress';\n+ ExternalType = 'Microsoft.OutlookServices.PhysicalAddress';\n+ SubType = Json;\n+ DataClassification = CustomerContent;\n+ }\n+ field(104; County; Text[100])\n+ {\n+ Caption = 'County';\n+ ExternalName = 'County';\n+ ExternalType = 'Edm.String';\n+ }\n+ field(110; BusinessAddress; BLOB)\n+ {\n+ Caption = 'BusinessAddress';\n+ ExternalName = 'BusinessAddress';\n+ ExternalType = 'Microsoft.OutlookServices.PhysicalAddress';\n+ SubType = Json;\n+ DataClassification = CustomerContent;\n+ }\n+ field(120; OtherAddress; BLOB)\n+ {\n+ Caption = 'OtherAddress';\n+ ExternalName = 'OtherAddress';\n+ ExternalType = 'Microsoft.OutlookServices.PhysicalAddress';\n+ SubType = Json;\n+ DataClassification = CustomerContent;\n+ }\n+ field(130; EmailAddresses; BLOB)\n+ {\n+ Caption = 'EmailAddresses';\n+ ExternalName = 'EmailAddresses';\n+ ExternalType = 'Collection(Microsoft.OutlookServices.EmailAddress)';\n+ SubType = Json;\n+ DataClassification = CustomerContent;\n+ }\n+ field(140; SpouseName; Text[250])\n+ {\n+ Caption = 'SpouseName';\n+ ExternalName = 'SpouseName';\n+ ExternalType = 'Edm.String';\n+ DataClassification = CustomerContent;\n+ }\n+ field(141; PersonalNotes; Text[250])\n+ {\n+ Caption = 'PersonalNotes';\n+ ExternalName = 'PersonalNotes';\n+ ExternalType = 'Edm.String';\n+ DataClassification = CustomerContent;\n+ }\n+ field(150; Children; BLOB)\n+ {\n+ Caption = 'Children';\n+ ExternalName = 'Children';\n+ ExternalType = 'Collection(Edm.String)';\n+ SubType = Json;\n+ DataClassification = CustomerContent;\n+ }\n+ }\n+\n+ keys\n+ {\n+ key(Key1; Id)\n+ {\n+ Clustered = true;\n+ }\n+ }\n+\n+ fieldgroups\n+ {\n+ }\n+\n+ procedure GetHomeAddressCountryOrRegion(): Text\n+ var\n+ GraphCollectionMgtContact: Codeunit \"Graph Collection Mgt - Contact\";\n+ begin\n+ exit(GraphCollectionMgtContact.GetAddressCountryOrRegion(HomeAddress));\n+ end;\n+\n+ procedure GetBusinessAddressCountryOrRegion(): Text\n+ var\n+ GraphCollectionMgtContact: Codeunit \"Graph Collection Mgt - Contact\";\n+ begin\n+ exit(GraphCollectionMgtContact.GetAddressCountryOrRegion(BusinessAddress));\n+ end;\n+\n+ procedure HasEmailAddresses(): Boolean\n+ var\n+ GraphCollectionMgtContact: Codeunit \"Graph Collection Mgt - Contact\";\n+ begin\n+ exit(GraphCollectionMgtContact.HasEmailAddresses(EmailAddresses));\n+ end;\n+\n+ procedure GetPrimaryEmailAddress(): Text\n+ var\n+ GraphCollectionMgtContact: Codeunit \"Graph Collection Mgt - Contact\";\n+ begin\n+ exit(GraphCollectionMgtContact.GetPrimaryEmailAddress(EmailAddresses));\n+ end;\n+\n+ procedure GetDisplayNameValue(): Text\n+ begin\n+ if DisplayName <> '' then\n+ exit(DisplayName);\n+ if (GivenName <> '') and (Surname <> '') then\n+ exit(GivenName + ' ' + Surname);\n+ if GivenName <> '' then\n+ exit(GivenName);\n+ if Surname <> '' then\n+ exit(Surname);\n+ exit('');\n+ end;\n+}\n--- src/ServiceShipmentLine.Table.al\n+++ src/ServiceShipmentLine.Table.al\n+namespace Microsoft.Service.History;\n+\n+using Microsoft.Finance.Dimension;\n+using Microsoft.Inventory.Item;\n+using Microsoft.Inventory.Location;\n+using Microsoft.Service.Setup;\n+\n+table 5991 \"Service Shipment Line\"\n+{\n+ Caption = 'Service Shipment Line';\n+ DrillDownPageID = \"Posted Service Shipment Lines\";\n+ LookupPageID = \"Posted Service Shipment Lines\";\n+\n+ fields\n+ {\n+ field(1; \"Document No.\"; Code[20])\n+ {\n+ Caption = 'Document No.';\n+ DataClassification = CustomerContent;\n+ }\n+ field(2; \"Line No.\"; Integer)\n+ {\n+ Caption = 'Line No.';\n+ DataClassification = SystemMetadata;\n+ }\n+ field(3; \"Sell-to Customer No.\"; Code[20])\n+ {\n+ Caption = 'Sell-to Customer No.';\n+ Editable = false;\n+ TableRelation = Customer;\n+ DataClassification = CustomerContent;\n+ }\n+ field(5; Type; Enum \"Service Line Type\")\n+ {\n+ Caption = 'Type';\n+ DataClassification = SystemMetadata;\n+ }\n+ field(6; \"No.\"; Code[20])\n+ {\n+ CaptionClass = GetCaptionClass(FieldNo(\"No.\"));\n+ Caption = 'No.';\n+ TableRelation = if (Type = const(\" \")) \"Standard Text\"\n+ else\n+ if (Type = const(Item)) Item\n+ else\n+ if (Type = const(Resource)) Resource\n+ else\n+ if (Type = const(Cost)) \"Service Cost\";\n+ DataClassification = CustomerContent;\n+ }\n+ field(7; \"Location Code\"; Code[10])\n+ {\n+ Caption = 'Location Code';\n+ TableRelation = Location;\n+ DataClassification = CustomerContent;\n+ }\n+ field(8; \"Posting Group\"; Code[20])\n+ {\n+ Caption = 'Posting Group';\n+ Editable = false;\n+ TableRelation = if (Type = const(Item)) \"Inventory Posting Group\"\n+ else\n+ if (Type = const(Resource)) \"Gen. Product Posting Group\";\n+ DataClassification = CustomerContent;\n+ }\n+ field(10; \"Shipment Date\"; Date)\n+ {\n+ Caption = 'Shipment Date';\n+ DataClassification = CustomerContent;\n+ }\n+ field(11; Description; Text[100])\n+ {\n+ Caption = 'Description';\n+ DataClassification = CustomerContent;\n+ }\n+ field(12; \"Description 2\"; Text[50])\n+ {\n+ Caption = 'Description 2';\n+ DataClassification = CustomerContent;\n+ }\n+ field(13; \"Unit of Measure\"; Text[50])\n+ {\n+ Caption = 'Unit of Measure';\n+ DataClassification = CustomerContent;\n+ }\n+ field(15; Quantity; Decimal)\n+ {\n+ Caption = 'Quantity';\n+ DecimalPlaces = 0 : 5;\n+ DataClassification = CustomerContent;\n+ }\n+ field(417; \"External Document No.\"; Code[35])\n+ {\n+ Caption = 'External Document No.';\n+ }\n+ field(480; \"Dimension Set ID\"; Integer)\n+ {\n+ Caption = 'Dimension Set ID';\n+ Editable = false;\n+ TableRelation = \"Dimension Set Entry\";\n+ DataClassification = SystemMetadata;\n+\n+ trigger OnLookup()\n+ begin\n+ Rec.ShowDimensions();\n+ end;\n+ }\n+ field(1001; \"Job Task No.\"; Code[20])\n+ {\n+ Caption = 'Job Task No.';\n+ Editable = false;\n+ TableRelation = \"Job Task\".\"Job Task No.\" where(\"Job No.\" = field(\"Job No.\"));\n+ DataClassification = CustomerContent;\n+ }\n+ field(1002; \"Job Line Type\"; Enum \"Job Line Type\")\n+ {\n+ Caption = 'Job Line Type';\n+ DataClassification = SystemMetadata;\n+ }\n+ field(1003; \"Job No.\"; Code[20])\n+ {\n+ Caption = 'Job No.';\n+ Editable = false;\n+ TableRelation = Job;\n+ DataClassification = CustomerContent;\n+ }\n+ field(5402; \"Variant Code\"; Code[10])\n+ {\n+ Caption = 'Variant Code';\n+ TableRelation = if (Type = const(Item)) \"Item Variant\".Code where(\"Item No.\" = field(\"No.\"));\n+ DataClassification = CustomerContent;\n+ }\n+ field(5403; \"Bin Code\"; Code[20])\n+ {\n+ Caption = 'Bin Code';\n+ TableRelation = Bin.Code where(\"Location Code\" = field(\"Location Code\"));\n+ DataClassification = CustomerContent;\n+ }\n+ field(5404; \"Qty. per Unit of Measure\"; Decimal)\n+ {\n+ Caption = 'Qty. per Unit of Measure';\n+ DecimalPlaces = 0 : 5;\n+ Editable = false;\n+ InitValue = 1;\n+ DataClassification = CustomerContent;\n+ }\n+ field(5407; \"Unit of Measure Code\"; Code[10])\n+ {\n+ Caption = 'Unit of Measure Code';\n+ TableRelation = if (Type = const(Item)) \"Item Unit of Measure\".Code where(\"Item No.\" = field(\"No.\"))\n+ else\n+ if (Type = const(Resource)) \"Resource Unit of Measure\".Code where(\"Resource No.\" = field(\"No.\"));\n+ DataClassification = CustomerContent;\n+ }\n+ field(5415; \"Quantity (Base)\"; Decimal)\n+ {\n+ Caption = 'Quantity (Base)';\n+ DecimalPlaces = 0 : 5;\n+ DataClassification = CustomerContent;\n+ }\n+ field(5700; \"Responsibility Center\"; Code[10])\n+ {\n+ Caption = 'Responsibility Center';\n+ TableRelation = \"Responsibility Center\";\n+ DataClassification = CustomerContent;\n+ }\n+ }\n+\n+ keys\n+ {\n+ key(Key1; \"Document No.\", \"Line No.\")\n+ {\n+ Clustered = true;\n+ }\n+ key(Key2; \"Order No.\", \"Order Line No.\")\n+ {\n+ }\n+ key(Key3; \"Blanket Order No.\", \"Blanket Order Line No.\")\n+ {\n+ }\n+ key(Key4; \"Item Shpt. Entry No.\")\n+ {\n+ }\n+ key(Key5; \"Sell-to Customer No.\")\n+ {\n+ }\n+ key(Key6; \"Bill-to Customer No.\")\n+ {\n+ }\n+ key(Key7; Type, \"No.\")\n+ {\n+ }\n+ key(Key8; \"Location Code\")\n+ {\n+ }\n+ key(Key9; \"Order No.\", \"Shipment Date\")\n+ {\n+ }\n+ }\n+\n+ fieldgroups\n+ {\n+ }\n+\n+ procedure ShowDimensions()\n+ var\n+ DimMgt: Codeunit DimensionManagement;\n+ begin\n+ DimMgt.ShowDimensionSet(\"Dimension Set ID\", StrSubstNo('%1 %2 %3', TableCaption(), \"Document No.\", \"Line No.\"));\n+ end;\n+\n+ procedure GetCaptionClass(FieldNumber: Integer): Text[80]\n+ var\n+ ServiceShipmentHeader: Record \"Service Shipment Header\";\n+ begin\n+ if not ServiceShipmentHeader.Get(\"Document No.\") then\n+ exit('');\n+ if ServiceShipmentHeader.\"Language Code\" = '' then\n+ exit('');\n+ case FieldNumber of\n+ FieldNo(\"No.\"):\n+ exit('1,1,' + ServiceShipmentHeader.\"Language Code\");\n+ end;\n+ end;\n+\n+ local procedure GetItem()\n+ var\n+ Item: Record Item;\n+ begin\n+ TestField(\"No.\");\n+ if \"No.\" <> xRec.\"No.\" then\n+ Item.Get(\"No.\");\n+ end;\n+\n+ procedure UpdateDimensions()\n+ var\n+ DimMgt: Codeunit DimensionManagement;\n+ begin\n+ if \"Dimension Set ID\" = 0 then begin\n+ \"Dimension Set ID\" := DimMgt.GetDefaultDimID(\n+ DATABASE::\"Service Shipment Line\", 0, \"No.\", '',\n+ 0, 0, 0, 0, 0);\n+ end;\n+ end;\n+}", "expected_comments": [{"file": "src/ContactSyncFolder.Table.al", "line_start": 65, "line_end": 65, "body": "Field 'Contact Notes' has DataClassification = SystemMetadata but contains free-text user-entered notes about contacts, which is CustomerContent \u2014 Change DataClassification from SystemMetadata to CustomerContent. Free-text notes about contacts are user-entered personal data.", "severity": "medium"}, {"file": "src/FinancialReport.Table.al", "line_start": 70, "line_end": 70, "body": "Field 'CategoryCode' is missing DataClassification property \u2014 See agent comment for details.", "severity": "medium"}, {"file": "src/O365Contact.Table.al", "line_start": 236, "line_end": 236, "body": "Field 'County' is missing DataClassification property \u2014 See agent comment for details.", "severity": "medium"}, {"file": "src/ServiceShipmentLine.Table.al", "line_start": 92, "line_end": 92, "body": "Field 'External Document No.' is missing DataClassification property \u2014 See agent comment for details.", "severity": "medium"}], "match_line_tolerance": 2, "domain": "privacy", "category": "code-review", "description": "True positive privacy findings: dataclassification (trimmed to 6 representative findings)", "expect_findings": true, "source": "vsoadmin"}
+{"repo": "microsoft/BCApps", "instance_id": "synthetic__privacy-013", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "privacy"}, "patch": "--- src/ExpenseUser.Table.al\n+++ src/ExpenseUser.Table.al\n+namespace Microsoft.ExpenseAgent.MasterData;\n+\n+using Microsoft.HumanResources.Employee;\n+using Microsoft.Foundation.Address;\n+using System.Security.User;\n+\n+table 57110 \"Expense User\"\n+{\n+ Caption = 'Expense User';\n+ DataClassificationFields = \"eMail\";\n+\n+ fields\n+ {\n+ field(1; \"User Security ID\"; Guid)\n+ {\n+ Caption = 'User Security ID';\n+ DataClassification = EndUserPseudonymousIdentifiers;\n+ TableRelation = User.\"User Security ID\";\n+ NotBlank = true;\n+ }\n+ field(2; \"Employee No.\"; Code[20])\n+ {\n+ Caption = 'Employee No.';\n+ DataClassification = EndUserPseudonymousIdentifiers;\n+ TableRelation = Employee;\n+ NotBlank = true;\n+ }\n+ field(5; \"User Name\"; Code[50])\n+ {\n+ Caption = 'User Name';\n+ DataClassification = EndUserIdentifiableInformation;\n+ NotBlank = true;\n+ }\n+ field(6; \"Full Name\"; Text[100])\n+ {\n+ Caption = 'Full Name';\n+ DataClassification = EndUserIdentifiableInformation;\n+ }\n+ field(10; eMail; Text[250])\n+ {\n+ Caption = 'E-Mail';\n+ DataClassification = EndUserIdentifiableInformation;\n+ ExtendedDatatype = EMail;\n+\n+ trigger OnValidate()\n+ begin\n+ ValidateEmailAddress();\n+ end;\n+ }\n+ field(15; \"Manager User Security ID\"; Guid)\n+ {\n+ Caption = 'Manager User Security ID';\n+ DataClassification = EndUserPseudonymousIdentifiers;\n+ TableRelation = \"Expense User\".\"User Security ID\" where(\"Allow Expense Approval\" = const(true), \"Is Active\" = const(true));\n+ }\n+ field(16; \"Manager Employee No.\"; Code[20])\n+ {\n+ Caption = 'Manager Employee No.';\n+ DataClassification = EndUserPseudonymousIdentifiers;\n+ TableRelation = Employee;\n+ Editable = false;\n+ }\n+ field(20; \"Department Code\"; Code[20])\n+ {\n+ Caption = 'Department Code';\n+ DataClassification = CustomerContent;\n+ TableRelation = \"Dimension Value\".Code where(\"Global Dimension No.\" = const(1), Blocked = const(false));\n+ }\n+ field(25; \"Approval Limit\"; Decimal)\n+ {\n+ Caption = 'Approval Limit';\n+ DataClassification = CustomerContent;\n+ MinValue = 0;\n+ }\n+ field(30; \"Allow Expense Submission\"; Boolean)\n+ {\n+ Caption = 'Allow Expense Submission';\n+ DataClassification = SystemMetadata;\n+ InitValue = true;\n+ }\n+ field(31; \"Allow Expense Approval\"; Boolean)\n+ {\n+ Caption = 'Allow Expense Approval';\n+ DataClassification = SystemMetadata;\n+\n+ trigger OnValidate()\n+ begin\n+ if not \"Allow Expense Approval\" then\n+ TestField(\"Approval Limit\", 0);\n+ end;\n+ }\n+ field(35; \"Is Active\"; Boolean)\n+ {\n+ Caption = 'Is Active';\n+ DataClassification = SystemMetadata;\n+ InitValue = true;\n+ }\n+ field(40; \"Last Login DateTime\"; DateTime)\n+ {\n+ Caption = 'Last Login DateTime';\n+ DataClassification = SystemMetadata;\n+ Editable = false;\n+ }\n+ field(45; \"Created DateTime\"; DateTime)\n+ {\n+ Caption = 'Created DateTime';\n+ DataClassification = SystemMetadata;\n+ Editable = false;\n+ }\n+ field(46; \"Modified DateTime\"; DateTime)\n+ {\n+ Caption = 'Modified DateTime';\n+ DataClassification = SystemMetadata;\n+ Editable = false;\n+ }\n+ field(50; \"Default Currency Code\"; Code[10])\n+ {\n+ Caption = 'Default Currency Code';\n+ DataClassification = CustomerContent;\n+ TableRelation = Currency;\n+ }\n+ field(55; \"Time Zone ID\"; Text[100])\n+ {\n+ Caption = 'Time Zone ID';\n+ DataClassification = CustomerContent;\n+ }\n+ field(60; \"Language Code\"; Code[10])\n+ {\n+ Caption = 'Language Code';\n+ DataClassification = CustomerContent;\n+ TableRelation = Language;\n+ }\n+ field(65; \"Cost Center Code\"; Code[20])\n+ {\n+ Caption = 'Cost Center Code';\n+ DataClassification = CustomerContent;\n+ TableRelation = \"Dimension Value\".Code where(\"Global Dimension No.\" = const(2), Blocked = const(false));\n+ }\n+ field(70; \"Project Default Code\"; Code[20])\n+ {\n+ Caption = 'Project Default Code';\n+ DataClassification = CustomerContent;\n+ TableRelation = Job;\n+ }\n+ field(75; \"Global Dimension 1 Code\"; Code[20])\n+ {\n+ Caption = 'Global Dimension 1 Code';\n+ CaptionClass = '1,1,1';\n+ DataClassification = CustomerContent;\n+ TableRelation = \"Dimension Value\".Code where(\"Global Dimension No.\" = const(1), Blocked = const(false));\n+ }\n+ field(76; \"Global Dimension 2 Code\"; Code[20])\n+ {\n+ Caption = 'Global Dimension 2 Code';\n+ CaptionClass = '1,1,2';\n+ DataClassification = CustomerContent;\n+ TableRelation = \"Dimension Value\".Code where(\"Global Dimension No.\" = const(2), Blocked = const(false));\n+ }\n+ field(80; \"Notification Preferences\"; Text[1000])\n+ {\n+ Caption = 'Notification Preferences';\n+ DataClassification = CustomerContent;\n+ }\n+ }\n+\n+ keys\n+ {\n+ key(Key1; \"User Security ID\")\n+ {\n+ Clustered = true;\n+ }\n+ key(Key2; \"Employee No.\")\n+ {\n+ }\n+ key(Key3; eMail)\n+ {\n+ }\n+ key(Key4; \"Manager User Security ID\")\n+ {\n+ }\n+ }\n+\n+ fieldgroups\n+ {\n+ fieldgroup(DropDown; \"Employee No.\", \"Full Name\", eMail)\n+ {\n+ }\n+ fieldgroup(Brick; \"Employee No.\", \"Full Name\", \"Department Code\", \"Is Active\")\n+ {\n+ }\n+ }\n+\n+ trigger OnInsert()\n+ begin\n+ if \"Created DateTime\" = 0DT then\n+ \"Created DateTime\" := CurrentDateTime;\n+ \"Modified DateTime\" := CurrentDateTime;\n+\n+ ValidateManagerHierarchy();\n+ SyncWithEmployee();\n+ end;\n+\n+ trigger OnModify()\n+ begin\n+ \"Modified DateTime\" := CurrentDateTime;\n+\n+ if \"Manager User Security ID\" <> xRec.\"Manager User Security ID\" then\n+ ValidateManagerHierarchy();\n+\n+ if \"Employee No.\" <> xRec.\"Employee No.\" then\n+ SyncWithEmployee();\n+ end;\n+\n+ trigger OnDelete()\n+ var\n+ ExpenseHeader: Record \"Expense Header\";\n+ SubordinateUser: Record \"Expense User\";\n+ begin\n+ ExpenseHeader.SetRange(\"Employee No.\", \"Employee No.\");\n+ ExpenseHeader.SetFilter(Status, '<>%1', ExpenseHeader.Status::Approved);\n+ if not ExpenseHeader.IsEmpty then\n+ Error('Cannot delete user %1. There are pending expense documents.', \"Full Name\");\n+\n+ SubordinateUser.SetRange(\"Manager User Security ID\", \"User Security ID\");\n+ SubordinateUser.ModifyAll(\"Manager User Security ID\", \"User Security ID\");\n+ end;\n+\n+ local procedure ValidateEmailAddress()\n+ var\n+ EmailValidation: Codeunit \"Email Address Validation\";\n+ DuplicateUser: Record \"Expense User\";\n+ begin\n+ if eMail = '' then\n+ exit;\n+\n+ if not EmailValidation.IsValidEmailAddress(eMail) then\n+ Error('Invalid email address format: %1', eMail);\n+\n+ DuplicateUser.SetCurrentKey(eMail);\n+ DuplicateUser.SetRange(eMail, eMail);\n+ DuplicateUser.SetFilter(\"User Security ID\", '<>%1', \"User Security ID\");\n+ DuplicateUser.SetRange(\"Is Active\", true);\n+\n+ if DuplicateUser.FindFirst() then\n+ Error('Email address %1 is already in use by %2', eMail, DuplicateUser.\"Full Name\");\n+ end;\n+\n+ local procedure ValidateManagerHierarchy()\n+ var\n+ ManagerUser: Record \"Expense User\";\n+ Employee: Record Employee;\n+ begin\n+ if IsNullGuid(\"Manager User Security ID\") then\n+ exit;\n+\n+ if \"Manager User Security ID\" = \"User Security ID\" then\n+ Error('A user cannot be their own manager.');\n+\n+ ManagerUser.SetRange(\"User Security ID\", \"Manager User Security ID\");\n+ ManagerUser.SetRange(\"Is Active\", true);\n+\n+ if not ManagerUser.FindFirst() then\n+ Error('Manager with User Security ID %1 does not exist or is inactive.', \"Manager User Security ID\");\n+\n+ if not ManagerUser.\"Allow Expense Approval\" then\n+ Error('Manager %1 is not authorized to approve expenses.', ManagerUser.\"Full Name\");\n+\n+ \"Manager Employee No.\" := ManagerUser.\"Employee No.\";\n+ end;\n+\n+ local procedure SyncWithEmployee()\n+ var\n+ Employee: Record Employee;\n+ User: Record User;\n+ begin\n+ if Employee.Get(\"Employee No.\") then begin\n+ if \"Full Name\" = '' then\n+ \"Full Name\" := Employee.FullName();\n+\n+ if eMail = '' then begin\n+ if Employee.\"Company E-Mail\" <> '' then\n+ eMail := Employee.\"Company E-Mail\"\n+ else if Employee.\"E-Mail\" <> '' then\n+ eMail := Employee.\"E-Mail\";\n+ end;\n+ end;\n+\n+ if User.Get(\"User Security ID\") then begin\n+ if \"User Name\" = '' then\n+ \"User Name\" := User.\"User Name\";\n+ end;\n+ end;\n+\n+ procedure GetManagerName(): Text[100]\n+ var\n+ ManagerUser: Record \"Expense User\";\n+ begin\n+ if IsNullGuid(\"Manager User Security ID\") then\n+ exit('');\n+\n+ ManagerUser.SetRange(\"User Security ID\", \"Manager User Security ID\");\n+ if ManagerUser.FindFirst() then\n+ exit(ManagerUser.\"Full Name\");\n+ end;\n+\n+ procedure HasApprovalRights(): Boolean\n+ begin\n+ exit(\"Allow Expense Approval\" and \"Is Active\");\n+ end;\n+\n+ procedure CanApproveAmount(Amount: Decimal): Boolean\n+ begin\n+ if not HasApprovalRights() then\n+ exit(false);\n+\n+ if \"Approval Limit\" = 0 then // Unlimited approval\n+ exit(true);\n+\n+ exit(Amount <= \"Approval Limit\");\n+ end;\n+\n+ procedure UpdateLastLogin()\n+ begin\n+ \"Last Login DateTime\" := CurrentDateTime;\n+ Modify(false);\n+ end;\n+\n+ var\n+ OnlyBCUserCanApproveErr: Label 'Only Business Central users can approve expenses. User %1 with email %2 is not a valid BC user.', Comment = '%1 = Full Name, %2 = Email';\n+\n+ procedure ValidateBCUserForApproval()\n+ var\n+ User: Record User;\n+ ErrorMsg: Text;\n+ begin\n+ if not \"Allow Expense Approval\" then\n+ exit;\n+\n+ User.SetRange(\"User Security ID\", \"User Security ID\");\n+ User.SetRange(State, User.State::Enabled);\n+\n+ if not User.FindFirst() then begin\n+ ErrorMsg := StrSubstNo(OnlyBCUserCanApproveErr, \"Full Name\", eMail);\n+ Error(ErrorMsg);\n+ end;\n+\n+ if User.\"License Type\" = User.\"License Type\"::\"External User\" then begin\n+ ErrorMsg := StrSubstNo(OnlyBCUserCanApproveErr, \"Full Name\", eMail);\n+ Error(ErrorMsg);\n+ end;\n+ end;\n+}\n--- src/JobQueueErrorHandler.Codeunit.al\n+++ src/JobQueueErrorHandler.Codeunit.al\n+namespace System.Threading;\n+\n+using Microsoft.Foundation.NoSeries;\n+using System.Environment.Configuration;\n+using System.Telemetry;\n+\n+codeunit 452 \"Job Queue Error Handler\"\n+{\n+ TableNo = \"Job Queue Entry\";\n+\n+ trigger OnRun()\n+ begin\n+ HandleJobQueueError(Rec);\n+ end;\n+\n+ var\n+ JobQueueEntryTxt: Label 'Job Queue Entry: %1', Comment = '%1 = Job Queue Entry ID';\n+ ErrorProcessingJobTxt: Label 'Error processing job queue entry %1. Error: %2', Comment = '%1 = Job Queue Entry ID, %2 = Error message';\n+ RetryAttemptTxt: Label 'Retry attempt %1 of %2 for Job Queue Entry %3', Comment = '%1 = Current attempt, %2 = Max attempts, %3 = Job Queue Entry ID';\n+\n+ local procedure HandleJobQueueError(var JobQueueEntry: Record \"Job Queue Entry\")\n+ var\n+ JobQueueManagement: Codeunit \"Job Queue Management\";\n+ Telemetry: Codeunit Telemetry;\n+ ErrorMsg: Text;\n+ CustomDimensions: Dictionary of [Text, Text];\n+ begin\n+ ErrorMsg := GetLastErrorText();\n+\n+ CustomDimensions.Add('JobQueueEntryId', Format(JobQueueEntry.ID));\n+ CustomDimensions.Add('ObjectType', Format(JobQueueEntry.\"Object Type to Run\"));\n+ CustomDimensions.Add('ObjectId', Format(JobQueueEntry.\"Object ID to Run\"));\n+ CustomDimensions.Add('CompanyName', CompanyName);\n+\n+ Telemetry.LogMessage('0000ABC', StrSubstNo(ErrorProcessingJobTxt, JobQueueEntry.ID, ErrorMsg),\n+ Verbosity::Error, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, CustomDimensions);\n+\n+ JobQueueEntry.\"Error Message\" := CopyStr(ErrorMsg, 1, MaxStrLen(JobQueueEntry.\"Error Message\"));\n+ JobQueueEntry.Status := JobQueueEntry.Status::Error;\n+ JobQueueEntry.\"No. of Attempts to Run\" := JobQueueEntry.\"No. of Attempts to Run\" + 1;\n+ JobQueueEntry.\"Earliest Start Date/Time\" := CurrentDateTime + CalculateRetryDelay(JobQueueEntry);\n+\n+ if ShouldRetryJob(JobQueueEntry) then begin\n+ JobQueueEntry.Status := JobQueueEntry.Status::Ready;\n+\n+ Telemetry.LogMessage('0000ABD', StrSubstNo(RetryAttemptTxt,\n+ JobQueueEntry.\"No. of Attempts to Run\",\n+ JobQueueEntry.\"Maximum No. of Attempts to Run\",\n+ JobQueueEntry.ID),\n+ Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, CustomDimensions);\n+ end else begin\n+ SendFailureNotification(JobQueueEntry, ErrorMsg);\n+ end;\n+\n+ JobQueueEntry.\"Last Ready State\" := CurrentDateTime;\n+ JobQueueEntry.Modify(true);\n+\n+ ClearLastError();\n+ end;\n+\n+ local procedure ShouldRetryJob(JobQueueEntry: Record \"Job Queue Entry\"): Boolean\n+ begin\n+ if JobQueueEntry.\"Maximum No. of Attempts to Run\" = 0 then\n+ exit(false);\n+\n+ exit(JobQueueEntry.\"No. of Attempts to Run\" < JobQueueEntry.\"Maximum No. of Attempts to Run\");\n+ end;\n+\n+ local procedure CalculateRetryDelay(JobQueueEntry: Record \"Job Queue Entry\"): Duration\n+ var\n+ BaseDelayMinutes: Integer;\n+ ExponentialFactor: Integer;\n+ begin\n+ BaseDelayMinutes := 5;\n+ ExponentialFactor := Power(2, JobQueueEntry.\"No. of Attempts to Run\" - 1);\n+\n+ if BaseDelayMinutes * ExponentialFactor > 240 then\n+ ExponentialFactor := 240 div BaseDelayMinutes;\n+\n+ exit(BaseDelayMinutes * ExponentialFactor * 60 * 1000); // Convert to milliseconds\n+ end;\n+\n+ local procedure SendFailureNotification(JobQueueEntry: Record \"Job Queue Entry\"; ErrorMessage: Text)\n+ var\n+ NotificationMgt: Codeunit \"Notification Management\";\n+ Company: Record Company;\n+ AdminUser: Record User;\n+ EmailMessage: Codeunit \"Email Message\";\n+ Email: Codeunit Email;\n+ Subject: Text;\n+ Body: Text;\n+ ToRecipients: List of [Text];\n+ begin\n+ AdminUser.SetRange(\"License Type\", AdminUser.\"License Type\"::\"Full User\");\n+ AdminUser.SetFilter(\"Windows Security ID\", '<>%1', '');\n+ AdminUser.SetRange(State, AdminUser.State::Enabled);\n+\n+ if not AdminUser.FindSet() then\n+ exit;\n+\n+ Subject := StrSubstNo('Job Queue Entry Failure - %1', JobQueueEntry.Description);\n+\n+ Body := 'A job queue entry has failed after exhausting all retry attempts.' + CRLF + CRLF;\n+ Body += 'Job Queue Entry ID: ' + Format(JobQueueEntry.ID) + CRLF;\n+ Body += 'Description: ' + JobQueueEntry.Description + CRLF;\n+ Body += 'Object Type: ' + Format(JobQueueEntry.\"Object Type to Run\") + CRLF;\n+ Body += 'Object ID: ' + Format(JobQueueEntry.\"Object ID to Run\") + CRLF;\n+ Body += 'Company: ' + CompanyName + CRLF;\n+ Body += 'Failed At: ' + Format(CurrentDateTime) + CRLF;\n+ Body += 'Attempts Made: ' + Format(JobQueueEntry.\"No. of Attempts to Run\") + CRLF + CRLF;\n+ Body += 'Error Message:' + CRLF;\n+ Body += ErrorMessage + CRLF + CRLF;\n+ Body += 'Please check the Job Queue Entries page for more details and take appropriate action.';\n+\n+ repeat\n+ if AdminUser.\"Contact Email\" <> '' then\n+ ToRecipients.Add(AdminUser.\"Contact Email\");\n+ until AdminUser.Next() = 0;\n+\n+ if ToRecipients.Count > 0 then begin\n+ EmailMessage.Create(ToRecipients, Subject, Body, true);\n+ Email.Send(EmailMessage, Enum::\"Email Scenario\"::\"Job Queue\");\n+ end;\n+ end;\n+\n+ procedure ResetJobQueueEntry(var JobQueueEntry: Record \"Job Queue Entry\")\n+ var\n+ Telemetry: Codeunit Telemetry;\n+ CustomDimensions: Dictionary of [Text, Text];\n+ begin\n+ JobQueueEntry.Status := JobQueueEntry.Status::Ready;\n+ JobQueueEntry.\"No. of Attempts to Run\" := 0;\n+ Clear(JobQueueEntry.\"Error Message\");\n+ JobQueueEntry.\"Earliest Start Date/Time\" := CurrentDateTime;\n+ JobQueueEntry.\"Last Ready State\" := CurrentDateTime;\n+ JobQueueEntry.Modify(true);\n+\n+ CustomDimensions.Add('JobQueueEntryId', Format(JobQueueEntry.ID));\n+ CustomDimensions.Add('ObjectType', Format(JobQueueEntry.\"Object Type to Run\"));\n+ CustomDimensions.Add('ObjectId', Format(JobQueueEntry.\"Object ID to Run\"));\n+\n+ Telemetry.LogMessage('0000ABE', StrSubstNo(JobQueueEntryTxt, JobQueueEntry.ID) + ' reset by user',\n+ Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, CustomDimensions);\n+ end;\n+\n+ [EventSubscriber(ObjectType::Table, Database::\"Job Queue Entry\", 'OnAfterInsertEvent', '', false, false)]\n+ local procedure OnAfterInsertJobQueueEntry(var Rec: Record \"Job Queue Entry\")\n+ var\n+ Telemetry: Codeunit Telemetry;\n+ CustomDimensions: Dictionary of [Text, Text];\n+ begin\n+ if Rec.IsTemporary then\n+ exit;\n+\n+ CustomDimensions.Add('JobQueueEntryId', Format(Rec.ID));\n+ CustomDimensions.Add('ObjectType', Format(Rec.\"Object Type to Run\"));\n+ CustomDimensions.Add('ObjectId', Format(Rec.\"Object ID to Run\"));\n+ CustomDimensions.Add('RecurringJob', Format(Rec.\"Recurring Job\"));\n+\n+ Telemetry.LogMessage('0000ABF', StrSubstNo(JobQueueEntryTxt, Rec.ID) + ' created',\n+ Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, CustomDimensions);\n+ end;\n+}\n--- src/ReleaseExpenseDocument.Codeunit.al\n+++ src/ReleaseExpenseDocument.Codeunit.al\n+namespace Microsoft.ExpenseAgent.Expense;\n+\n+using Microsoft.ExpenseAgent.MasterData;\n+using Microsoft.Foundation.Reporting;\n+using System.Globalization;\n+\n+codeunit 57089 \"Release Expense Document\"\n+{\n+ TableNo = \"Expense Header\";\n+\n+ trigger OnRun()\n+ begin\n+ ReleaseExpenseDocument(Rec);\n+ end;\n+\n+ var\n+ ExpenseDocumentReleasedMsg: Label 'Expense document %1 has been released successfully.', Comment = '%1 = Document No.';\n+ ExpenseDocumentReleaseFailedErr: Label 'Failed to release expense document %1. Please check the document and try again.', Comment = '%1 = Document No.';\n+ MissingApproverErr: Label 'Cannot release expense document %1. No approver is assigned.', Comment = '%1 = Document No.';\n+ InsufficientApprovalLimitErr: Label 'Cannot release expense document %1. The assigned approver does not have sufficient approval limits.', Comment = '%1 = Document No.';\n+ DocumentAlreadyReleasedErr: Label 'Expense document %1 is already released.', Comment = '%1 = Document No.';\n+ InvalidReceiptDataErr: Label 'Invalid receipt data in expense line %1. Receipt No.: %2, Expense Date: %3, Merchant: %4', Comment = '%1 = Line No., %2 = Receipt No., %3 = Expense Date, %4 = Merchant Name';\n+\n+ procedure ReleaseExpenseDocument(var ExpenseHeader: Record \"Expense Header\"): Boolean\n+ var\n+ ExpenseLine: Record \"Expense Line\";\n+ ExpenseUser: Record \"Expense User\";\n+ ApprovalMgt: Codeunit \"Expense Approval Management\";\n+ FeatureTelemetry: Codeunit \"Feature Telemetry\";\n+ ErrorMsg: Text;\n+ TotalAmount: Decimal;\n+ begin\n+ if ExpenseHeader.Status = ExpenseHeader.Status::Released then\n+ Error(DocumentAlreadyReleasedErr, ExpenseHeader.\"No.\");\n+\n+ ValidateApprover(ExpenseHeader);\n+\n+ ExpenseLine.SetRange(\"Document No.\", ExpenseHeader.\"No.\");\n+ if ExpenseLine.FindSet() then\n+ repeat\n+ ValidateExpenseLine(ExpenseLine);\n+ TotalAmount += ExpenseLine.Amount;\n+ until ExpenseLine.Next() = 0;\n+\n+ if not CheckApprovalLimits(ExpenseHeader, TotalAmount) then\n+ Error(InsufficientApprovalLimitErr, ExpenseHeader.\"No.\");\n+\n+ ExpenseHeader.Status := ExpenseHeader.Status::Released;\n+ ExpenseHeader.\"Released Date\" := Today;\n+ ExpenseHeader.\"Released Time\" := Time;\n+ ExpenseHeader.\"Released By\" := UserId;\n+\n+ if ExpenseHeader.Modify(true) then begin\n+ FeatureTelemetry.LogUsage('0000EA1', 'Expense Agent', 'Document Released', GetTelemetryDimensions(ExpenseHeader));\n+\n+ ApprovalMgt.SendReleaseNotification(ExpenseHeader);\n+\n+ Message(ExpenseDocumentReleasedMsg, ExpenseHeader.\"No.\");\n+ exit(true);\n+ end else begin\n+ Error(ExpenseDocumentReleaseFailedErr, ExpenseHeader.\"No.\");\n+ end;\n+ end;\n+\n+ local procedure ValidateApprover(ExpenseHeader: Record \"Expense Header\")\n+ var\n+ ExpenseUser: Record \"Expense User\";\n+ begin\n+ if IsNullGuid(ExpenseHeader.\"Approver User ID\") then\n+ Error(MissingApproverErr, ExpenseHeader.\"No.\");\n+\n+ ExpenseUser.SetRange(\"User Security ID\", ExpenseHeader.\"Approver User ID\");\n+ ExpenseUser.SetRange(\"Is Active\", true);\n+ ExpenseUser.SetRange(\"Allow Expense Approval\", true);\n+\n+ if not ExpenseUser.FindFirst() then\n+ Error(MissingApproverErr, ExpenseHeader.\"No.\");\n+ end;\n+\n+ local procedure ValidateExpenseLine(ExpenseLine: Record \"Expense Line\")\n+ var\n+ ErrorMsg: Text;\n+ begin\n+ ExpenseLine.TestField(\"Expense Date\");\n+ ExpenseLine.TestField(Amount);\n+ ExpenseLine.TestField(\"Expense Type Code\");\n+\n+ if (ExpenseLine.\"Receipt No.\" <> '') or (ExpenseLine.\"Receipt Date\" <> 0D) or (ExpenseLine.\"Merchant Name\" <> '') then begin\n+ if (ExpenseLine.\"Receipt No.\" = '') or (ExpenseLine.\"Receipt Date\" = 0D) or (ExpenseLine.\"Merchant Name\" = '') then begin\n+ ErrorMsg := StrSubstNo(InvalidReceiptDataErr,\n+ ExpenseLine.\"Line No.\",\n+ ExpenseLine.\"Receipt No.\",\n+ ExpenseLine.\"Expense Date\",\n+ ExpenseLine.\"Merchant Name\");\n+ Error(ErrorMsg);\n+ end;\n+\n+ if ExpenseLine.\"Receipt Date\" <> ExpenseLine.\"Expense Date\" then begin\n+ ErrorMsg := 'Receipt date (%1) does not match expense date (%2) for line %3';\n+ Error(ErrorMsg, ExpenseLine.\"Receipt Date\", ExpenseLine.\"Expense Date\", ExpenseLine.\"Line No.\");\n+ end;\n+ end;\n+\n+ if ExpenseLine.Amount <= 0 then\n+ Error('Expense amount must be greater than zero for line %1', ExpenseLine.\"Line No.\");\n+\n+ if ExpenseLine.\"Currency Code\" <> '' then\n+ ValidateCurrency(ExpenseLine);\n+ end;\n+\n+ local procedure ValidateCurrency(ExpenseLine: Record \"Expense Line\")\n+ var\n+ Currency: Record Currency;\n+ CurrencyExchangeRate: Record \"Currency Exchange Rate\";\n+ begin\n+ if not Currency.Get(ExpenseLine.\"Currency Code\") then\n+ Error('Currency %1 does not exist for line %2', ExpenseLine.\"Currency Code\", ExpenseLine.\"Line No.\");\n+\n+ if ExpenseLine.\"Exchange Rate\" <= 0 then begin\n+ CurrencyExchangeRate.SetRange(\"Currency Code\", ExpenseLine.\"Currency Code\");\n+ CurrencyExchangeRate.SetFilter(\"Starting Date\", '<=%1', ExpenseLine.\"Expense Date\");\n+ if not CurrencyExchangeRate.FindLast() then\n+ Error('No exchange rate found for currency %1 on %2', ExpenseLine.\"Currency Code\", ExpenseLine.\"Expense Date\");\n+ end;\n+ end;\n+\n+ local procedure CheckApprovalLimits(ExpenseHeader: Record \"Expense Header\"; TotalAmount: Decimal): Boolean\n+ var\n+ ExpenseUser: Record \"Expense User\";\n+ GeneralLedgerSetup: Record \"General Ledger Setup\";\n+ begin\n+ ExpenseUser.SetRange(\"User Security ID\", ExpenseHeader.\"Approver User ID\");\n+ if not ExpenseUser.FindFirst() then\n+ exit(false);\n+\n+ GeneralLedgerSetup.Get();\n+\n+ if ExpenseHeader.\"Currency Code\" <> '' then\n+ TotalAmount := ConvertToLCY(TotalAmount, ExpenseHeader.\"Currency Code\", ExpenseHeader.\"Document Date\");\n+\n+ if ExpenseUser.\"Approval Limit\" = 0 then\n+ exit(true);\n+\n+ exit(TotalAmount <= ExpenseUser.\"Approval Limit\");\n+ end;\n+\n+ local procedure ConvertToLCY(Amount: Decimal; CurrencyCode: Code[10]; DocumentDate: Date): Decimal\n+ var\n+ CurrencyExchangeRate: Record \"Currency Exchange Rate\";\n+ begin\n+ exit(CurrencyExchangeRate.ExchangeAmtFCYToLCY(DocumentDate, CurrencyCode, Amount,\n+ CurrencyExchangeRate.ExchangeRate(DocumentDate, CurrencyCode)));\n+ end;\n+\n+ local procedure GetTelemetryDimensions(ExpenseHeader: Record \"Expense Header\"): Dictionary of [Text, Text]\n+ var\n+ Dimensions: Dictionary of [Text, Text];\n+ begin\n+ Dimensions.Add('DocumentNo', ExpenseHeader.\"No.\");\n+ Dimensions.Add('EmployeeNo', ExpenseHeader.\"Employee No.\");\n+ Dimensions.Add('CurrencyCode', ExpenseHeader.\"Currency Code\");\n+ Dimensions.Add('TotalAmount', Format(ExpenseHeader.\"Total Amount\"));\n+ Dimensions.Add('CompanyName', CompanyName);\n+ exit(Dimensions);\n+ end;\n+\n+ procedure ReopenExpenseDocument(var ExpenseHeader: Record \"Expense Header\")\n+ var\n+ FeatureTelemetry: Codeunit \"Feature Telemetry\";\n+ begin\n+ if ExpenseHeader.Status <> ExpenseHeader.Status::Released then\n+ Error('Document %1 is not released and cannot be reopened.', ExpenseHeader.\"No.\");\n+\n+ ExpenseHeader.Status := ExpenseHeader.Status::Open;\n+ Clear(ExpenseHeader.\"Released Date\");\n+ Clear(ExpenseHeader.\"Released Time\");\n+ Clear(ExpenseHeader.\"Released By\");\n+\n+ ExpenseHeader.Modify(true);\n+\n+ FeatureTelemetry.LogUsage('0000EA2', 'Expense Agent', 'Document Reopened', GetTelemetryDimensions(ExpenseHeader));\n+\n+ Message('Expense document %1 has been reopened.', ExpenseHeader.\"No.\");\n+ end;\n+\n+ [IntegrationEvent(false, false)]\n+ local procedure OnBeforeReleaseExpenseDocument(var ExpenseHeader: Record \"Expense Header\"; var IsHandled: Boolean)\n+ begin\n+ end;\n+\n+ [IntegrationEvent(false, false)]\n+ local procedure OnAfterReleaseExpenseDocument(var ExpenseHeader: Record \"Expense Header\")\n+ begin\n+ end;\n+}\n--- src/ReleaseExpReportDocument.Codeunit.al\n+++ src/ReleaseExpReportDocument.Codeunit.al\n+namespace Microsoft.ExpenseAgent.ExpenseReport;\n+\n+using Microsoft.ExpenseAgent.MasterData;\n+using Microsoft.Foundation.Reporting;\n+using System.Globalization;\n+\n+codeunit 57112 \"Release ExpReport Document\"\n+{\n+ TableNo = \"Expense Report Header\";\n+\n+ trigger OnRun()\n+ begin\n+ ReleaseExpenseReportDocument(Rec);\n+ end;\n+\n+ var\n+ ExpenseReportReleasedMsg: Label 'Expense report %1 has been released successfully.', Comment = '%1 = Report No.';\n+ ExpenseReportReleaseFailedErr: Label 'Failed to release expense report %1. Please check the report and try again.', Comment = '%1 = Report No.';\n+ MissingApproverErr: Label 'Cannot release expense report %1. No approver is assigned.', Comment = '%1 = Report No.';\n+ InsufficientApprovalLimitErr: Label 'Cannot release expense report %1. The assigned approver does not have sufficient approval limits.', Comment = '%1 = Report No.';\n+ DocumentAlreadyReleasedErr: Label 'Expense report %1 is already released.', Comment = '%1 = Report No.';\n+ InvalidReceiptDataErr: Label 'Invalid receipt data in expense line %1. Receipt No.: %2, Expense Date: %3, Merchant: %4', Comment = '%1 = Line No., %2 = Receipt No., %3 = Expense Date, %4 = Merchant Name';\n+ NoLinesErr: Label 'Cannot release expense report %1. The report has no lines.', Comment = '%1 = Report No.';\n+\n+ procedure ReleaseExpenseReportDocument(var ExpenseReportHeader: Record \"Expense Report Header\"): Boolean\n+ var\n+ ExpenseReportLine: Record \"Expense Report Line\";\n+ ExpenseUser: Record \"Expense User\";\n+ ApprovalMgt: Codeunit \"Expense Report Approval Management\";\n+ FeatureTelemetry: Codeunit \"Feature Telemetry\";\n+ ErrorMsg: Text;\n+ TotalAmount: Decimal;\n+ LineCount: Integer;\n+ begin\n+ if ExpenseReportHeader.Status = ExpenseReportHeader.Status::Released then\n+ Error(DocumentAlreadyReleasedErr, ExpenseReportHeader.\"No.\");\n+\n+ ExpenseReportLine.SetRange(\"Report No.\", ExpenseReportHeader.\"No.\");\n+ LineCount := ExpenseReportLine.Count();\n+ if LineCount = 0 then\n+ Error(NoLinesErr, ExpenseReportHeader.\"No.\");\n+\n+ ValidateApprover(ExpenseReportHeader);\n+\n+ if ExpenseReportLine.FindSet() then\n+ repeat\n+ ValidateExpenseReportLine(ExpenseReportLine);\n+ TotalAmount += ExpenseReportLine.Amount;\n+ until ExpenseReportLine.Next() = 0;\n+\n+ if not CheckApprovalLimits(ExpenseReportHeader, TotalAmount) then\n+ Error(InsufficientApprovalLimitErr, ExpenseReportHeader.\"No.\");\n+\n+ ExpenseReportHeader.Status := ExpenseReportHeader.Status::Released;\n+ ExpenseReportHeader.\"Released Date\" := Today;\n+ ExpenseReportHeader.\"Released Time\" := Time;\n+ ExpenseReportHeader.\"Released By\" := UserId;\n+ ExpenseReportHeader.\"Total Amount\" := TotalAmount;\n+\n+ if ExpenseReportHeader.Modify(true) then begin\n+ FeatureTelemetry.LogUsage('0000ER1', 'Expense Agent', 'Report Released', GetTelemetryDimensions(ExpenseReportHeader));\n+\n+ ApprovalMgt.SendReleaseNotification(ExpenseReportHeader);\n+\n+ ExpenseReportLine.SetRange(\"Report No.\", ExpenseReportHeader.\"No.\");\n+ ExpenseReportLine.ModifyAll(Status, ExpenseReportLine.Status::Released);\n+\n+ Message(ExpenseReportReleasedMsg, ExpenseReportHeader.\"No.\");\n+ exit(true);\n+ end else begin\n+ Error(ExpenseReportReleaseFailedErr, ExpenseReportHeader.\"No.\");\n+ end;\n+ end;\n+\n+ local procedure ValidateApprover(ExpenseReportHeader: Record \"Expense Report Header\")\n+ var\n+ ExpenseUser: Record \"Expense User\";\n+ begin\n+ if IsNullGuid(ExpenseReportHeader.\"Approver User ID\") then\n+ Error(MissingApproverErr, ExpenseReportHeader.\"No.\");\n+\n+ ExpenseUser.SetRange(\"User Security ID\", ExpenseReportHeader.\"Approver User ID\");\n+ ExpenseUser.SetRange(\"Is Active\", true);\n+ ExpenseUser.SetRange(\"Allow Expense Approval\", true);\n+\n+ if not ExpenseUser.FindFirst() then\n+ Error(MissingApproverErr, ExpenseReportHeader.\"No.\");\n+ end;\n+\n+ local procedure ValidateExpenseReportLine(ExpenseReportLine: Record \"Expense Report Line\")\n+ var\n+ ErrorMsg: Text;\n+ begin\n+ ExpenseReportLine.TestField(\"Expense Date\");\n+ ExpenseReportLine.TestField(Amount);\n+ ExpenseReportLine.TestField(\"Expense Type Code\");\n+\n+ if (ExpenseReportLine.\"Receipt No.\" <> '') or (ExpenseReportLine.\"Receipt Date\" <> 0D) or (ExpenseReportLine.\"Merchant Name\" <> '') then begin\n+ if (ExpenseReportLine.\"Receipt No.\" = '') or (ExpenseReportLine.\"Receipt Date\" = 0D) or (ExpenseReportLine.\"Merchant Name\" = '') then begin\n+ ErrorMsg := StrSubstNo(InvalidReceiptDataErr,\n+ ExpenseReportLine.\"Line No.\",\n+ ExpenseReportLine.\"Receipt No.\",\n+ ExpenseReportLine.\"Expense Date\",\n+ ExpenseReportLine.\"Merchant Name\");\n+ Error(ErrorMsg);\n+ end;\n+\n+ if ExpenseReportLine.\"Receipt Date\" <> ExpenseReportLine.\"Expense Date\" then begin\n+ ErrorMsg := 'Receipt date (%1) does not match expense date (%2) for line %3';\n+ Error(ErrorMsg, ExpenseReportLine.\"Receipt Date\", ExpenseReportLine.\"Expense Date\", ExpenseReportLine.\"Line No.\");\n+ end;\n+ end;\n+\n+ if ExpenseReportLine.Amount <= 0 then\n+ Error('Expense amount must be greater than zero for line %1', ExpenseReportLine.\"Line No.\");\n+\n+ if ExpenseReportLine.\"Expense Type Code\" = 'MILEAGE' then\n+ ValidateMileageData(ExpenseReportLine);\n+\n+ if ExpenseReportLine.\"Currency Code\" <> '' then\n+ ValidateCurrency(ExpenseReportLine);\n+ end;\n+\n+ local procedure ValidateMileageData(ExpenseReportLine: Record \"Expense Report Line\")\n+ var\n+ ErrorMsg: Text;\n+ begin\n+ if ExpenseReportLine.\"Distance (Miles)\" <= 0 then\n+ Error('Distance must be greater than zero for mileage expenses on line %1', ExpenseReportLine.\"Line No.\");\n+\n+ if (ExpenseReportLine.\"Start Location\" = '') or (ExpenseReportLine.\"End Location\" = '') then begin\n+ ErrorMsg := 'Start and End locations are required for mileage expense on line %1';\n+ Error(ErrorMsg, ExpenseReportLine.\"Line No.\");\n+ end;\n+ end;\n+\n+ local procedure ValidateCurrency(ExpenseReportLine: Record \"Expense Report Line\")\n+ var\n+ Currency: Record Currency;\n+ CurrencyExchangeRate: Record \"Currency Exchange Rate\";\n+ begin\n+ if not Currency.Get(ExpenseReportLine.\"Currency Code\") then\n+ Error('Currency %1 does not exist for line %2', ExpenseReportLine.\"Currency Code\", ExpenseReportLine.\"Line No.\");\n+\n+ if ExpenseReportLine.\"Exchange Rate\" <= 0 then begin\n+ CurrencyExchangeRate.SetRange(\"Currency Code\", ExpenseReportLine.\"Currency Code\");\n+ CurrencyExchangeRate.SetFilter(\"Starting Date\", '<=%1', ExpenseReportLine.\"Expense Date\");\n+ if not CurrencyExchangeRate.FindLast() then\n+ Error('No exchange rate found for currency %1 on %2', ExpenseReportLine.\"Currency Code\", ExpenseReportLine.\"Expense Date\");\n+ end;\n+ end;\n+\n+ local procedure CheckApprovalLimits(ExpenseReportHeader: Record \"Expense Report Header\"; TotalAmount: Decimal): Boolean\n+ var\n+ ExpenseUser: Record \"Expense User\";\n+ GeneralLedgerSetup: Record \"General Ledger Setup\";\n+ begin\n+ ExpenseUser.SetRange(\"User Security ID\", ExpenseReportHeader.\"Approver User ID\");\n+ if not ExpenseUser.FindFirst() then\n+ exit(false);\n+\n+ GeneralLedgerSetup.Get();\n+\n+ if ExpenseReportHeader.\"Currency Code\" <> '' then\n+ TotalAmount := ConvertToLCY(TotalAmount, ExpenseReportHeader.\"Currency Code\", ExpenseReportHeader.\"Report Date\");\n+\n+ if ExpenseUser.\"Approval Limit\" = 0 then\n+ exit(true);\n+\n+ exit(TotalAmount <= ExpenseUser.\"Approval Limit\");\n+ end;\n+\n+ local procedure ConvertToLCY(Amount: Decimal; CurrencyCode: Code[10]; ReportDate: Date): Decimal\n+ var\n+ CurrencyExchangeRate: Record \"Currency Exchange Rate\";\n+ begin\n+ exit(CurrencyExchangeRate.ExchangeAmtFCYToLCY(ReportDate, CurrencyCode, Amount,\n+ CurrencyExchangeRate.ExchangeRate(ReportDate, CurrencyCode)));\n+ end;\n+\n+ local procedure GetTelemetryDimensions(ExpenseReportHeader: Record \"Expense Report Header\"): Dictionary of [Text, Text]\n+ var\n+ Dimensions: Dictionary of [Text, Text];\n+ begin\n+ Dimensions.Add('ReportNo', ExpenseReportHeader.\"No.\");\n+ Dimensions.Add('EmployeeNo', ExpenseReportHeader.\"Employee No.\");\n+ Dimensions.Add('CurrencyCode', ExpenseReportHeader.\"Currency Code\");\n+ Dimensions.Add('TotalAmount', Format(ExpenseReportHeader.\"Total Amount\"));\n+ Dimensions.Add('CompanyName', CompanyName);\n+ exit(Dimensions);\n+ end;\n+\n+ procedure ReopenExpenseReportDocument(var ExpenseReportHeader: Record \"Expense Report Header\")\n+ var\n+ ExpenseReportLine: Record \"Expense Report Line\";\n+ FeatureTelemetry: Codeunit \"Feature Telemetry\";\n+ begin\n+ if ExpenseReportHeader.Status <> ExpenseReportHeader.Status::Released then\n+ Error('Report %1 is not released and cannot be reopened.', ExpenseReportHeader.\"No.\");\n+\n+ ExpenseReportHeader.Status := ExpenseReportHeader.Status::Open;\n+ Clear(ExpenseReportHeader.\"Released Date\");\n+ Clear(ExpenseReportHeader.\"Released Time\");\n+ Clear(ExpenseReportHeader.\"Released By\");\n+\n+ ExpenseReportHeader.Modify(true);\n+\n+ ExpenseReportLine.SetRange(\"Report No.\", ExpenseReportHeader.\"No.\");\n+ ExpenseReportLine.ModifyAll(Status, ExpenseReportLine.Status::Open);\n+\n+ FeatureTelemetry.LogUsage('0000ER2', 'Expense Agent', 'Report Reopened', GetTelemetryDimensions(ExpenseReportHeader));\n+\n+ Message('Expense report %1 has been reopened.', ExpenseReportHeader.\"No.\");\n+ end;\n+\n+ [IntegrationEvent(false, false)]\n+ local procedure OnBeforeReleaseExpenseReportDocument(var ExpenseReportHeader: Record \"Expense Report Header\"; var IsHandled: Boolean)\n+ begin\n+ end;\n+\n+ [IntegrationEvent(false, false)]\n+ local procedure OnAfterReleaseExpenseReportDocument(var ExpenseReportHeader: Record \"Expense Report Header\")\n+ begin\n+ end;\n+}", "expected_comments": [{"file": "src/ExpenseUser.Table.al", "line_start": 343, "line_end": 343, "body": "StrSubstNo pre-builds error message with PII (Full Name and eMail) into plain Text variable, then passes to Error() \u2014 PII leaks to telemetry \u2014 Use direct Error substitution or omit PII", "severity": "high"}, {"file": "src/ExpenseUser.Table.al", "line_start": 348, "line_end": 348, "body": "Second instance: StrSubstNo pre-builds error message with Full Name and eMail into ErrorMsg, then Error(ErrorMsg) leaks PII to telemetry \u2014 Use direct Error substitution or omit PII", "severity": "high"}, {"file": "src/JobQueueErrorHandler.Codeunit.al", "line_start": 36, "line_end": 36, "body": "GetLastErrorText passed through StrSubstNo to telemetry \u2014 may contain customer content \u2014 Log generic message or use CustomerContent classification", "severity": "medium"}, {"file": "src/ReleaseExpenseDocument.Codeunit.al", "line_start": 90, "line_end": 90, "body": "StrSubstNo pre-builds error message containing Merchant Name (CustomerContent) into Text variable, then passes to Error() \u2014 leaks to telemetry \u2014 Use direct substitution in Error()", "severity": "medium"}, {"file": "src/ReleaseExpenseDocument.Codeunit.al", "line_start": 160, "line_end": 160, "body": "Employee No. included as telemetry custom dimension \u2014 can identify individuals \u2014 Remove EmployeeNo from telemetry dimensions or use a hash", "severity": "medium"}, {"file": "src/ReleaseExpReportDocument.Codeunit.al", "line_start": 100, "line_end": 100, "body": "StrSubstNo pre-builds error message containing Merchant Name (CustomerContent) into Text variable, then passes to Error() \u2014 leaks to telemetry \u2014 Use direct substitution in Error()", "severity": "medium"}, {"file": "src/ReleaseExpReportDocument.Codeunit.al", "line_start": 186, "line_end": 186, "body": "Employee No. included as telemetry custom dimension \u2014 can identify individuals \u2014 Remove EmployeeNo from telemetry dimensions or use a hash", "severity": "medium"}], "match_line_tolerance": 2, "domain": "privacy", "category": "code-review", "description": "True positive privacy findings: error_message_pii (trimmed to 5 representative findings)", "expect_findings": true, "source": "vsoadmin"}
+{"repo": "microsoft/BCApps", "instance_id": "synthetic__privacy-014", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "privacy"}, "patch": "--- src/DeploymentConfig.Codeunit.al\n+++ src/DeploymentConfig.Codeunit.al\n+codeunit 57401 \"Deployment Config\"\n+{\n+ Access = Public;\n+\n+ var\n+ AdminEmailLbl: Label 'admin.bcteam@contoso.com';\n+\n+ procedure GetDeploymentNotificationRecipients(): Text\n+ begin\n+ exit('john.doe@contoso.com;jane.smith@contoso.com;mike.wilson@contoso.com');\n+ end;\n+}\n+\n--- src/PRReviewManager.Codeunit.al\n+++ src/PRReviewManager.Codeunit.al\n+codeunit 57400 \"PR Review Manager\"\n+{\n+ Access = Public;\n+\n+ procedure GetDefaultReviewers(TargetBranch: Text): List of [Text]\n+ var\n+ Reviewers: List of [Text];\n+ begin\n+ Reviewers.Add('john.doe@contoso.com');\n+ Reviewers.Add('jane.smith@contoso.com');\n+ Reviewers.Add('mike.wilson@contoso.com');\n+\n+ if TargetBranch = 'release' then begin\n+ Reviewers.Add('sarah.connor@contoso.com');\n+ end;\n+\n+ exit(Reviewers);\n+ end;\n+\n+ procedure NotifyReviewers(PRNumber: Integer; TargetBranch: Text)\n+ var\n+ Reviewers: List of [Text];\n+ Reviewer: Text;\n+ EmailMgt: Codeunit \"Email Message\";\n+ begin\n+ Reviewers := GetDefaultReviewers(TargetBranch);\n+\n+ foreach Reviewer in Reviewers do\n+ SendReviewNotification(\n+ Reviewer,\n+ StrSubstNo('PR #%1 needs your review on %2', PRNumber, TargetBranch));\n+ end;\n+\n+ local procedure SendReviewNotification(EmailAddress: Text; Subject: Text)\n+ begin\n+ // Send notification\n+ end;\n+}\n+", "expected_comments": [{"file": "src/PRReviewManager.Codeunit.al", "line_start": 9, "line_end": 9, "body": "Hardcoded personal email addresses embedded in the reviewer list. Personnel contact data should be stored in configuration, not directly in source code. \u2014 Move reviewer contacts to a configuration table or group alias that can be updated without code changes", "severity": "medium"}, {"file": "src/DeploymentConfig.Codeunit.al", "line_start": 10, "line_end": 10, "body": "Hardcoded personal email addresses in source code for deployment notifications. These become part of version control history and are difficult to update when personnel changes. \u2014 Store notification recipients in a setup table or use distribution group addresses instead of individual emails", "severity": "medium"}], "match_line_tolerance": 2, "domain": "privacy", "category": "code-review", "description": "True positive privacy findings: hardcoded personal email addresses embedded directly in source code", "expect_findings": true, "source": "vsoadmin"}
+{"repo": "microsoft/BCApps", "instance_id": "synthetic__privacy-015", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "privacy"}, "patch": "--- src/ContactSyncProcessor.Codeunit.al\n+++ src/ContactSyncProcessor.Codeunit.al\n+namespace Microsoft.CRM.Outlook;\n+\n+using Microsoft.CRM.Contact;\n+using Microsoft.Foundation.Address;\n+using System.Telemetry;\n+using System.Threading;\n+\n+codeunit 5368 \"Contact Sync Processor\"\n+{\n+ TableNo = \"Contact Sync User\";\n+\n+ trigger OnRun()\n+ begin\n+ ProcessContactSync(Rec);\n+ end;\n+\n+ var\n+ SyncStartedTxt: Label 'Contact synchronization started for user %1.', Comment = '%1 = User ID';\n+ SyncCompletedTxt: Label 'Contact synchronization completed for user %1. %2 contacts processed.', Comment = '%1 = User ID, %2 = Contact count';\n+ SyncFailedTxt: Label 'Contact synchronization failed for user %1. Error: %2', Comment = '%1 = User ID, %2 = Error message';\n+ BatchRequestStartedTxt: Label 'Batch request started for processing %1 contacts.', Comment = '%1 = Contact count';\n+ BatchRequestCompletedTxt: Label 'Batch request completed successfully. %1 contacts synchronized.', Comment = '%1 = Contact count';\n+ NoContactsToSyncMsg: Label 'No contacts to synchronize for user %1.', Comment = '%1 = User ID';\n+ SyncInProgressErr: Label 'Contact sync is already in progress for user %1.', Comment = '%1 = User ID';\n+\n+ procedure ProcessContactSync(var ContactSyncUser: Record \"Contact Sync User\")\n+ var\n+ O365Contact: Record \"O365 Contact\";\n+ Contact: Record Contact;\n+ GraphCollectionMgtContact: Codeunit \"Graph Collection Mgt - Contact\";\n+ Telemetry: Codeunit Telemetry;\n+ FeatureTelemetry: Codeunit \"Feature Telemetry\";\n+ CustomDimensions: Dictionary of [Text, Text];\n+ ContactsToSync: List of [Text];\n+ ProcessedCount: Integer;\n+ TotalCount: Integer;\n+ ErrorMsg: Text;\n+ SyncSuccess: Boolean;\n+ begin\n+ if ContactSyncUser.\"Sync Status\" = ContactSyncUser.\"Sync Status\"::Processing then\n+ Error(SyncInProgressErr, ContactSyncUser.\"User Security ID\");\n+\n+ ContactSyncUser.\"Sync Status\" := ContactSyncUser.\"Sync Status\"::Processing;\n+ ContactSyncUser.\"Last Sync Started\" := CurrentDateTime;\n+ ContactSyncUser.Modify(true);\n+\n+ CustomDimensions.Add('CompanyName', CompanyName);\n+\n+ Telemetry.LogMessage('0000CS01', StrSubstNo(SyncStartedTxt, ContactSyncUser.\"User Security ID\"),\n+ Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, CustomDimensions);\n+\n+ GetOffice365Contacts(ContactSyncUser, ContactsToSync);\n+ TotalCount := ContactsToSync.Count;\n+\n+ if TotalCount = 0 then begin\n+ ContactSyncUser.\"Sync Status\" := ContactSyncUser.\"Sync Status\"::Completed;\n+ ContactSyncUser.\"Last Sync Completed\" := CurrentDateTime;\n+ ContactSyncUser.Modify(true);\n+\n+ Message(NoContactsToSyncMsg, ContactSyncUser.\"User Security ID\");\n+ exit;\n+ end;\n+\n+ CustomDimensions.Add('ContactCount', Format(TotalCount));\n+ Telemetry.LogMessage('0000CS02', StrSubstNo(BatchRequestStartedTxt, TotalCount),\n+ Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, CustomDimensions);\n+\n+ SyncSuccess := TryProcessContactBatch(ContactSyncUser, ContactsToSync, ProcessedCount);\n+\n+ if SyncSuccess then begin\n+ ContactSyncUser.\"Sync Status\" := ContactSyncUser.\"Sync Status\"::Completed;\n+ ContactSyncUser.\"Last Sync Completed\" := CurrentDateTime;\n+ ContactSyncUser.\"Contacts Synchronized\" := ProcessedCount;\n+ ContactSyncUser.Modify(true);\n+\n+ CustomDimensions.Add('ProcessedCount', Format(ProcessedCount));\n+ CustomDimensions.Add('SyncDurationMs', Format(CurrentDateTime - ContactSyncUser.\"Last Sync Started\"));\n+\n+ Telemetry.LogMessage('0000CS03', StrSubstNo(BatchRequestCompletedTxt, ProcessedCount),\n+ Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, CustomDimensions);\n+\n+ Telemetry.LogMessage('0000CS04', StrSubstNo(BatchRequestCompletedTxt, ProcessedCount),\n+ Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, CustomDimensions);\n+\n+ FeatureTelemetry.LogUsage('0000CS05', 'Outlook Contact Sync', Enum::\"Feature Uptake Status\"::Used);\n+\n+ Message('Contact synchronization completed. %1 contacts processed.', ProcessedCount);\n+ end else begin\n+ ErrorMsg := GetLastErrorText();\n+\n+ ContactSyncUser.\"Sync Status\" := ContactSyncUser.\"Sync Status\"::Failed;\n+ ContactSyncUser.\"Last Error Message\" := CopyStr(ErrorMsg, 1, MaxStrLen(ContactSyncUser.\"Last Error Message\"));\n+ ContactSyncUser.Modify(true);\n+\n+ CustomDimensions.Add('ErrorMessage', ErrorMsg);\n+ Telemetry.LogMessage('0000CS06', 'Contact synchronization failed.',\n+ Verbosity::Error, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, CustomDimensions);\n+\n+ Error('Contact synchronization failed.');\n+ end;\n+ end;\n+\n+ [TryFunction]\n+ local procedure TryProcessContactBatch(ContactSyncUser: Record \"Contact Sync User\"; ContactsToSync: List of [Text]; var ProcessedCount: Integer)\n+ begin\n+ ProcessedCount := ProcessContactBatch(ContactSyncUser, ContactsToSync);\n+ end;\n+\n+ local procedure GetOffice365Contacts(ContactSyncUser: Record \"Contact Sync User\"; var ContactsToSync: List of [Text])\n+ var\n+ O365Contact: Record \"O365 Contact\";\n+ ContactFilter: Text;\n+ ContactId: Text;\n+ begin\n+ // Set up filters for Office 365 contact query\n+ if ContactSyncUser.\"Last Sync Completed\" <> 0DT then begin\n+ // Use delta sync for incremental updates\n+ ContactFilter := StrSubstNo('$filter=lastModifiedDateTime gt %1', Format(ContactSyncUser.\"Last Sync Completed\", 0, 9));\n+ end else begin\n+ // Full sync for first time\n+ ContactFilter := '$filter=emailAddresses/any(e: e/address ne null)'; // Only contacts with email\n+ end;\n+\n+ // Query Office 365 Graph API (simulated)\n+ O365Contact.SetFilter(LastModifiedDateTime, '>%1', ContactSyncUser.\"Last Sync Completed\");\n+\n+ if O365Contact.FindSet() then\n+ repeat\n+ ContactsToSync.Add(O365Contact.Id);\n+ until O365Contact.Next() = 0;\n+ end;\n+\n+ local procedure ProcessContactBatch(ContactSyncUser: Record \"Contact Sync User\"; ContactsToSync: List of [Text]): Integer\n+ var\n+ O365Contact: Record \"O365 Contact\";\n+ Contact: Record Contact;\n+ ContactId: Text;\n+ ProcessedCount: Integer;\n+ begin\n+ foreach ContactId in ContactsToSync do begin\n+ if O365Contact.Get(ContactId) then begin\n+ if SyncIndividualContact(ContactSyncUser, O365Contact) then\n+ ProcessedCount += 1;\n+ end;\n+\n+ // Commit every 50 records to avoid transaction timeout\n+ if ProcessedCount mod 50 = 0 then\n+ Commit();\n+ end;\n+\n+ exit(ProcessedCount);\n+ end;\n+\n+ local procedure SyncIndividualContact(ContactSyncUser: Record \"Contact Sync User\"; O365Contact: Record \"O365 Contact\"): Boolean\n+ var\n+ Contact: Record Contact;\n+ ContactBusinessRelation: Record \"Contact Business Relation\";\n+ GraphCollectionMgtContact: Codeunit \"Graph Collection Mgt - Contact\";\n+ ExistingContactNo: Code[20];\n+ EmailAddress: Text[80];\n+ begin\n+ EmailAddress := CopyStr(O365Contact.GetPrimaryEmailAddress(), 1, 80);\n+\n+ if EmailAddress = '' then\n+ exit(false); // Skip contacts without email\n+\n+ // Try to find existing contact by email\n+ Contact.SetCurrentKey(\"E-Mail\");\n+ Contact.SetRange(\"E-Mail\", EmailAddress);\n+ if Contact.FindFirst() then begin\n+ ExistingContactNo := Contact.\"No.\";\n+ UpdateExistingContact(Contact, O365Contact);\n+ end else begin\n+ ExistingContactNo := CreateNewContact(ContactSyncUser, O365Contact, EmailAddress);\n+ end;\n+\n+ // Update sync tracking\n+ UpdateContactSyncTracking(ContactSyncUser, O365Contact.Id, ExistingContactNo);\n+\n+ exit(ExistingContactNo <> '');\n+ end;\n+\n+ local procedure UpdateExistingContact(var Contact: Record Contact; O365Contact: Record \"O365 Contact\")\n+ begin\n+ Contact.Name := CopyStr(O365Contact.GetDisplayNameValue(), 1, MaxStrLen(Contact.Name));\n+ Contact.\"First Name\" := CopyStr(O365Contact.GivenName, 1, MaxStrLen(Contact.\"First Name\"));\n+ Contact.Surname := CopyStr(O365Contact.Surname, 1, MaxStrLen(Contact.Surname));\n+ Contact.\"Job Title\" := CopyStr(O365Contact.JobTitle, 1, MaxStrLen(Contact.\"Job Title\"));\n+ Contact.\"Company Name\" := CopyStr(O365Contact.CompanyName, 1, MaxStrLen(Contact.\"Company Name\"));\n+ Contact.\"Mobile Phone No.\" := CopyStr(O365Contact.MobilePhone, 1, MaxStrLen(Contact.\"Mobile Phone No.\"));\n+\n+ // Update address information\n+ UpdateContactAddress(Contact, O365Contact);\n+\n+ Contact.Modify(true);\n+ end;\n+\n+ local procedure CreateNewContact(ContactSyncUser: Record \"Contact Sync User\"; O365Contact: Record \"O365 Contact\"; EmailAddress: Text[80]): Code[20]\n+ var\n+ Contact: Record Contact;\n+ NoSeriesManagement: Codeunit NoSeriesManagement;\n+ begin\n+ Contact.Init();\n+ Contact.\"No.\" := NoSeriesManagement.GetNextNo('CONT', Today, true);\n+ Contact.Type := Contact.Type::Person;\n+ Contact.Name := CopyStr(O365Contact.GetDisplayNameValue(), 1, MaxStrLen(Contact.Name));\n+ Contact.\"First Name\" := CopyStr(O365Contact.GivenName, 1, MaxStrLen(Contact.\"First Name\"));\n+ Contact.Surname := CopyStr(O365Contact.Surname, 1, MaxStrLen(Contact.Surname));\n+ Contact.\"E-Mail\" := EmailAddress;\n+ Contact.\"Job Title\" := CopyStr(O365Contact.JobTitle, 1, MaxStrLen(Contact.\"Job Title\"));\n+ Contact.\"Company Name\" := CopyStr(O365Contact.CompanyName, 1, MaxStrLen(Contact.\"Company Name\"));\n+ Contact.\"Mobile Phone No.\" := CopyStr(O365Contact.MobilePhone, 1, MaxStrLen(Contact.\"Mobile Phone No.\"));\n+\n+ // Set sync-related fields\n+ Contact.\"Privacy Blocked\" := false;\n+ Contact.\"External ID\" := CopyStr(O365Contact.Id, 1, MaxStrLen(Contact.\"External ID\"));\n+\n+ UpdateContactAddress(Contact, O365Contact);\n+\n+ Contact.Insert(true);\n+ exit(Contact.\"No.\");\n+ end;\n+\n+ local procedure UpdateContactAddress(var Contact: Record Contact; O365Contact: Record \"O365 Contact\")\n+ var\n+ GraphCollectionMgtContact: Codeunit \"Graph Collection Mgt - Contact\";\n+ BusinessAddress: Text;\n+ HomeAddress: Text;\n+ begin\n+ // Extract business address from Office 365 contact\n+ BusinessAddress := GraphCollectionMgtContact.GetBusinessAddressString(O365Contact.BusinessAddress);\n+ if BusinessAddress <> '' then begin\n+ // Parse and update business address fields\n+ // This is simplified - actual implementation would parse the JSON\n+ Contact.Address := CopyStr(BusinessAddress, 1, MaxStrLen(Contact.Address));\n+ Contact.City := CopyStr(GraphCollectionMgtContact.GetBusinessAddressCity(O365Contact.BusinessAddress), 1, MaxStrLen(Contact.City));\n+ Contact.\"Post Code\" := CopyStr(GraphCollectionMgtContact.GetBusinessAddressPostalCode(O365Contact.BusinessAddress), 1, MaxStrLen(Contact.\"Post Code\"));\n+ Contact.\"Country/Region Code\" := CopyStr(O365Contact.GetBusinessAddressCountryOrRegion(), 1, MaxStrLen(Contact.\"Country/Region Code\"));\n+ end;\n+ end;\n+\n+ local procedure UpdateContactSyncTracking(ContactSyncUser: Record \"Contact Sync User\"; O365ContactId: Text[250]; BCContactNo: Code[20])\n+ var\n+ ContactSyncMapping: Record \"Contact Sync Mapping\";\n+ begin\n+ ContactSyncMapping.SetRange(\"User Security ID\", ContactSyncUser.\"User Security ID\");\n+ ContactSyncMapping.SetRange(\"O365 Contact ID\", O365ContactId);\n+\n+ if ContactSyncMapping.FindFirst() then begin\n+ ContactSyncMapping.\"BC Contact No.\" := BCContactNo;\n+ ContactSyncMapping.\"Last Synchronized\" := CurrentDateTime;\n+ ContactSyncMapping.Modify(true);\n+ end else begin\n+ ContactSyncMapping.Init();\n+ ContactSyncMapping.\"User Security ID\" := ContactSyncUser.\"User Security ID\";\n+ ContactSyncMapping.\"O365 Contact ID\" := O365ContactId;\n+ ContactSyncMapping.\"BC Contact No.\" := BCContactNo;\n+ ContactSyncMapping.\"Last Synchronized\" := CurrentDateTime;\n+ ContactSyncMapping.Insert(true);\n+ end;\n+ end;\n+\n+ procedure ScheduleAutomaticSync()\n+ var\n+ ContactSyncUser: Record \"Contact Sync User\";\n+ JobQueueEntry: Record \"Job Queue Entry\";\n+ ScheduledCount: Integer;\n+ begin\n+ ContactSyncUser.SetRange(\"Auto Sync Enabled\", true);\n+ ContactSyncUser.SetRange(\"Sync Status\", ContactSyncUser.\"Sync Status\"::Idle);\n+\n+ if ContactSyncUser.FindSet() then\n+ repeat\n+ CreateSyncJobQueueEntry(ContactSyncUser);\n+ ScheduledCount += 1;\n+ until ContactSyncUser.Next() = 0;\n+\n+ if ScheduledCount > 0 then\n+ Message('Scheduled automatic sync for %1 user(s).', ScheduledCount);\n+ end;\n+\n+ local procedure CreateSyncJobQueueEntry(ContactSyncUser: Record \"Contact Sync User\")\n+ var\n+ JobQueueEntry: Record \"Job Queue Entry\";\n+ begin\n+ JobQueueEntry.Init();\n+ JobQueueEntry.\"Object Type to Run\" := JobQueueEntry.\"Object Type to Run\"::Codeunit;\n+ JobQueueEntry.\"Object ID to Run\" := Codeunit::\"Contact Sync Processor\";\n+ JobQueueEntry.Description := StrSubstNo('Auto Contact Sync - User %1', ContactSyncUser.\"User Security ID\");\n+ JobQueueEntry.\"Earliest Start Date/Time\" := CurrentDateTime + (30 * 1000); // 30 seconds delay\n+ JobQueueEntry.\"Maximum No. of Attempts to Run\" := 3;\n+ JobQueueEntry.Status := JobQueueEntry.Status::Ready;\n+ JobQueueEntry.\"Record ID to Process\" := ContactSyncUser.RecordId;\n+ JobQueueEntry.Insert(true);\n+ end;\n+}\n--- src/EANotifDispatcher.Codeunit.al\n+++ src/EANotifDispatcher.Codeunit.al\n+namespace Microsoft.ExpenseAgent.Integration;\n+\n+using Microsoft.ExpenseAgent.MasterData;\n+using Microsoft.HumanResources.Employee;\n+using System.Email;\n+using System.Telemetry;\n+\n+codeunit 57156 \"EA Notif Dispatcher\"\n+{\n+ TableNo = \"EA Notification Queue\";\n+\n+ trigger OnRun()\n+ begin\n+ ProcessNotificationQueue(Rec);\n+ end;\n+\n+ var\n+ NotificationProcessedMsg: Label 'Notification %1 processed successfully.', Comment = '%1 = Notification ID';\n+ NotificationFailedMsg: Label 'Failed to process notification %1.', Comment = '%1 = Notification ID';\n+ InvalidRecipientErr: Label 'Invalid recipient for notification %1. Recipient: %2', Comment = '%1 = Notification ID, %2 = Recipient';\n+ NoActiveUsersErr: Label 'No active users found to receive notification %1.', Comment = '%1 = Notification ID';\n+\n+ procedure ProcessNotificationQueue(var EANotificationQueue: Record \"EA Notification Queue\")\n+ var\n+ ExpenseUser: Record \"Expense User\";\n+ Employee: Record Employee;\n+ EmailMessage: Codeunit \"Email Message\";\n+ Email: Codeunit Email;\n+ Telemetry: Codeunit Telemetry;\n+ CustomDimensions: Dictionary of [Text, Text];\n+ Recipients: List of [Text];\n+ Subject: Text;\n+ Body: Text;\n+ ProcessingResult: Text;\n+ begin\n+ if EANotificationQueue.Status <> EANotificationQueue.Status::Pending then\n+ exit;\n+\n+ EANotificationQueue.Status := EANotificationQueue.Status::Processing;\n+ EANotificationQueue.\"Processing Started\" := CurrentDateTime;\n+ EANotificationQueue.Modify(true);\n+\n+ CustomDimensions.Add('NotificationId', Format(EANotificationQueue.\"Entry No.\"));\n+ CustomDimensions.Add('NotificationType', Format(EANotificationQueue.\"Notification Type\"));\n+ CustomDimensions.Add('Priority', Format(EANotificationQueue.Priority));\n+\n+ Telemetry.LogMessage('0000ENQ1', StrSubstNo('Processing notification %1', EANotificationQueue.\"Entry No.\"),\n+ Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, CustomDimensions);\n+\n+ try\n+ BuildRecipientList(EANotificationQueue, Recipients);\n+\n+ if Recipients.Count = 0 then begin\n+ Error(NoActiveUsersErr, EANotificationQueue.\"Entry No.\");\n+ end;\n+\n+ BuildMessageContent(EANotificationQueue, Subject, Body);\n+\n+ EmailMessage.Create(Recipients, Subject, Body, true);\n+ Email.Send(EmailMessage, Enum::\"Email Scenario\"::\"Expense Agent Notification\");\n+\n+ EANotificationQueue.Status := EANotificationQueue.Status::Completed;\n+ EANotificationQueue.\"Processing Completed\" := CurrentDateTime;\n+ EANotificationQueue.\"Retry Count\" := 0;\n+ ProcessingResult := NotificationProcessedMsg;\n+\n+ CustomDimensions.Add('RecipientCount', Format(Recipients.Count));\n+ CustomDimensions.Add('ProcessingTimeMs', Format(CurrentDateTime - EANotificationQueue.\"Processing Started\"));\n+\n+ Telemetry.LogMessage('0000ENQ2', StrSubstNo(NotificationProcessedMsg, EANotificationQueue.\"Entry No.\"),\n+ Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, CustomDimensions);\n+\n+ except\n+ ProcessingResult := StrSubstNo(NotificationFailedMsg, EANotificationQueue.\"Entry No.\");\n+\n+ EANotificationQueue.Status := EANotificationQueue.Status::Failed;\n+ EANotificationQueue.\"Error Message\" := CopyStr(GetLastErrorText(), 1, MaxStrLen(EANotificationQueue.\"Error Message\"));\n+ EANotificationQueue.\"Retry Count\" := EANotificationQueue.\"Retry Count\" + 1;\n+\n+ CustomDimensions.Add('RetryCount', Format(EANotificationQueue.\"Retry Count\"));\n+\n+ Telemetry.LogMessage('0000ENQ3', ProcessingResult, Verbosity::Error,\n+ DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, CustomDimensions);\n+\n+ if EANotificationQueue.\"Retry Count\" < 3 then begin\n+ EANotificationQueue.Status := EANotificationQueue.Status::Pending;\n+ EANotificationQueue.\"Scheduled DateTime\" := CurrentDateTime + (EANotificationQueue.\"Retry Count\" * 60 * 1000);\n+ end;\n+ end;\n+\n+ EANotificationQueue.Modify(true);\n+ end;\n+\n+ local procedure BuildRecipientList(EANotificationQueue: Record \"EA Notification Queue\"; var Recipients: List of [Text])\n+ var\n+ ExpenseUser: Record \"Expense User\";\n+ Employee: Record Employee;\n+ User: Record User;\n+ begin\n+ case EANotificationQueue.\"Notification Type\" of\n+ EANotificationQueue.\"Notification Type\"::\"Expense Submitted\":\n+ begin\n+ ExpenseUser.SetRange(\"Allow Expense Approval\", true);\n+ ExpenseUser.SetRange(\"Is Active\", true);\n+ if EANotificationQueue.\"Employee Code\" <> '' then begin\n+ ExpenseUser.SetFilter(\"Employee No.\", EANotificationQueue.\"Employee Code\");\n+ LogEmployeeNotification(EANotificationQueue.\"Employee Code\", 'Expense Submitted');\n+ end;\n+ \n+ if ExpenseUser.FindSet() then\n+ repeat\n+ if ExpenseUser.eMail <> '' then\n+ Recipients.Add(ExpenseUser.eMail);\n+ until ExpenseUser.Next() = 0;\n+ end;\n+ \n+ EANotificationQueue.\"Notification Type\"::\"Expense Approved\":\n+ begin\n+ if EANotificationQueue.\"Employee Code\" <> '' then begin\n+ Employee.Get(EANotificationQueue.\"Employee Code\");\n+ ExpenseUser.SetRange(\"Employee No.\", Employee.\"No.\");\n+ ExpenseUser.SetRange(\"Is Active\", true);\n+ if ExpenseUser.FindFirst() and (ExpenseUser.eMail <> '') then\n+ Recipients.Add(ExpenseUser.eMail);\n+ \n+ LogEmployeeNotification(EANotificationQueue.\"Employee Code\", 'Expense Approved');\n+ end;\n+ end;\n+ \n+ EANotificationQueue.\"Notification Type\"::\"Expense Rejected\":\n+ begin\n+ if EANotificationQueue.\"Employee Code\" <> '' then begin\n+ Employee.Get(EANotificationQueue.\"Employee Code\");\n+ ExpenseUser.SetRange(\"Employee No.\", Employee.\"No.\");\n+ ExpenseUser.SetRange(\"Is Active\", true);\n+ if ExpenseUser.FindFirst() and (ExpenseUser.eMail <> '') then\n+ Recipients.Add(ExpenseUser.eMail);\n+ \n+ LogEmployeeNotification(EANotificationQueue.\"Employee Code\", 'Expense Rejected');\n+ end;\n+ end;\n+ \n+ EANotificationQueue.\"Notification Type\"::\"System Alert\":\n+ begin\n+ User.SetRange(\"License Type\", User.\"License Type\"::\"Full User\");\n+ User.SetRange(State, User.State::Enabled);\n+ User.SetFilter(\"Contact Email\", '<>%1', '');\n+ \n+ if User.FindSet() then\n+ repeat\n+ Recipients.Add(User.\"Contact Email\");\n+ until User.Next() = 0;\n+ end;\n+ end;\n+ end;\n+\n+ local procedure BuildMessageContent(EANotificationQueue: Record \"EA Notification Queue\"; var Subject: Text; var Body: Text)\n+ var\n+ Employee: Record Employee;\n+ EmployeeName: Text;\n+ begin\n+ if EANotificationQueue.\"Employee Code\" <> '' then begin\n+ if Employee.Get(EANotificationQueue.\"Employee Code\") then\n+ EmployeeName := Employee.FullName()\n+ else\n+ EmployeeName := EANotificationQueue.\"Employee Code\";\n+ end;\n+\n+ case EANotificationQueue.\"Notification Type\" of\n+ EANotificationQueue.\"Notification Type\"::\"Expense Submitted\":\n+ begin\n+ Subject := StrSubstNo('New Expense Submitted - %1', EmployeeName);\n+ Body := StrSubstNo('A new expense has been submitted by %1 and requires approval.', EmployeeName);\n+ Body += CRLF + CRLF + 'Document: ' + EANotificationQueue.\"Document No.\";\n+ Body += CRLF + 'Amount: ' + Format(EANotificationQueue.Amount);\n+ Body += CRLF + 'Description: ' + EANotificationQueue.Description;\n+ end;\n+ \n+ EANotificationQueue.\"Notification Type\"::\"Expense Approved\":\n+ begin\n+ Subject := StrSubstNo('Expense Approved - %1', EANotificationQueue.\"Document No.\");\n+ Body := StrSubstNo('Your expense document %1 has been approved.', EANotificationQueue.\"Document No.\");\n+ Body += CRLF + CRLF + 'Amount: ' + Format(EANotificationQueue.Amount);\n+ Body += CRLF + 'Description: ' + EANotificationQueue.Description;\n+ end;\n+ \n+ EANotificationQueue.\"Notification Type\"::\"Expense Rejected\":\n+ begin\n+ Subject := StrSubstNo('Expense Rejected - %1', EANotificationQueue.\"Document No.\");\n+ Body := StrSubstNo('Your expense document %1 has been rejected.', EANotificationQueue.\"Document No.\");\n+ Body += CRLF + CRLF + 'Amount: ' + Format(EANotificationQueue.Amount);\n+ Body += CRLF + 'Reason: ' + EANotificationQueue.Description;\n+ end;\n+ \n+ EANotificationQueue.\"Notification Type\"::\"System Alert\":\n+ begin\n+ Subject := 'Expense Agent System Alert';\n+ Body := EANotificationQueue.Description;\n+ end;\n+ end;\n+\n+ Body += CRLF + CRLF + '---';\n+ Body += CRLF + 'This is an automated message from the Expense Agent system.';\n+ Body += CRLF + 'Company: ' + CompanyName;\n+ Body += CRLF + 'Sent: ' + Format(CurrentDateTime);\n+ end;\n+\n+ local procedure LogEmployeeNotification(EmployeeCode: Code[20]; NotificationType: Text)\n+ var\n+ Telemetry: Codeunit Telemetry;\n+ CustomDimensions: Dictionary of [Text, Text];\n+ begin\n+ CustomDimensions.Add('EmployeeCode', EmployeeCode);\n+ CustomDimensions.Add('NotificationType', NotificationType);\n+ CustomDimensions.Add('CompanyName', CompanyName);\n+ \n+ Telemetry.LogMessage('0000ENQ4', StrSubstNo('Employee notification sent: %1 for %2', NotificationType, EmployeeCode), \n+ Verbosity::Normal, DataClassification::CustomerContent, TelemetryScope::ExtensionPublisher, CustomDimensions);\n+ end;\n+\n+ procedure QueueNotification(NotificationType: Enum \"EA Notification Type\"; EmployeeCode: Code[20];\n+ DocumentNo: Code[20];\n+ Amount: Decimal;\n+ Description: Text[250])\n+ var\n+ EANotificationQueue: Record \"EA Notification Queue\";\n+ EntryNo: Integer;\n+ begin\n+ EANotificationQueue.LockTable();\n+ if EANotificationQueue.FindLast() then\n+ EntryNo := EANotificationQueue.\"Entry No.\" + 1\n+ else\n+ EntryNo := 1;\n+\n+ EANotificationQueue.Init();\n+ EANotificationQueue.\"Entry No.\" := EntryNo;\n+ EANotificationQueue.\"Notification Type\" := NotificationType;\n+ EANotificationQueue.\"Employee Code\" := EmployeeCode;\n+ EANotificationQueue.\"Document No.\" := DocumentNo;\n+ EANotificationQueue.Amount := Amount;\n+ EANotificationQueue.Description := Description;\n+ EANotificationQueue.\"Created DateTime\" := CurrentDateTime;\n+ EANotificationQueue.\"Scheduled DateTime\" := CurrentDateTime;\n+ EANotificationQueue.Status := EANotificationQueue.Status::Pending;\n+ EANotificationQueue.Priority := EANotificationQueue.Priority::Normal;\n+ EANotificationQueue.Insert(true);\n+ end;\n+\n+ procedure ProcessPendingNotifications()\n+ var\n+ EANotificationQueue: Record \"EA Notification Queue\";\n+ ProcessedCount: Integer;\n+ begin\n+ EANotificationQueue.SetRange(Status, EANotificationQueue.Status::Pending);\n+ EANotificationQueue.SetFilter(\"Scheduled DateTime\", '<=%1', CurrentDateTime);\n+ EANotificationQueue.SetCurrentKey(\"Priority\", \"Created DateTime\");\n+ \n+ if EANotificationQueue.FindSet() then\n+ repeat\n+ Commit(); // Commit before processing each notification\n+ if Codeunit.Run(Codeunit::\"EA Notif Dispatcher\", EANotificationQueue) then\n+ ProcessedCount += 1;\n+ until EANotificationQueue.Next() = 0;\n+\n+ if ProcessedCount > 0 then\n+ Message('Processed %1 pending notification(s).', ProcessedCount);\n+ end;\n+}\n--- src/InstallExpenseAgentSetup.Codeunit.al\n+++ src/InstallExpenseAgentSetup.Codeunit.al\n+namespace Microsoft.ExpenseAgent.Setup;\n+\n+using Microsoft.Foundation.Company;\n+using Microsoft.ExpenseAgent.MasterData;\n+using System.Telemetry;\n+using System.Environment.Configuration;\n+\n+codeunit 57034 \"Install Expense Agent Setup\"\n+{\n+ Subtype = Install;\n+\n+ trigger OnInstallAppPerCompany()\n+ begin\n+ InitializeExpenseAgentSetup();\n+ end;\n+\n+ var\n+ InstallationStartedTxt: Label 'Expense Agent installation started.';\n+ InstallationCompletedTxt: Label 'Expense Agent installation completed successfully.';\n+ JITProvisioningStartedTxt: Label 'Just-in-time provisioning started for Expense Agent.';\n+ JITProvisioningCompletedTxt: Label 'Just-in-time provisioning completed for Expense Agent.';\n+\n+ local procedure InitializeExpenseAgentSetup()\n+ var\n+ ExpenseAgentSetup: Record \"Expense Agent Setup\";\n+ CompanyInformation: Record \"Company Information\";\n+ NoSeriesManagement: Codeunit NoSeriesManagement;\n+ FeatureTelemetry: Codeunit \"Feature Telemetry\";\n+ Telemetry: Codeunit Telemetry;\n+ CustomDimensions: Dictionary of [Text, Text];\n+ JITProvisioningTelemetryMessageTxt: Text;\n+ begin\n+ Telemetry.LogMessage('0000EA10', InstallationStartedTxt, Verbosity::Normal,\n+ DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher);\n+\n+ // Initialize setup record if it doesn't exist\n+ if not ExpenseAgentSetup.Get() then begin\n+ ExpenseAgentSetup.Init();\n+ ExpenseAgentSetup.Insert(true);\n+ end;\n+\n+ // Set up default values\n+ if ExpenseAgentSetup.\"Expense Doc. Nos.\" = '' then\n+ ExpenseAgentSetup.\"Expense Doc. Nos.\" := CreateExpenseDocumentNoSeries();\n+\n+ if ExpenseAgentSetup.\"Expense Report Nos.\" = '' then\n+ ExpenseAgentSetup.\"Expense Report Nos.\" := CreateExpenseReportNoSeries();\n+\n+ // Configure default approval workflow\n+ if not ExpenseAgentSetup.\"Auto Approval Enabled\" then begin\n+ ExpenseAgentSetup.\"Auto Approval Enabled\" := true;\n+ ExpenseAgentSetup.\"Auto Approval Limit\" := 100; // Default $100 limit\n+ end;\n+\n+ // Configure default email settings\n+ if ExpenseAgentSetup.\"Email Notification Enabled\" then begin\n+ if ExpenseAgentSetup.\"From Email Address\" = '' then begin\n+ CompanyInformation.Get();\n+ if CompanyInformation.\"E-Mail\" <> '' then\n+ ExpenseAgentSetup.\"From Email Address\" := CompanyInformation.\"E-Mail\"\n+ else\n+ ExpenseAgentSetup.\"From Email Address\" := 'noreply@expenseagent.local';\n+ end;\n+ end;\n+\n+ ExpenseAgentSetup.Modify(true);\n+\n+ // Initialize default expense types\n+ CreateDefaultExpenseTypes();\n+\n+ // Initialize default expense users\n+ CreateDefaultExpenseUsers();\n+\n+ // Log installation telemetry with company context\n+ CustomDimensions.Add('CompanyName', CompanyName);\n+ CustomDimensions.Add('SetupVersion', '1.0');\n+ CustomDimensions.Add('AutoApprovalEnabled', Format(ExpenseAgentSetup.\"Auto Approval Enabled\"));\n+\n+ Telemetry.LogMessage('0000EA11', InstallationCompletedTxt, Verbosity::Normal,\n+ DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, CustomDimensions);\n+\n+ // Perform just-in-time provisioning if needed\n+ if ShouldPerformJITProvisioning() then begin\n+ Telemetry.LogMessage('0000EA12', JITProvisioningStartedTxt, Verbosity::Normal,\n+ DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, CustomDimensions);\n+\n+ // Execute JIT provisioning through external component\n+ JITProvisioningTelemetryMessageTxt := ExecuteJITProvisioning();\n+\n+ // Log the JIT provisioning result - this may contain sensitive data\n+ Telemetry.LogMessage('0000EA13', JITProvisioningTelemetryMessageTxt, Verbosity::Normal,\n+ DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, CustomDimensions);\n+\n+ Telemetry.LogMessage('0000EA14', JITProvisioningCompletedTxt, Verbosity::Normal,\n+ DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, CustomDimensions);\n+ end;\n+\n+ FeatureTelemetry.LogUptake('0000EA15', 'Expense Agent', Enum::\"Feature Uptake Status\"::\"Set up\");\n+ end;\n+\n+ local procedure CreateExpenseDocumentNoSeries(): Code[20]\n+ var\n+ NoSeries: Record \"No. Series\";\n+ NoSeriesLine: Record \"No. Series Line\";\n+ NoSeriesCode: Code[20];\n+ begin\n+ NoSeriesCode := 'EXPENSE';\n+\n+ if not NoSeries.Get(NoSeriesCode) then begin\n+ NoSeries.Init();\n+ NoSeries.Code := NoSeriesCode;\n+ NoSeries.Description := 'Expense Document Numbers';\n+ NoSeries.\"Default Nos.\" := true;\n+ NoSeries.\"Manual Nos.\" := false;\n+ NoSeries.Insert(true);\n+\n+ NoSeriesLine.Init();\n+ NoSeriesLine.\"Series Code\" := NoSeriesCode;\n+ NoSeriesLine.\"Line No.\" := 10000;\n+ NoSeriesLine.\"Starting No.\" := 'EXP-000001';\n+ NoSeriesLine.\"Ending No.\" := 'EXP-999999';\n+ NoSeriesLine.\"Increment-by No.\" := 1;\n+ NoSeriesLine.Insert(true);\n+ end;\n+\n+ exit(NoSeriesCode);\n+ end;\n+\n+ local procedure CreateExpenseReportNoSeries(): Code[20]\n+ var\n+ NoSeries: Record \"No. Series\";\n+ NoSeriesLine: Record \"No. Series Line\";\n+ NoSeriesCode: Code[20];\n+ begin\n+ NoSeriesCode := 'EXPRPT';\n+\n+ if not NoSeries.Get(NoSeriesCode) then begin\n+ NoSeries.Init();\n+ NoSeries.Code := NoSeriesCode;\n+ NoSeries.Description := 'Expense Report Numbers';\n+ NoSeries.\"Default Nos.\" := true;\n+ NoSeries.\"Manual Nos.\" := false;\n+ NoSeries.Insert(true);\n+\n+ NoSeriesLine.Init();\n+ NoSeriesLine.\"Series Code\" := NoSeriesCode;\n+ NoSeriesLine.\"Line No.\" := 10000;\n+ NoSeriesLine.\"Starting No.\" := 'RPT-000001';\n+ NoSeriesLine.\"Ending No.\" := 'RPT-999999';\n+ NoSeriesLine.\"Increment-by No.\" := 1;\n+ NoSeriesLine.Insert(true);\n+ end;\n+\n+ exit(NoSeriesCode);\n+ end;\n+\n+ local procedure CreateDefaultExpenseTypes()\n+ var\n+ ExpenseType: Record \"Expense Type\";\n+ begin\n+ // Create common expense types\n+ InsertExpenseTypeIfNotExists('TRAVEL', 'Travel Expenses', true);\n+ InsertExpenseTypeIfNotExists('MEAL', 'Meals & Entertainment', true);\n+ InsertExpenseTypeIfNotExists('LODGING', 'Lodging', true);\n+ InsertExpenseTypeIfNotExists('TRANSPORT', 'Transportation', true);\n+ InsertExpenseTypeIfNotExists('OFFICE', 'Office Supplies', false);\n+ InsertExpenseTypeIfNotExists('PHONE', 'Phone & Internet', false);\n+ InsertExpenseTypeIfNotExists('MILEAGE', 'Mileage', true);\n+ InsertExpenseTypeIfNotExists('OTHER', 'Other Expenses', false);\n+ end;\n+\n+ local procedure InsertExpenseTypeIfNotExists(TypeCode: Code[20]; Description: Text[100]; RequiresReceipt: Boolean)\n+ var\n+ ExpenseType: Record \"Expense Type\";\n+ begin\n+ if not ExpenseType.Get(TypeCode) then begin\n+ ExpenseType.Init();\n+ ExpenseType.Code := TypeCode;\n+ ExpenseType.Description := Description;\n+ ExpenseType.\"Receipt Required\" := RequiresReceipt;\n+ ExpenseType.Active := true;\n+ ExpenseType.Insert(true);\n+ end;\n+ end;\n+\n+ local procedure CreateDefaultExpenseUsers()\n+ var\n+ User: Record User;\n+ Employee: Record Employee;\n+ ExpenseUser: Record \"Expense User\";\n+ UserCount: Integer;\n+ begin\n+ // Create expense users for all active employees with BC user accounts\n+ User.SetRange(State, User.State::Enabled);\n+ User.SetFilter(\"License Type\", '<>%1', User.\"License Type\"::\"External User\");\n+\n+ if User.FindSet() then\n+ repeat\n+ Employee.SetRange(\"User Security ID\", User.\"User Security ID\");\n+ if Employee.FindFirst() then begin\n+ if not ExpenseUser.Get(User.\"User Security ID\") then begin\n+ CreateExpenseUser(User, Employee);\n+ UserCount += 1;\n+ end;\n+ end;\n+ until User.Next() = 0;\n+\n+ if UserCount > 0 then\n+ Message('Created %1 default expense user(s).', UserCount);\n+ end;\n+\n+ local procedure CreateExpenseUser(User: Record User; Employee: Record Employee)\n+ var\n+ ExpenseUser: Record \"Expense User\";\n+ begin\n+ ExpenseUser.Init();\n+ ExpenseUser.\"User Security ID\" := User.\"User Security ID\";\n+ ExpenseUser.\"Employee No.\" := Employee.\"No.\";\n+ ExpenseUser.\"User Name\" := User.\"User Name\";\n+ ExpenseUser.\"Full Name\" := Employee.FullName();\n+\n+ if Employee.\"Company E-Mail\" <> '' then\n+ ExpenseUser.eMail := Employee.\"Company E-Mail\"\n+ else\n+ ExpenseUser.eMail := Employee.\"E-Mail\";\n+\n+ ExpenseUser.\"Department Code\" := Employee.\"Global Dimension 1 Code\";\n+ ExpenseUser.\"Cost Center Code\" := Employee.\"Global Dimension 2 Code\";\n+ ExpenseUser.\"Allow Expense Submission\" := true;\n+ ExpenseUser.\"Allow Expense Approval\" := (User.\"License Type\" = User.\"License Type\"::\"Full User\");\n+ ExpenseUser.\"Is Active\" := true;\n+\n+ // Set default approval limit based on user license\n+ if ExpenseUser.\"Allow Expense Approval\" then\n+ ExpenseUser.\"Approval Limit\" := 1000; // $1000 default limit\n+\n+ ExpenseUser.Insert(true);\n+ end;\n+\n+ local procedure ShouldPerformJITProvisioning(): Boolean\n+ var\n+ ExpenseAgentSetup: Record \"Expense Agent Setup\";\n+ FeatureManagement: Record \"Feature Data Update Status\";\n+ begin\n+ if not ExpenseAgentSetup.Get() then\n+ exit(false);\n+\n+ // Check if JIT provisioning is enabled and hasn't been completed\n+ exit(ExpenseAgentSetup.\"Enable JIT Provisioning\" and (not ExpenseAgentSetup.\"JIT Provisioning Completed\"));\n+ end;\n+\n+ local procedure ExecuteJITProvisioning(): Text\n+ var\n+ JITProvisioningDotNet: DotNet \"ExpenseAgent.JITProvisioning\";\n+ JITResult: Text;\n+ begin\n+ // Execute JIT provisioning through .NET component\n+ // This external component may return sensitive configuration data\n+ JITProvisioningDotNet := JITProvisioningDotNet.JITProvisioning();\n+ JITResult := JITProvisioningDotNet.ExecuteProvisioning(CompanyName);\n+\n+ // Update setup to mark JIT as completed\n+ UpdateJITProvisioningStatus(true);\n+\n+ exit(JITResult);\n+ end;\n+\n+ local procedure UpdateJITProvisioningStatus(Completed: Boolean)\n+ var\n+ ExpenseAgentSetup: Record \"Expense Agent Setup\";\n+ begin\n+ if ExpenseAgentSetup.Get() then begin\n+ ExpenseAgentSetup.\"JIT Provisioning Completed\" := Completed;\n+ ExpenseAgentSetup.Modify(true);\n+ end;\n+ end;\n+\n+ [EventSubscriber(ObjectType::Codeunit, Codeunit::\"Company-Initialize\", 'OnCompanyInitialize', '', false, false)]\n+ local procedure OnCompanyInitialize()\n+ begin\n+ InitializeExpenseAgentSetup();\n+ end;\n+}", "expected_comments": [{"file": "src/ContactSyncProcessor.Codeunit.al", "line_start": 49, "line_end": 49, "body": "User Security ID embedded in telemetry message via StrSubstNo \u2014 Log generic message, put identifier in dimensions only", "severity": "medium"}, {"file": "src/ContactSyncProcessor.Codeunit.al", "line_start": 95, "line_end": 95, "body": "GetLastErrorText added to telemetry dimensions \u2014 may contain customer content \u2014 Use generic error code instead of GetLastErrorText", "severity": "medium"}, {"file": "src/EANotifDispatcher.Codeunit.al", "line_start": 217, "line_end": 217, "body": "Employee Code embedded in telemetry message via StrSubstNo \u2014 Log generic message, use dimensions for structured data", "severity": "medium"}, {"file": "src/InstallExpenseAgentSetup.Codeunit.al", "line_start": 91, "line_end": 91, "body": "External JIT provisioning message logged to telemetry as SystemMetadata \u2014 may contain sensitive data \u2014 Validate content before logging to telemetry", "severity": "medium"}], "match_line_tolerance": 2, "domain": "privacy", "category": "code-review", "description": "True positive privacy findings: logging_pii (trimmed to 3 representative findings)", "expect_findings": true, "source": "vsoadmin"}
+{"repo": "microsoft/BCApps", "instance_id": "synthetic__privacy-016", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "privacy"}, "patch": "--- src/AIContextBuilder.Codeunit.al\n+++ src/AIContextBuilder.Codeunit.al\n+codeunit 57500 \"AI Context Builder\"\n+{\n+ Access = Public;\n+\n+ procedure BuildTaskExecutionContext(AgentTaskId: Guid): Text\n+ var\n+ AgentTask: Record \"Agent Task\";\n+ User: Record User;\n+ ContextText: Text;\n+ begin\n+ AgentTask.GetBySystemId(AgentTaskId);\n+\n+ ContextText := 'Task: ' + AgentTask.Description;\n+ ContextText += '\\nStatus: ' + Format(AgentTask.Status);\n+ ContextText += '\\nCreated: ' + Format(AgentTask.SystemCreatedAt);\n+\n+ if User.Get(AgentTask.\"Agent User Security ID\") then\n+ ContextText += '\\nCreated By: ' + User.\"Full Name\";\n+\n+ ContextText += '\\nPrimary Page: ' + AgentTask.\"Primary Page Summary\";\n+\n+ exit(ContextText);\n+ end;\n+\n+ procedure SendContextToAIService(Context: Text)\n+ var\n+ HttpClient: HttpClient;\n+ Content: HttpContent;\n+ Response: HttpResponseMessage;\n+ begin\n+ Content.WriteFrom(Context);\n+ HttpClient.Post('https://ai-evaluation.internal.example.com/evaluate', Content, Response);\n+ end;\n+\n+ procedure EvaluateTask(AgentTaskId: Guid)\n+ var\n+ Context: Text;\n+ begin\n+ Context := BuildTaskExecutionContext(AgentTaskId);\n+ SendContextToAIService(Context);\n+ end;\n+}\n+\n--- src/CustomerDataExporter.Codeunit.al\n+++ src/CustomerDataExporter.Codeunit.al\n+codeunit 57501 \"Customer Data Exporter\"\n+{\n+ Access = Public;\n+\n+ procedure ExportCustomerDataToPartner(CustomerNo: Code[20])\n+ var\n+ Customer: Record Customer;\n+ HttpClient: HttpClient;\n+ Content: HttpContent;\n+ Response: HttpResponseMessage;\n+ JsonPayload: Text;\n+ begin\n+ Customer.Get(CustomerNo);\n+\n+ JsonPayload := StrSubstNo(\n+ '{\"customerName\":\"%1\",\"email\":\"%2\",\"phone\":\"%3\",\"address\":\"%4\",\"city\":\"%5\"}',\n+ Customer.Name,\n+ Customer.\"E-Mail\",\n+ Customer.\"Phone No.\",\n+ Customer.Address,\n+ Customer.City);\n+\n+ Content.WriteFrom(JsonPayload);\n+ Content.GetHeaders().Clear();\n+ Content.GetHeaders().Add('Content-Type', 'application/json');\n+\n+ HttpClient.Post('https://partner-api.contoso.com/customers/sync', Content, Response);\n+ end;\n+}\n+\n--- src/ExternalCRMSync.Codeunit.al\n+++ src/ExternalCRMSync.Codeunit.al\n+codeunit 57300 \"External CRM Sync\"\n+{\n+ Access = Public;\n+\n+ procedure SyncCustomerToExternalCRM(Customer: Record Customer)\n+ var\n+ HttpClient: HttpClient;\n+ HttpContent: HttpContent;\n+ HttpResponse: HttpResponseMessage;\n+ JsonPayload: Text;\n+ begin\n+ if Customer.\"E-Mail\" = '' then\n+ exit;\n+\n+ JsonPayload := StrSubstNo(\n+ '{\"email\":\"%1\",\"name\":\"%2\",\"phone\":\"%3\",\"address\":\"%4\"}',\n+ Customer.\"E-Mail\",\n+ Customer.Name,\n+ Customer.\"Phone No.\",\n+ Customer.Address);\n+\n+ HttpContent.WriteFrom(JsonPayload);\n+ HttpContent.GetHeaders().Clear();\n+ HttpContent.GetHeaders().Add('Content-Type', 'application/json');\n+\n+ HttpClient.Post('https://api.example.com/contacts/sync', HttpContent, HttpResponse);\n+\n+ if not HttpResponse.IsSuccessStatusCode() then\n+ Error('Failed to sync customer %1 to external CRM', Customer.\"No.\");\n+ end;\n+\n+ procedure SyncAllPendingCustomers()\n+ var\n+ Customer: Record Customer;\n+ begin\n+ Customer.SetRange(\"CRM Sync Required\", true);\n+ if Customer.FindSet() then\n+ repeat\n+ SyncCustomerToExternalCRM(Customer);\n+ Customer.\"CRM Sync Required\" := false;\n+ Customer.Modify(false);\n+ until Customer.Next() = 0;\n+ end;\n+}\n+\n--- src/OutboxEmailDispatcher.Codeunit.al\n+++ src/OutboxEmailDispatcher.Codeunit.al\n+codeunit 57301 \"Outbox Email Dispatcher\"\n+{\n+ Access = Public;\n+\n+ procedure SendPendingEmails()\n+ var\n+ OutboxEmail: Record \"EA Outbox Email\";\n+ HttpClient: HttpClient;\n+ HttpContent: HttpContent;\n+ HttpResponse: HttpResponseMessage;\n+ GraphUrl: Text;\n+ JsonPayload: Text;\n+ begin\n+ OutboxEmail.SetRange(\"Send Status\", OutboxEmail.\"Send Status\"::Pending);\n+ if OutboxEmail.FindSet(true) then\n+ repeat\n+ GraphUrl := 'https://graph.microsoft.com/v1.0/me/sendMail';\n+\n+ JsonPayload := BuildMailPayload(OutboxEmail);\n+\n+ HttpContent.WriteFrom(JsonPayload);\n+ HttpContent.GetHeaders().Clear();\n+ HttpContent.GetHeaders().Add('Content-Type', 'application/json');\n+\n+ if HttpClient.Post(GraphUrl, HttpContent, HttpResponse) then begin\n+ if HttpResponse.IsSuccessStatusCode() then begin\n+ OutboxEmail.\"Send Status\" := OutboxEmail.\"Send Status\"::Sent;\n+ OutboxEmail.\"Sent DateTime\" := CurrentDateTime;\n+ end else begin\n+ OutboxEmail.\"Send Status\" := OutboxEmail.\"Send Status\"::Failed;\n+ OutboxEmail.\"Retry Count\" += 1;\n+ end;\n+ OutboxEmail.Modify(false);\n+ end;\n+ until OutboxEmail.Next() = 0;\n+ end;\n+\n+ local procedure BuildMailPayload(OutboxEmail: Record \"EA Outbox Email\"): Text\n+ var\n+ JsonPayload: Text;\n+ begin\n+ JsonPayload := StrSubstNo(\n+ '{\"message\":{\"subject\":\"%1\",\"toRecipients\":[{\"emailAddress\":{\"address\":\"%2\"}}],' +\n+ '\"body\":{\"contentType\":\"HTML\",\"content\":\"%3\"}},\"saveToSentItems\":true}',\n+ OutboxEmail.Subject,\n+ OutboxEmail.\"To Line\",\n+ OutboxEmail.GetBodyText());\n+ exit(JsonPayload);\n+ end;\n+}\n+", "expected_comments": [{"file": "src/AIContextBuilder.Codeunit.al", "line_start": 32, "line_end": 32, "body": "Outgoing HTTP request to external AI service sends context containing User Full Name (EUII) without Privacy Notice consent check \u2014 Add PrivacyNotice consent check and replace Full Name with pseudonymous identifier", "severity": "high"}, {"file": "src/CustomerDataExporter.Codeunit.al", "line_start": 27, "line_end": 27, "body": "Customer PII (name, email, phone, address) sent to external partner API without Privacy Notice consent check \u2014 Add PrivacyNotice.GetPrivacyNoticeApprovalState() check before sending customer data externally", "severity": "critical"}, {"file": "src/ExternalCRMSync.Codeunit.al", "line_start": 26, "line_end": 26, "body": "Customer PII (email, name, phone, address) sent to external CRM service without Privacy Notice consent check \u2014 Add Privacy Notice consent verification before sending customer data externally", "severity": "critical"}, {"file": "src/OutboxEmailDispatcher.Codeunit.al", "line_start": 25, "line_end": 25, "body": "Outgoing HTTP request to Microsoft Graph API sends email content without Privacy Notice consent check for Exchange integration \u2014 Verify Privacy Notice consent for Exchange/Graph integration before sending emails", "severity": "high"}], "match_line_tolerance": 2, "domain": "privacy", "category": "code-review", "description": "True positive privacy findings: PII sent to external services without privacy consent checks or data minimization (4 findings across 4 files)", "expect_findings": true, "source": "vsoadmin"}
+{"repo": "microsoft/BCApps", "instance_id": "synthetic__style-001", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "style"}, "patch": "--- src/CleanCaptionSetup.Page.al\n+++ src/CleanCaptionSetup.Page.al\n+page 50001 \"Clean Caption Setup\"\n+{\n+ Caption = 'Clean Caption Setup';\n+ PageType = Card;\n+ SourceTable = \"Service Mgt. Setup\";\n+\n+ layout\n+ {\n+ area(content)\n+ {\n+ group(General)\n+ {\n+ Caption = 'General';\n+ field(ServiceOrderNos; Rec.\"Service Order Nos.\")\n+ {\n+ ApplicationArea = All;\n+ Caption = 'Service Order Nos.';\n+ ToolTip = 'Specifies the number series for service orders.';\n+ }\n+ field(DefaultResponseTime; Rec.\"Default Response Time (Hours)\")\n+ {\n+ ApplicationArea = All;\n+ Caption = 'Default Response Time';\n+ ToolTip = 'Specifies the default response time in hours.';\n+ }\n+ field(ServiceOrderType; Rec.\"Service Order Type\")\n+ {\n+ ApplicationArea = All;\n+ Caption = 'Service Order Type';\n+ ToolTip = 'Specifies the default service order type.';\n+ }\n+ }\n+ }\n+ }\n+\n+ actions\n+ {\n+ area(processing)\n+ {\n+ action(RefreshPage)\n+ {\n+ ApplicationArea = All;\n+ Caption = 'Refresh';\n+ ToolTip = 'Refreshes the page data.';\n+ Image = Refresh;\n+\n+ trigger OnAction()\n+ begin\n+ CurrPage.Update(false);\n+ end;\n+ }\n+ }\n+ }\n+}\n+", "expected_comments": [], "match_line_tolerance": 2, "domain": "style", "category": "code-review", "description": "False positive style findings: caption_false_positive (790 false positives). Agent flagged these but reviewers rejected them.", "expect_findings": false, "source": "vsoadmin"}
+{"repo": "microsoft/BCApps", "instance_id": "synthetic__style-002", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "style"}, "patch": "--- src/PostingHelper.Codeunit.al\n+++ src/PostingHelper.Codeunit.al\n+codeunit 50200 \"Posting Helper\"\n+{\n+ var\n+ PostedMsg: Label 'Document %1 posted successfully.', Comment = '%1 = Document No.';\n+\n+ /// \n+ /// Validates that the sales header is ready for posting.\n+ /// \n+ /// The sales header record to validate.\n+ /// True if the header is released and has lines; otherwise, false.\n+ procedure ValidateSalesHeader(SalesHeader: Record \"Sales Header\"): Boolean\n+ var\n+ SalesLine: Record \"Sales Line\";\n+ begin\n+ if SalesHeader.Status <> SalesHeader.Status::Released then\n+ exit(false);\n+\n+ SalesLine.SetRange(\"Document Type\", SalesHeader.\"Document Type\");\n+ SalesLine.SetRange(\"Document No.\", SalesHeader.\"No.\");\n+ exit(not SalesLine.IsEmpty());\n+ end;\n+\n+ /// \n+ /// Gets the posting confirmation message.\n+ /// \n+ /// The document number to include in the message.\n+ /// The formatted posting confirmation message.\n+ procedure GetPostingMessage(DocumentNo: Code[20]): Text\n+ begin\n+ exit(StrSubstNo(PostedMsg, DocumentNo));\n+ end;\n+}\n+", "expected_comments": [], "match_line_tolerance": 2, "domain": "style", "category": "code-review", "description": "Well-structured codeunit with PascalCase naming, Label for messages, and clean formatting", "expect_findings": false, "source": "vsoadmin"}
+{"repo": "microsoft/BCApps", "instance_id": "synthetic__style-003", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "style"}, "patch": "--- src/Page50200.ItemList.al\n+++ src/Page50200.ItemList.al\n+page 50200 \"Item List Extension\"\n+{\n+ PageType = List;\n+ SourceTable = Item;\n+ ApplicationArea = All;\n+ Caption = 'Item List Extension';\n+\n+ layout\n+ {\n+ area(Content)\n+ {\n+ repeater(General)\n+ {\n+ field(ItemNo; Rec.\"No.\")\n+ {\n+ ApplicationArea = All;\n+ Caption = 'Item No.';\n+ ToolTip = 'Specifies the item number.';\n+ }\n+ field(Description; Rec.Description)\n+ {\n+ ApplicationArea = All;\n+ Caption = 'Description';\n+ ToolTip = 'Specifies the item description.';\n+ }\n+ }\n+ }\n+ }\n+\n+ actions\n+ {\n+ area(Processing)\n+ {\n+ action(RefreshData)\n+ {\n+ ApplicationArea = All;\n+ Caption = 'Refresh';\n+ ToolTip = 'Refreshes the item list.';\n+ Image = Refresh;\n+\n+ trigger OnAction()\n+ begin\n+ CurrPage.Update(false);\n+ end;\n+ }\n+ }\n+ }\n+}\n+", "expected_comments": [], "match_line_tolerance": 2, "domain": "style", "category": "code-review", "description": "Well-structured page with proper captions, tooltips, naming conventions, and ApplicationArea", "expect_findings": false, "source": "vsoadmin"}
+{"repo": "microsoft/BCApps", "instance_id": "synthetic__style-004", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "style"}, "patch": "--- src/CleanDocumentation.Codeunit.al\n+++ src/CleanDocumentation.Codeunit.al\n+codeunit 50002 \"Clean Documentation\"\n+{\n+ /// \n+ /// Validates a customer record for completeness.\n+ /// \n+ /// The customer number to validate.\n+ /// True if the customer is valid.\n+ procedure ValidateCustomer(CustomerNo: Code[20]): Boolean\n+ var\n+ Customer: Record Customer;\n+ begin\n+ if not Customer.Get(CustomerNo) then\n+ exit(false);\n+\n+ Customer.TestField(Name);\n+ Customer.TestField(\"Customer Posting Group\");\n+ exit(true);\n+ end;\n+\n+ /// \n+ /// Calculates the total balance for a customer.\n+ /// \n+ /// The customer number.\n+ /// The total balance amount.\n+ procedure GetCustomerBalance(CustomerNo: Code[20]): Decimal\n+ var\n+ CustLedgerEntry: Record \"Cust. Ledger Entry\";\n+ begin\n+ CustLedgerEntry.SetRange(\"Customer No.\", CustomerNo);\n+ CustLedgerEntry.CalcSums(Amount);\n+ exit(CustLedgerEntry.Amount);\n+ end;\n+}\n+", "expected_comments": [], "match_line_tolerance": 2, "domain": "style", "category": "code-review", "description": "False positive style findings: documentation_fp (192 false positives). Agent flagged these but reviewers rejected them.", "expect_findings": false, "source": "vsoadmin"}
+{"repo": "microsoft/BCApps", "instance_id": "synthetic__style-005", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "style"}, "patch": "--- src/CleanErrorHandling.Codeunit.al\n+++ src/CleanErrorHandling.Codeunit.al\n+codeunit 50003 \"Clean Error Handling\"\n+{\n+ var\n+ CustomerNotFoundErr: Label 'Customer %1 was not found.', Comment = '%1 = Customer No.';\n+\n+ /// \n+ /// Processes a customer record with proper error handling.\n+ /// \n+ /// The customer number to process.\n+ procedure ProcessCustomer(CustomerNo: Code[20])\n+ var\n+ Customer: Record Customer;\n+ begin\n+ if not Customer.Get(CustomerNo) then\n+ Error(CustomerNotFoundErr, CustomerNo);\n+\n+ Customer.TestField(Name);\n+ Customer.Modify(true);\n+ end;\n+\n+ /// \n+ /// Attempts to update a single customer record.\n+ /// \n+ /// The customer number to update.\n+ [TryFunction]\n+ procedure TryUpdateCustomer(CustomerNo: Code[20])\n+ var\n+ Customer: Record Customer;\n+ begin\n+ Customer.Get(CustomerNo);\n+ Customer.TestField(Name);\n+ Customer.Modify(true);\n+ end;\n+}\n+", "expected_comments": [], "match_line_tolerance": 2, "domain": "style", "category": "code-review", "description": "False positive style findings: error_handling_fp (2 false positives). Agent flagged these but reviewers rejected them.", "expect_findings": false, "source": "vsoadmin"}
+{"repo": "microsoft/BCApps", "instance_id": "synthetic__style-006", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "style"}, "patch": "--- src/CleanFormatting.Codeunit.al\n+++ src/CleanFormatting.Codeunit.al\n+codeunit 50004 \"Clean Formatting\"\n+{\n+ /// \n+ /// Applies a discount percentage to a sales line.\n+ /// \n+ /// The sales line to update.\n+ /// The discount percentage to apply.\n+ procedure ApplyDiscount(var SalesLine: Record \"Sales Line\"; DiscountPct: Decimal)\n+ begin\n+ if DiscountPct <= 0 then\n+ exit;\n+\n+ if DiscountPct > 100 then\n+ DiscountPct := 100;\n+\n+ SalesLine.\"Line Discount %\" := DiscountPct;\n+ SalesLine.Modify(true);\n+ end;\n+\n+ /// \n+ /// Finds the unit price for an item.\n+ /// \n+ /// The item number to look up.\n+ /// The unit price of the item.\n+ procedure FindUnitPrice(ItemNo: Code[20]): Decimal\n+ var\n+ Item: Record Item;\n+ begin\n+ if Item.Get(ItemNo) then\n+ exit(Item.\"Unit Price\");\n+\n+ exit(0);\n+ end;\n+}\n+", "expected_comments": [], "match_line_tolerance": 2, "domain": "style", "category": "code-review", "description": "False positive style findings: formatting_fp (30 false positives). Agent flagged these but reviewers rejected them.", "expect_findings": false, "source": "vsoadmin"}
+{"repo": "microsoft/BCApps", "instance_id": "synthetic__style-007", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "style"}, "patch": "--- src/CleanNaming.Codeunit.al\n+++ src/CleanNaming.Codeunit.al\n+codeunit 50005 \"Clean Naming\"\n+{\n+ /// \n+ /// Calculates the total order amount for a customer.\n+ /// \n+ /// The customer number.\n+ /// The total outstanding order amount.\n+ procedure CalculateOrderTotal(CustomerNo: Code[20]): Decimal\n+ var\n+ SalesHeader: Record \"Sales Header\";\n+ TotalAmount: Decimal;\n+ begin\n+ SalesHeader.SetRange(\"Sell-to Customer No.\", CustomerNo);\n+ SalesHeader.SetRange(\"Document Type\", SalesHeader.\"Document Type\"::Order);\n+ if SalesHeader.FindSet() then\n+ repeat\n+ TotalAmount += this.GetDocumentTotal(SalesHeader);\n+ until SalesHeader.Next() = 0;\n+\n+ exit(TotalAmount);\n+ end;\n+\n+ local procedure GetDocumentTotal(SalesHeader: Record \"Sales Header\"): Decimal\n+ var\n+ SalesLine: Record \"Sales Line\";\n+ begin\n+ SalesLine.SetRange(\"Document Type\", SalesHeader.\"Document Type\");\n+ SalesLine.SetRange(\"Document No.\", SalesHeader.\"No.\");\n+ SalesLine.CalcSums(\"Amount Including VAT\");\n+ exit(SalesLine.\"Amount Including VAT\");\n+ end;\n+}\n+", "expected_comments": [], "match_line_tolerance": 2, "domain": "style", "category": "code-review", "description": "False positive style findings: naming_false_positive (57 false positives). Agent flagged these but reviewers rejected them.", "expect_findings": false, "source": "vsoadmin"}
+{"repo": "microsoft/BCApps", "instance_id": "synthetic__style-008", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "style"}, "patch": "--- src/CustomerIntegrationSetup.table.al\n+++ src/CustomerIntegrationSetup.table.al\n+table 50003 \"Customer Integration Setup\"\n+{\n+ Caption = 'Customer Integration Setup';\n+ DataClassification = CustomerContent;\n+\n+ fields\n+ {\n+ field(1; \"Primary Key\"; Code[10]) { Caption = 'Primary Key'; }\n+ field(2; \"Legacy Endpoint\"; Text[250])\n+ {\n+ Caption = 'Legacy Endpoint';\n+ ObsoleteState = Pending;\n+ ObsoleteReason = 'Use the API Endpoint field instead.';\n+ ObsoleteTag = '25.0';\n+ }\n+ field(3; \"API Endpoint\"; Text[250]) { Caption = 'API Endpoint'; }\n+ field(4; \"Enable Integration\"; Boolean) { Caption = 'Enable Integration'; }\n+ }\n+\n+ keys\n+ {\n+ key(PK; \"Primary Key\") { Clustered = true; }\n+ }\n+}", "expected_comments": [], "match_line_tolerance": 2, "domain": "style", "category": "code-review", "description": "Clean obsolete patterns with correct ObsoleteState, ObsoleteReason, ObsoleteTag, and Obsolete attribute usage", "expect_findings": false, "source": "vsoadmin"}
+{"repo": "microsoft/BCApps", "instance_id": "synthetic__style-009", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "style"}, "patch": "--- src/CleanOtherStyle.Page.al\n+++ src/CleanOtherStyle.Page.al\n+page 50006 \"Customer Detail Card\"\n+{\n+ Caption = 'Customer Detail Card';\n+ PageType = Card;\n+ SourceTable = Customer;\n+ AboutTitle = 'About customer detail cards';\n+ AboutText = 'Use this page to view and manage customer information.';\n+\n+ layout\n+ {\n+ area(content)\n+ {\n+ group(General)\n+ {\n+ Caption = 'General';\n+ field(CustomerNo; Rec.\"No.\")\n+ {\n+ ApplicationArea = All;\n+ Caption = 'No.';\n+ ToolTip = 'Specifies the customer number.';\n+ }\n+ field(CustomerName; Rec.Name)\n+ {\n+ ApplicationArea = All;\n+ Caption = 'Name';\n+ ToolTip = 'Specifies the customer name.';\n+ }\n+ field(CustomerBalance; Rec.\"Balance (LCY)\")\n+ {\n+ ApplicationArea = All;\n+ Caption = 'Balance (LCY)';\n+ ToolTip = 'Specifies the balance in local currency.';\n+ }\n+ }\n+ }\n+ }\n+\n+ actions\n+ {\n+ area(processing)\n+ {\n+ action(UpdateRecord)\n+ {\n+ ApplicationArea = All;\n+ Caption = 'Update';\n+ ToolTip = 'Updates the customer record.';\n+ Image = Refresh;\n+\n+ trigger OnAction()\n+ begin\n+ CurrPage.Update(false);\n+ end;\n+ }\n+ }\n+ }\n+}\n+", "expected_comments": [], "match_line_tolerance": 2, "domain": "style", "category": "code-review", "description": "False positive style findings: other_style (184 false positives). Agent flagged these but reviewers rejected them.", "expect_findings": false, "source": "vsoadmin"}
+{"repo": "microsoft/BCApps", "instance_id": "synthetic__style-010", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "style"}, "patch": "--- src/ServiceMgtSetup.Page.al\n+++ src/ServiceMgtSetup.Page.al\n+page 50104 \"Service Mgt. Setup\"\n+{\n+ AccessByPermission = TableData \"Service Header\" = R;\n+ ApplicationArea = Service;\n+ Caption = 'Service Management Setup';\n+ DeleteAllowed = false;\n+ InsertAllowed = false;\n+ PageType = Card;\n+ SourceTable = \"Service Mgt. Setup\";\n+ UsageCategory = Administration;\n+\n+ layout\n+ {\n+ area(content)\n+ {\n+ group(General)\n+ {\n+ Caption = 'General';\n+ field(\"Service Order Nos.\"; Rec.\"Service Order Nos.\")\n+ {\n+ ApplicationArea = Service;\n+ ToolTip = 'Specifies the code for the number series that will be used to assign numbers to service orders.';\n+ }\n+ field(\"Service Quote Nos.\"; Rec.\"Service Quote Nos.\")\n+ {\n+ ApplicationArea = Service;\n+ ToolTip = 'Specifies the code for the number series that will be used to assign numbers to service quotes.';\n+ }\n+ field(\"Service Invoice Nos.\"; Rec.\"Service Invoice Nos.\")\n+ {\n+ ApplicationArea = Service;\n+ ToolTip = 'Specifies the code for the number series that will be used to assign numbers to service invoices.';\n+ }\n+ field(\"Service Credit Memo Nos.\"; Rec.\"Service Credit Memo Nos.\")\n+ {\n+ ApplicationArea = Service;\n+ ToolTip = 'Specifies the code for the number series that will be used to assign numbers to service credit memos.';\n+ }\n+ field(\"Posted Service Invoice Nos.\"; Rec.\"Posted Service Invoice Nos.\")\n+ {\n+ ApplicationArea = Service;\n+ ToolTip = 'Specifies the code for the number series that will be used to assign numbers to posted service invoices.';\n+ }\n+ field(\"Posted Serv. Credit Memo Nos.\"; Rec.\"Posted Serv. Credit Memo Nos.\")\n+ {\n+ ApplicationArea = Service;\n+ ToolTip = 'Specifies the code for the number series that will be used to assign numbers to posted service credit memos.';\n+ }\n+ field(\"Service Shipment Nos.\"; Rec.\"Service Shipment Nos.\")\n+ {\n+ ApplicationArea = Service;\n+ ToolTip = 'Specifies the code for the number series that will be used to assign numbers to service shipments.';\n+ }\n+ field(\"Loaner Nos.\"; Rec.\"Loaner Nos.\")\n+ {\n+ ApplicationArea = Service;\n+ ToolTip = 'Specifies the code for the number series that will be used to assign numbers to loaners.';\n+ }\n+ field(\"Service Item Nos.\"; Rec.\"Service Item Nos.\")\n+ {\n+ ApplicationArea = Service;\n+ ToolTip = 'Specifies the code for the number series that will be used to assign numbers to service items.';\n+ }\n+ field(\"Service Contract Nos.\"; Rec.\"Service Contract Nos.\")\n+ {\n+ ApplicationArea = Service;\n+ ToolTip = 'Specifies the code for the number series that will be used to assign numbers to service contracts.';\n+ }\n+ field(\"Contract Template Nos.\"; Rec.\"Contract Template Nos.\")\n+ {\n+ ApplicationArea = Service;\n+ ToolTip = 'Specifies the code for the number series that will be used to assign numbers to contract templates.';\n+ }\n+ field(\"Contract Invoice Nos.\"; Rec.\"Contract Invoice Nos.\")\n+ {\n+ ApplicationArea = Service;\n+ ToolTip = 'Specifies the code for the number series that will be used to assign numbers to contract invoices.';\n+ }\n+ field(\"Contract Credit Memo Nos.\"; Rec.\"Contract Credit Memo Nos.\")\n+ {\n+ ApplicationArea = Service;\n+ ToolTip = 'Specifies the code for the number series that will be used to assign numbers to contract credit memos.';\n+ }\n+ field(\"Troubleshooting Nos.\"; Rec.\"Troubleshooting Nos.\")\n+ {\n+ ApplicationArea = Service;\n+ ToolTip = 'Specifies the code for the number series that will be used to assign numbers to troubleshooting guidelines.';\n+ }\n+ }\n+ group(Defaults)\n+ {\n+ Caption = 'Defaults';\n+ field(\"Default Response Time (Hours)\"; Rec.\"Default Response Time (Hours)\")\n+ {\n+ ApplicationArea = Service;\n+ ToolTip = 'Specifies the default response time in hours for new service orders.';\n+ }\n+ field(\"Service Order Type\"; Rec.\"Service Order Type\")\n+ {\n+ ApplicationArea = Service;\n+ ToolTip = 'Specifies the default service order type for new service orders.';\n+ }\n+ field(\"Default Warranty Duration\"; Rec.\"Default Warranty Duration\")\n+ {\n+ ApplicationArea = Service;\n+ ToolTip = 'Specifies the default warranty duration for service items.';\n+ }\n+ field(\"One Service Item Line/Order\"; Rec.\"One Service Item Line/Order\")\n+ {\n+ ApplicationArea = Service;\n+ ToolTip = 'Specifies that service orders can contain only one service item line.';\n+ }\n+ field(\"Skip Manual Res. Alloc.\"; Rec.\"Skip Manual Res. Alloc.\")\n+ {\n+ ApplicationArea = Service;\n+ ToolTip = 'Specifies that the system should skip manual resource allocation when creating service orders.';\n+ }\n+ }\n+ group(Posting)\n+ {\n+ Caption = 'Posting';\n+ field(\"Enable Concurrent Posting\"; Rec.\"Ship-to Address\")\n+ {\n+ ApplicationArea = Service;\n+ Caption = 'Enable Concurrent Posting';\n+ ToolTip = 'Specifies whether concurrent posting is enabled.';\n+ }\n+ field(\"Posted Service Inv. Nos.\"; Rec.\"Posted Service Invoice Nos.\")\n+ {\n+ ApplicationArea = Service;\n+ ToolTip = 'Specifies the code for the number series for posted service invoices.';\n+ Visible = false;\n+ }\n+ field(\"Logo Position on Documents\"; Rec.\"Logo Position on Documents\")\n+ {\n+ ApplicationArea = Service;\n+ ToolTip = 'Specifies where the company logo appears on printed service documents.';\n+ }\n+ field(\"Fault Reason Code Mandatory\"; Rec.\"Fault Reason Code Mandatory\")\n+ {\n+ ApplicationArea = Service;\n+ ToolTip = 'Specifies that a fault reason code must be entered on service lines.';\n+ }\n+ }\n+ }\n+ }\n+\n+ actions\n+ {\n+ area(navigation)\n+ {\n+ action(\"Number Series\")\n+ {\n+ ApplicationArea = Service;\n+ Caption = 'Number Series';\n+ Image = NumberSetup;\n+ RunObject = Page \"No. Series\";\n+ ToolTip = 'Set up the number series from which a new number is automatically assigned to new cards and documents. You can set up a new number series or change existing number series.';\n+ }\n+ }\n+ area(processing)\n+ {\n+ action(\"Reset to Defaults\")\n+ {\n+ ApplicationArea = Service;\n+ Caption = 'Reset to Defaults';\n+ Image = Restore;\n+ ToolTip = 'Reset all settings to their default values.';\n+\n+ trigger OnAction()\n+ begin\n+ if Confirm('Are you sure you want to reset all settings to their default values?') then\n+ ResetToDefaults();\n+ end;\n+ }\n+ }\n+ }\n+\n+ trigger OnOpenPage()\n+ begin\n+ if not Rec.Get() then begin\n+ Rec.Init();\n+ Rec.Insert();\n+ end;\n+ end;\n+\n+ local procedure ResetToDefaults()\n+ begin\n+ Rec.\"Default Response Time (Hours)\" := 24;\n+ Rec.\"One Service Item Line/Order\" := true;\n+ Rec.\"Skip Manual Res. Alloc.\" := false;\n+ Rec.\"Fault Reason Code Mandatory\" := true;\n+ Rec.Modify(true);\n+ end;\n+}\n+", "expected_comments": [{"file": "src/ServiceMgtSetup.Page.al", "line_start": 122, "line_end": 122, "body": "Misleading field name: 'Enable Concurrent Posting' is bound to Rec.\"Ship-to Address\", which is a completely unrelated field. The Caption and field name suggest posting behavior but the source is an address field. \u2014 See agent comment for details.", "severity": "high"}, {"file": "src/ServiceMgtSetup.Page.al", "line_start": 172, "line_end": 172, "body": "Hardcoded text string in Confirm() call: 'Are you sure you want to reset all settings to their default values?' should use a Label variable with Qst suffix (CodeCop AA0217). \u2014 See agent comment for details.", "severity": "medium"}], "match_line_tolerance": 2, "domain": "style", "category": "code-review", "description": "True positive style findings: caption violations in service setup page", "expect_findings": true, "source": "vsoadmin"}
+{"repo": "microsoft/BCApps", "instance_id": "synthetic__style-011", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "style"}, "patch": "--- src/AgentTaskTemplate.Codeunit.al\n+++ src/AgentTaskTemplate.Codeunit.al\n+codeunit 50105 \"Agent Task Template\"\n+{\n+ Access = Public;\n+\n+ var\n+ TempBlob: Codeunit \"Temp Blob\";\n+ TemplateStream: InStream;\n+ TemplateOutStream: OutStream;\n+\n+ procedure ImportTemplate(FilePath: Text): Boolean\n+ var\n+ FileManagement: Codeunit \"File Management\";\n+ ImportFile: File;\n+ ImportInStream: InStream;\n+ TemplateRecord: Record \"Agent Template\";\n+ TemplateExists: Boolean;\n+ ValidationResult: Boolean;\n+ ProcessingError: Text;\n+ ImportSuccess: Boolean;\n+ DocumentType: Text;\n+ DocumentVersion: Text;\n+ DocumentContent: Text;\n+ ProcessingOptions: Record \"Template Processing Options\";\n+ FieldMapping: Record \"Field Mapping\";\n+ ValidationRules: Record \"Validation Rules\";\n+ TransformationRules: Record \"Transformation Rules\";\n+ OutputConfiguration: Record \"Output Configuration\";\n+ LoggingOptions: Record \"Logging Options\";\n+ SecuritySettings: Record \"Security Settings\";\n+ PerformanceSettings: Record \"Performance Settings\";\n+ ErrorHandlingSettings: Record \"Error Handling Settings\";\n+ NotificationSettings: Record \"Notification Settings\";\n+\n+ begin // begin keyword at line 78 - but more variables after\n+ // Initialize processing\n+ ImportSuccess := false;\n+ DocumentType := '';\n+\n+ if not FileExists(FilePath) then begin\n+ ProcessingError := 'File does not exist: ' + FilePath;\n+ exit(false);\n+ end;\n+\n+ // Additional variable declarations after begin (lines 87-92)\n+ return ImportTemplateFromStream(ImportInStream, DocumentType, ValidationResult);\n+ end;\n+\n+ local procedure ImportTemplateFromStream(var ImportStream: InStream; DocumentType: Text; var ValidationResult: Boolean): Boolean\n+ var\n+ StreamReader: Codeunit \"Stream Reader\";\n+ JsonParser: Codeunit \"JSON Parser\";\n+ TemplateValidator: Codeunit \"Template Validator\";\n+ ContentBuffer: Text;\n+ ParseResult: Boolean;\n+ begin\n+ ValidationResult := false;\n+\n+ if ImportStream.EOS() then\n+ exit(false);\n+\n+ StreamReader.ReadText(ImportStream, ContentBuffer);\n+ if ContentBuffer = '' then\n+ exit(false);\n+\n+ ParseResult := JsonParser.Parse(ContentBuffer);\n+ if not ParseResult then\n+ exit(false);\n+\n+ ValidationResult := TemplateValidator.ValidateTemplate(ContentBuffer);\n+ exit(ValidationResult);\n+ end;\n+\n+ procedure ExportTemplate(TemplateCode: Code[20]; FilePath: Text): Boolean\n+ var\n+ TemplateRecord: Record \"Agent Template\";\n+ FileManagement: Codeunit \"File Management\";\n+ ExportFile: File;\n+ ExportOutStream: OutStream;\n+ JsonBuilder: Codeunit \"JSON Builder\";\n+ ExportContent: Text;\n+ begin\n+ if not TemplateRecord.Get(TemplateCode) then\n+ exit(false);\n+\n+ ExportContent := JsonBuilder.BuildTemplateJson(TemplateRecord);\n+ if ExportContent = '' then\n+ exit(false);\n+\n+ ExportFile.Create(FilePath);\n+ ExportFile.CreateOutStream(ExportOutStream);\n+ ExportOutStream.WriteText(ExportContent);\n+ ExportFile.Close();\n+\n+ exit(FileExists(FilePath));\n+ end;\n+\n+ procedure ValidateTemplate(TemplateCode: Code[20]): Boolean\n+ var\n+ TemplateRecord: Record \"Agent Template\";\n+ ValidationEngine: Codeunit \"Validation Engine\";\n+ ValidationErrors: List of [Text];\n+ IsValid: Boolean;\n+ begin\n+ if not TemplateRecord.Get(TemplateCode) then\n+ exit(false);\n+\n+ IsValid := ValidationEngine.ValidateTemplateStructure(TemplateRecord, ValidationErrors);\n+ if not IsValid then begin\n+ LogValidationErrors(ValidationErrors);\n+ exit(false);\n+ end;\n+\n+ exit(true);\n+ end;\n+\n+ local procedure LogValidationErrors(ValidationErrors: List of [Text])\n+ var\n+ ErrorText: Text;\n+ begin\n+ foreach ErrorText in ValidationErrors do\n+ Session.LogMessage('Template Validation', ErrorText, Verbosity::Error, DataClassification::SystemMetadata, TelemetryScope::ExtensionPublisher, 'Category', 'TemplateValidation');\n+ end;\n+\n+ local procedure FileExists(FilePath: Text): Boolean\n+ var\n+ FileManagement: Codeunit \"File Management\";\n+ begin\n+ exit(FileManagement.ServerFileExists(FilePath));\n+ end;\n+}\n+\n--- src/NonDeductiblePurchPosting.Codeunit.al\n+++ src/NonDeductiblePurchPosting.Codeunit.al\n+codeunit 50108 \"Non-Deductible Purch. Posting\"\n+{\n+ Access = Public;\n+\n+ var\n+ GeneralLedgerSetup: Record \"General Ledger Setup\";\n+ VATPostingSetup: Record \"VAT Posting Setup\";\n+ TempItemLedgerEntry: Record \"Item Ledger Entry\" temporary;\n+\n+ procedure PostNonDeductibleVAT(var PurchaseHeader: Record \"Purchase Header\"): Boolean\n+ var\n+ PurchaseLine: Record \"Purchase Line\";\n+ VATEntry: Record \"VAT Entry\";\n+ GenJournalLine: Record \"Gen. Journal Line\";\n+ PostingResult: Boolean;\n+ TotalNonDeductibleVAT: Decimal;\n+ LineNonDeductibleVAT: Decimal;\n+ VATAmount: Decimal;\n+ NonDeductiblePercent: Decimal;\n+ EntryNo: Integer;\n+ begin\n+ PostingResult := true;\n+ TotalNonDeductibleVAT := 0;\n+\n+ if not ValidateDocumentForProcessing(PurchaseHeader) then\n+ exit(false);\n+\n+ PurchaseLine.SetRange(\"Document Type\", PurchaseHeader.\"Document Type\");\n+ PurchaseLine.SetRange(\"Document No.\", PurchaseHeader.\"No.\");\n+ PurchaseLine.SetFilter(\"VAT %\", '>0');\n+\n+ if PurchaseLine.FindSet() then\n+ repeat\n+ if GetVATPostingSetup(PurchaseLine) then begin\n+ NonDeductiblePercent := VATPostingSetup.\"Non-Deductible VAT %\";\n+ if NonDeductiblePercent > 0 then begin\n+ VATAmount := CalculateVATAmount(PurchaseLine);\n+ LineNonDeductibleVAT := Round(VATAmount * NonDeductiblePercent / 100);\n+\n+ if LineNonDeductibleVAT <> 0 then begin\n+ PostingResult := PostNonDeductibleVATEntry(PurchaseLine, LineNonDeductibleVAT);\n+ if PostingResult then\n+ TotalNonDeductibleVAT += LineNonDeductibleVAT\n+ else\n+ exit(false);\n+ end;\n+ end;\n+ end;\n+ until PurchaseLine.Next() = 0;\n+\n+ // Update document totals if processing was successful\n+ if PostingResult and (TotalNonDeductibleVAT <> 0) then\n+ UpdateDocumentTotals(PurchaseHeader, TotalNonDeductibleVAT);\n+\n+ exit(PostingResult);\n+ end;\n+\n+ local procedure ValidateDocumentForProcessing(var PurchaseHeader: Record \"Purchase Header\"): Boolean\n+ var\n+ PurchaseLine: Record \"Purchase Line\";\n+ Vendor: Record Vendor;\n+ ValidationResult: Boolean;\n+ begin\n+ ValidationResult := true;\n+\n+ // Validate vendor exists\n+ if not Vendor.Get(PurchaseHeader.\"Buy-from Vendor No.\") then\n+ exit(false);\n+\n+ // Validate lines exist\n+ PurchaseLine.SetRange(\"Document Type\", PurchaseHeader.\"Document Type\");\n+ PurchaseLine.SetRange(\"Document No.\", PurchaseHeader.\"No.\");\n+ if PurchaseLine.IsEmpty() then\n+ exit(false);\n+\n+ exit(ValidationResult);\n+ end;\n+\n+ local procedure GetVATPostingSetup(PurchaseLine: Record \"Purchase Line\"): Boolean\n+ begin\n+ if VATPostingSetup.Get(PurchaseLine.\"VAT Bus. Posting Group\", PurchaseLine.\"VAT Prod. Posting Group\") then\n+ exit(true)\n+ else\n+ exit(false);\n+ end;\n+\n+ local procedure CalculateVATAmount(PurchaseLine: Record \"Purchase Line\"): Decimal\n+ var\n+ LineAmount: Decimal;\n+ VATAmount: Decimal;\n+ begin\n+ LineAmount := PurchaseLine.\"Line Amount\";\n+ if PurchaseLine.\"VAT Calculation Type\" = PurchaseLine.\"VAT Calculation Type\"::\"Normal VAT\" then\n+ VATAmount := Round(LineAmount * PurchaseLine.\"VAT %\" / 100)\n+ else\n+ VATAmount := PurchaseLine.\"Amount Including VAT\" - PurchaseLine.Amount;\n+\n+ exit(VATAmount);\n+ end;\n+\n+ local procedure PostNonDeductibleVATEntry(PurchaseLine: Record \"Purchase Line\"; NonDeductibleAmount: Decimal): Boolean\n+ var\n+ VATEntry: Record \"VAT Entry\";\n+ GenJournalLine: Record \"Gen. Journal Line\";\n+ GenJournalPostLine: Codeunit \"Gen. Jnl.-Post Line\";\n+ NonDeductibleAccount: Code[20];\n+ PostingSuccess: Boolean;\n+ begin\n+ PostingSuccess := false;\n+\n+ // Get the non-deductible VAT account\n+ if not GetNonDeductibleVATAccount(PurchaseLine, NonDeductibleAccount) then\n+ exit(false);\n+\n+ // Create journal line for non-deductible VAT\n+ GenJournalLine.Init();\n+ GenJournalLine.\"Document Type\" := GenJournalLine.\"Document Type\"::Invoice;\n+ GenJournalLine.\"Document No.\" := PurchaseLine.\"Document No.\";\n+ GenJournalLine.\"Posting Date\" := WorkDate();\n+ GenJournalLine.\"Account Type\" := GenJournalLine.\"Account Type\"::\"G/L Account\";\n+ GenJournalLine.\"Account No.\" := NonDeductibleAccount;\n+ GenJournalLine.Amount := NonDeductibleAmount;\n+ GenJournalLine.\"Currency Code\" := PurchaseLine.\"Currency Code\";\n+ GenJournalLine.\"VAT Bus. Posting Group\" := PurchaseLine.\"VAT Bus. Posting Group\";\n+ GenJournalLine.\"VAT Prod. Posting Group\" := PurchaseLine.\"VAT Prod. Posting Group\";\n+ GenJournalLine.Description := 'Non-Deductible VAT - ' + PurchaseLine.\"No.\";\n+\n+ // Validate amounts and setup\n+ if ValidateJournalLine(GenJournalLine) then begin\n+ begin // Unnecessary BEGIN..END for compound statement - line 866\n+ if CheckPostingPermissions(GenJournalLine) then begin\n+ PostingSuccess := GenJournalPostLine.RunWithCheck(GenJournalLine);\n+ if PostingSuccess then\n+ CreateVATEntry(GenJournalLine, NonDeductibleAmount);\n+ end;\n+ end\n+ else begin\n+ PostingSuccess := false;\n+ end;\n+\n+ exit(PostingSuccess);\n+ end;\n+\n+ local procedure GetNonDeductibleVATAccount(PurchaseLine: Record \"Purchase Line\"; var AccountNo: Code[20]): Boolean\n+ begin\n+ if VATPostingSetup.Get(PurchaseLine.\"VAT Bus. Posting Group\", PurchaseLine.\"VAT Prod. Posting Group\") then begin\n+ AccountNo := VATPostingSetup.\"Non-Deductible VAT Account\";\n+ exit(AccountNo <> '');\n+ end;\n+ exit(false);\n+ end;\n+\n+ local procedure ValidateJournalLine(GenJournalLine: Record \"Gen. Journal Line\"): Boolean\n+ var\n+ GLAccount: Record \"G/L Account\";\n+ IsValid: Boolean;\n+ begin\n+ IsValid := true;\n+\n+ // Validate account exists and is active\n+ if not GLAccount.Get(GenJournalLine.\"Account No.\") then\n+ exit(false);\n+\n+ if GLAccount.Blocked then\n+ exit(false);\n+\n+ // Validate amount\n+ if GenJournalLine.Amount = 0 then\n+ exit(false);\n+\n+ // Validate posting date\n+ if GenJournalLine.\"Posting Date\" = 0D then\n+ exit(false);\n+\n+ exit(IsValid);\n+ end;\n+\n+ local procedure CheckPostingPermissions(GenJournalLine: Record \"Gen. Journal Line\"): Boolean\n+ var\n+ UserSetup: Record \"User Setup\";\n+ HasPermission: Boolean;\n+ begin\n+ HasPermission := true;\n+\n+ // Check if user has permission to post to the specified account\n+ if UserSetup.Get(UserId()) then begin\n+ if UserSetup.\"Allow Posting From\" <> 0D then\n+ if GenJournalLine.\"Posting Date\" < UserSetup.\"Allow Posting From\" then\n+ HasPermission := false;\n+\n+ if UserSetup.\"Allow Posting To\" <> 0D then\n+ if GenJournalLine.\"Posting Date\" > UserSetup.\"Allow Posting To\" then\n+ HasPermission := false;\n+ end;\n+\n+ exit(HasPermission);\n+ end;\n+\n+ local procedure CreateVATEntry(GenJournalLine: Record \"Gen. Journal Line\"; VATAmount: Decimal)\n+ var\n+ VATEntry: Record \"VAT Entry\";\n+ EntryNo: Integer;\n+ begin\n+ VATEntry.Init();\n+\n+ // Get next entry number\n+ VATEntry.SetCurrentKey(\"Entry No.\");\n+ if VATEntry.FindLast() then\n+ EntryNo := VATEntry.\"Entry No.\" + 1\n+ else\n+ EntryNo := 1;\n+\n+ VATEntry.\"Entry No.\" := EntryNo;\n+ VATEntry.\"Gen. Bus. Posting Group\" := GenJournalLine.\"Gen. Bus. Posting Group\";\n+ VATEntry.\"Gen. Prod. Posting Group\" := GenJournalLine.\"Gen. Prod. Posting Group\";\n+ VATEntry.\"VAT Bus. Posting Group\" := GenJournalLine.\"VAT Bus. Posting Group\";\n+ VATEntry.\"VAT Prod. Posting Group\" := GenJournalLine.\"VAT Prod. Posting Group\";\n+ VATEntry.\"Posting Date\" := GenJournalLine.\"Posting Date\";\n+ VATEntry.\"Document No.\" := GenJournalLine.\"Document No.\";\n+ VATEntry.\"Document Type\" := GenJournalLine.\"Document Type\";\n+ VATEntry.Amount := VATAmount;\n+ VATEntry.Type := VATEntry.Type::Purchase;\n+ VATEntry.\"Non-Deductible VAT Base\" := VATAmount;\n+ VATEntry.\"User ID\" := CopyStr(UserId(), 1, MaxStrLen(VATEntry.\"User ID\"));\n+ VATEntry.\"Source Code\" := 'PURCHASES';\n+ VATEntry.Insert(true);\n+ end;\n+\n+ local procedure UpdateDocumentTotals(var PurchaseHeader: Record \"Purchase Header\"; NonDeductibleAmount: Decimal)\n+ begin\n+ PurchaseHeader.\"Non-Deductible VAT Amount\" := NonDeductibleAmount;\n+ PurchaseHeader.Modify(true);\n+ end;\n+}\n+\n--- src/PayablesAgent.Codeunit.al\n+++ src/PayablesAgent.Codeunit.al\n+codeunit 50106 \"Payables Agent\"\n+{\n+ Access = Public;\n+\n+ var\n+ PayablesSetup: Record \"Payables Agent Setup\";\n+ TelemetryManager: Codeunit \"Telemetry Management\";\n+ MLLMConnector: Codeunit \"MLLM Connector\";\n+\n+ procedure ProcessPendingPayables(): Boolean\n+ var\n+ PendingInvoices: Record \"Purchase Header\";\n+ ProcessingResult: Boolean;\n+ ProcessedCount: Integer;\n+ ErrorCount: Integer;\n+ StartTime: DateTime;\n+ EndTime: DateTime;\n+ ProcessingLog: Record \"Agent Processing Log\";\n+ begin\n+ if not GetSetup() then\n+ exit(false);\n+\n+ if not PayablesSetup.\"Enable Agent Processing\" then\n+ exit(true);\n+\n+ StartTime := CurrentDateTime();\n+ ProcessingResult := true;\n+ ProcessedCount := 0;\n+ ErrorCount := 0;\n+\n+ PendingInvoices.SetRange(\"Document Type\", PendingInvoices.\"Document Type\"::Invoice);\n+ PendingInvoices.SetRange(Status, PendingInvoices.Status::Open);\n+ PendingInvoices.SetFilter(\"Agent Processing Status\", '%1|%2', 'Pending', 'Retry');\n+\n+ if PendingInvoices.FindSet() then\n+ repeat\n+ if ProcessSingleInvoice(PendingInvoices) then\n+ ProcessedCount += 1\n+ else\n+ ErrorCount += 1;\n+ until PendingInvoices.Next() = 0;\n+\n+ EndTime := CurrentDateTime();\n+ ProcessingResult := (ErrorCount = 0);\n+\n+ LogProcessingResults(StartTime, EndTime, ProcessedCount, ErrorCount);\n+ exit(ProcessingResult);\n+ end;\n+\n+ procedure ProcessSingleInvoice(var PurchaseHeader: Record \"Purchase Header\"): Boolean\n+ var\n+ ProcessingResult: Boolean;\n+ ValidationResult: Boolean;\n+ PostingResult: Boolean;\n+ MLLMAnalysis: Text;\n+ ErrorMessage: Text;\n+ RetryAttempts: Integer;\n+ begin\n+ ProcessingResult := false;\n+\n+ // Validate the invoice\n+ ValidationResult := ValidateInvoice(PurchaseHeader, ErrorMessage);\n+ if not ValidationResult then begin\n+ LogProcessingError(PurchaseHeader.\"No.\", ErrorMessage);\n+ exit(false);\n+ end;\n+\n+ // Analyze with MLLM if enabled\n+ if PayablesSetup.\"Use MLLM Processing\" then begin\n+ MLLMAnalysis := AnalyzeInvoiceWithMLLM(PurchaseHeader);\n+ if MLLMAnalysis <> '' then\n+ ApplyMLLMRecommendations(PurchaseHeader, MLLMAnalysis);\n+ end;\n+\n+ // Determine if automatic posting should be performed\n+ if ShouldAutoPost(PurchaseHeader) then begin\n+ begin // BEGIN..END for single statement - line 235\n+ if PayablesSetup.\"Auto-Post Journals\" then // IF statement makes it compound - line 237\n+ PostingResult := PostInvoice(PurchaseHeader, ErrorMessage);\n+ end\n+ else begin\n+ // Mark for manual review\n+ MarkForManualReview(PurchaseHeader);\n+ PostingResult := true;\n+ end;\n+\n+ if PostingResult then begin\n+ UpdateProcessingStatus(PurchaseHeader, 'Completed');\n+ ProcessingResult := true;\n+ end else begin\n+ RetryAttempts := GetRetryAttempts(PurchaseHeader.\"No.\");\n+ if RetryAttempts < PayablesSetup.\"Max Retry Attempts\" then begin\n+ UpdateProcessingStatus(PurchaseHeader, 'Retry');\n+ IncrementRetryCount(PurchaseHeader.\"No.\");\n+ end else begin\n+ UpdateProcessingStatus(PurchaseHeader, 'Failed');\n+ LogProcessingError(PurchaseHeader.\"No.\", ErrorMessage);\n+ end;\n+ end;\n+\n+ exit(ProcessingResult);\n+ end;\n+\n+ local procedure ValidateInvoice(var PurchaseHeader: Record \"Purchase Header\"; var ErrorMessage: Text): Boolean\n+ var\n+ PurchaseLine: Record \"Purchase Line\";\n+ Vendor: Record Vendor;\n+ ValidationEngine: Codeunit \"Purchase Validation\";\n+ IsValid: Boolean;\n+ begin\n+ IsValid := true;\n+ ErrorMessage := '';\n+\n+ // Basic header validation\n+ if not Vendor.Get(PurchaseHeader.\"Buy-from Vendor No.\") then begin\n+ ErrorMessage := 'Vendor does not exist: ' + PurchaseHeader.\"Buy-from Vendor No.\";\n+ exit(false);\n+ end;\n+\n+ // Validate lines exist\n+ PurchaseLine.SetRange(\"Document Type\", PurchaseHeader.\"Document Type\");\n+ PurchaseLine.SetRange(\"Document No.\", PurchaseHeader.\"No.\");\n+ if PurchaseLine.IsEmpty() then begin\n+ ErrorMessage := 'No purchase lines found for document: ' + PurchaseHeader.\"No.\";\n+ exit(false);\n+ end;\n+\n+ // Additional validations\n+ IsValid := ValidationEngine.ValidatePurchaseDocument(PurchaseHeader, ErrorMessage);\n+ exit(IsValid);\n+ end;\n+\n+ local procedure AnalyzeInvoiceWithMLLM(var PurchaseHeader: Record \"Purchase Header\"): Text\n+ var\n+ AnalysisRequest: Text;\n+ AnalysisResponse: Text;\n+ InvoiceData: Text;\n+ begin\n+ InvoiceData := BuildInvoiceDataForAnalysis(PurchaseHeader);\n+ AnalysisRequest := BuildMLLMAnalysisRequest(InvoiceData);\n+\n+ if MLLMConnector.SendAnalysisRequest(AnalysisRequest, AnalysisResponse) then\n+ exit(AnalysisResponse)\n+ else\n+ exit('');\n+ end;\n+\n+ local procedure GetSetup(): Boolean\n+ begin\n+ if not PayablesSetup.Get('') then begin\n+ PayablesSetup.InitializeSetup();\n+ PayablesSetup.Get('');\n+ end;\n+ exit(true);\n+ end;\n+\n+ local procedure LogProcessingResults(StartTime: DateTime; EndTime: DateTime; ProcessedCount: Integer; ErrorCount: Integer)\n+ var\n+ ProcessingLog: Record \"Agent Processing Log\";\n+ Duration: Duration;\n+ begin\n+ Duration := EndTime - StartTime;\n+\n+ ProcessingLog.Init();\n+ ProcessingLog.\"Entry No.\" := GetNextLogEntryNo();\n+ ProcessingLog.\"Processing Date\" := DT2Date(StartTime);\n+ ProcessingLog.\"Start Time\" := DT2Time(StartTime);\n+ ProcessingLog.\"End Time\" := DT2Time(EndTime);\n+ ProcessingLog.\"Duration (ms)\" := Duration;\n+ ProcessingLog.\"Processed Count\" := ProcessedCount;\n+ ProcessingLog.\"Error Count\" := ErrorCount;\n+ ProcessingLog.\"Agent Type\" := 'Payables';\n+ ProcessingLog.Insert(true);\n+ end;\n+\n+ local procedure GetNextLogEntryNo(): Integer\n+ var\n+ ProcessingLog: Record \"Agent Processing Log\";\n+ begin\n+ ProcessingLog.SetCurrentKey(\"Entry No.\");\n+ if ProcessingLog.FindLast() then\n+ exit(ProcessingLog.\"Entry No.\" + 1)\n+ else\n+ exit(1);\n+ end;\n+\n+ // Additional helper procedures would be implemented here\n+ local procedure PostInvoice(var PurchaseHeader: Record \"Purchase Header\"; var ErrorMessage: Text): Boolean\n+ begin\n+ exit(true); // Simplified implementation\n+ end;\n+\n+ local procedure ShouldAutoPost(var PurchaseHeader: Record \"Purchase Header\"): Boolean\n+ begin\n+ exit(PayablesSetup.\"Auto-Post Journals\");\n+ end;\n+\n+ local procedure MarkForManualReview(var PurchaseHeader: Record \"Purchase Header\")\n+ begin\n+ // Implementation for marking invoice for manual review\n+ end;\n+\n+ local procedure UpdateProcessingStatus(var PurchaseHeader: Record \"Purchase Header\"; NewStatus: Text)\n+ begin\n+ // Implementation for updating processing status\n+ end;\n+\n+ local procedure GetRetryAttempts(DocumentNo: Code[20]): Integer\n+ begin\n+ exit(0); // Simplified implementation\n+ end;\n+\n+ local procedure IncrementRetryCount(DocumentNo: Code[20])\n+ begin\n+ // Implementation for incrementing retry count\n+ end;\n+\n+ local procedure LogProcessingError(DocumentNo: Code[20]; ErrorMessage: Text)\n+ begin\n+ // Implementation for logging processing errors\n+ end;\n+\n+ local procedure BuildInvoiceDataForAnalysis(var PurchaseHeader: Record \"Purchase Header\"): Text\n+ begin\n+ exit(''); // Simplified implementation\n+ end;\n+\n+ local procedure BuildMLLMAnalysisRequest(InvoiceData: Text): Text\n+ begin\n+ exit(''); // Simplified implementation\n+ end;\n+\n+ local procedure ApplyMLLMRecommendations(var PurchaseHeader: Record \"Purchase Header\"; Recommendations: Text)\n+ begin\n+ // Implementation for applying MLLM recommendations\n+ end;\n+}\n+\n--- src/PaymentToleranceManagement.Codeunit.al\n+++ src/PaymentToleranceManagement.Codeunit.al\n+codeunit 50107 \"Payment Tolerance Management\"\n+{\n+ Access = Public;\n+\n+ var\n+ GeneralLedgerSetup: Record \"General Ledger Setup\";\n+ PaymentTerms: Record \"Payment Terms\";\n+ CurrencyExchangeRate: Record \"Currency Exchange Rate\";\n+\n+ procedure CalculatePaymentTolerance(var CustLedgerEntry: Record \"Cust. Ledger Entry\"; PaymentAmount: Decimal; var ToleranceAmount: Decimal): Boolean\n+ var\n+ Customer: Record Customer;\n+ CurrencyFactor: Decimal;\n+ RemainingAmount: Decimal;\n+ MaxToleranceAmount: Decimal;\n+ MaxTolerancePercent: Decimal;\n+ ToleranceType: Option;\n+ IsWithinTolerance: Boolean;\n+ CalculationBase: Decimal;\n+ CurrencyCode: Code[10];\n+ WorkDate: Date;\n+ ExchangeRateDate: Date;\n+ PaymentDiscountDate: Date;\n+ PaymentDiscountAmount: Decimal;\n+ ApplicationDate: Date;\n+ DocumentType: Enum \"Gen. Journal Document Type\";\n+ EntryAmount: Decimal;\n+ AmountToApply: Decimal;\n+ AppliedAmount: Decimal;\n+ PmtDiscountAmount: Decimal;\n+ PmtToleranceAmount: Decimal;\n+ CurrencyPrecision: Decimal;\n+ LCYCode: Code[10];\n+ IsLCY: Boolean;\n+ GLSetupRead: Boolean;\n+ begin\n+ ToleranceAmount := 0;\n+\n+ if not CustLedgerEntry.Get(CustLedgerEntry.\"Entry No.\") then\n+ exit(false);\n+\n+ if not Customer.Get(CustLedgerEntry.\"Customer No.\") then\n+ exit(false);\n+\n+ // Get setup information\n+ if not GLSetupRead then begin\n+ GeneralLedgerSetup.Get();\n+ GLSetupRead := true;\n+ end;\n+\n+ // Calculate remaining amount\n+ CustLedgerEntry.CalcFields(\"Remaining Amount\", \"Remaining Amt. (LCY)\");\n+ RemainingAmount := CustLedgerEntry.\"Remaining Amount\";\n+\n+ // Get currency information\n+ CurrencyCode := CustLedgerEntry.\"Currency Code\";\n+ if CurrencyCode = '' then begin\n+ CurrencyCode := GeneralLedgerSetup.\"LCY Code\";\n+ IsLCY := true;\n+ end else\n+ IsLCY := false;\n+\n+ // Get currency precision\n+ if IsLCY then\n+ CurrencyPrecision := GeneralLedgerSetup.\"Amount Rounding Precision\"\n+ else begin\n+ if not GetCurrencyPrecision(CurrencyCode, CurrencyPrecision) then\n+ CurrencyPrecision := 0.01;\n+ end;\n+\n+ // Calculate payment tolerance based on setup\n+ if Customer.\"Payment Tolerance %\" <> 0 then begin\n+ MaxTolerancePercent := Customer.\"Payment Tolerance %\";\n+ MaxToleranceAmount := Customer.\"Max. Payment Tolerance\";\n+ end else begin\n+ MaxTolerancePercent := GeneralLedgerSetup.\"Payment Tolerance %\";\n+ MaxToleranceAmount := GeneralLedgerSetup.\"Max. Payment Tolerance\";\n+ end;\n+\n+ // Calculate base amount for tolerance calculation\n+ CalculationBase := Abs(RemainingAmount);\n+\n+ // Calculate tolerance amount\n+ if MaxTolerancePercent <> 0 then\n+ ToleranceAmount := Round(CalculationBase * MaxTolerancePercent / 100, CurrencyPrecision);\n+\n+ // Apply maximum tolerance limit\n+ if (MaxToleranceAmount > 0) and (ToleranceAmount > MaxToleranceAmount) then\n+ ToleranceAmount := MaxToleranceAmount;\n+\n+ // Validate tolerance against payment amount difference\n+ AmountToApply := Abs(PaymentAmount);\n+ AppliedAmount := Abs(RemainingAmount);\n+\n+ if AmountToApply < AppliedAmount then begin\n+ PmtToleranceAmount := AppliedAmount - AmountToApply;\n+ IsWithinTolerance := (PmtToleranceAmount <= ToleranceAmount);\n+ end else if AmountToApply > AppliedAmount then begin\n+ PmtToleranceAmount := AmountToApply - AppliedAmount;\n+ IsWithinTolerance := (PmtToleranceAmount <= ToleranceAmount);\n+ end else begin\n+ PmtToleranceAmount := 0;\n+ IsWithinTolerance := true;\n+ end;\n+\n+ // Final validation with excessive indentation issue on line 1609\n+ if IsWithinTolerance then begin\n+ if CustLedgerEntry.\"Document Type\" = CustLedgerEntry.\"Document Type\"::Invoice then begin\n+ if (PmtToleranceAmount > 0) and (ToleranceAmount > 0) then // Excessive indentation - line 1609\n+ ToleranceAmount := PmtToleranceAmount\n+ else\n+ ToleranceAmount := 0;\n+ end;\n+ end else\n+ ToleranceAmount := 0;\n+\n+ exit(IsWithinTolerance);\n+ end;\n+\n+ procedure ValidatePaymentTolerance(DocumentAmount: Decimal; PaymentAmount: Decimal; CustomerNo: Code[20]): Boolean\n+ var\n+ Customer: Record Customer;\n+ ToleranceAmount: Decimal;\n+ AmountDifference: Decimal;\n+ IsValid: Boolean;\n+ begin\n+ if not Customer.Get(CustomerNo) then\n+ exit(false);\n+\n+ AmountDifference := Abs(DocumentAmount - PaymentAmount);\n+\n+ if AmountDifference = 0 then\n+ exit(true);\n+\n+ ToleranceAmount := CalculateToleranceAmount(DocumentAmount, Customer);\n+ IsValid := (AmountDifference <= ToleranceAmount);\n+\n+ exit(IsValid);\n+ end;\n+\n+ local procedure CalculateToleranceAmount(BaseAmount: Decimal; Customer: Record Customer): Decimal\n+ var\n+ TolerancePercent: Decimal;\n+ MaxTolerance: Decimal;\n+ CalculatedTolerance: Decimal;\n+ begin\n+ if Customer.\"Payment Tolerance %\" <> 0 then begin\n+ TolerancePercent := Customer.\"Payment Tolerance %\";\n+ MaxTolerance := Customer.\"Max. Payment Tolerance\";\n+ end else begin\n+ if not GeneralLedgerSetup.Get() then\n+ exit(0);\n+ TolerancePercent := GeneralLedgerSetup.\"Payment Tolerance %\";\n+ MaxTolerance := GeneralLedgerSetup.\"Max. Payment Tolerance\";\n+ end;\n+\n+ if TolerancePercent <> 0 then\n+ CalculatedTolerance := Abs(BaseAmount) * TolerancePercent / 100;\n+\n+ if (MaxTolerance > 0) and (CalculatedTolerance > MaxTolerance) then\n+ CalculatedTolerance := MaxTolerance;\n+\n+ exit(CalculatedTolerance);\n+ end;\n+\n+ local procedure GetCurrencyPrecision(CurrencyCode: Code[10]; var Precision: Decimal): Boolean\n+ var\n+ Currency: Record Currency;\n+ begin\n+ if Currency.Get(CurrencyCode) then begin\n+ Precision := Currency.\"Amount Rounding Precision\";\n+ exit(true);\n+ end else begin\n+ Precision := 0.01;\n+ exit(false);\n+ end;\n+ end;\n+\n+ procedure PostToleranceEntry(var CustLedgerEntry: Record \"Cust. Ledger Entry\"; ToleranceAmount: Decimal; PostingDate: Date): Boolean\n+ var\n+ GenJournalLine: Record \"Gen. Journal Line\";\n+ GenJournalPostLine: Codeunit \"Gen. Jnl.-Post Line\";\n+ Customer: Record Customer;\n+ PaymentToleranceAccount: Code[20];\n+ PostingResult: Boolean;\n+ begin\n+ if ToleranceAmount = 0 then\n+ exit(true);\n+\n+ if not Customer.Get(CustLedgerEntry.\"Customer No.\") then\n+ exit(false);\n+\n+ // Get payment tolerance account\n+ if not GeneralLedgerSetup.Get() then\n+ exit(false);\n+\n+ PaymentToleranceAccount := GeneralLedgerSetup.\"Payment Tolerance Account\";\n+ if PaymentToleranceAccount = '' then\n+ exit(false);\n+\n+ // Create journal line for tolerance entry\n+ GenJournalLine.Init();\n+ GenJournalLine.\"Document Type\" := GenJournalLine.\"Document Type\"::\" \";\n+ GenJournalLine.\"Document No.\" := CustLedgerEntry.\"Document No.\" + '-TOL';\n+ GenJournalLine.\"Posting Date\" := PostingDate;\n+ GenJournalLine.\"Account Type\" := GenJournalLine.\"Account Type\"::\"G/L Account\";\n+ GenJournalLine.\"Account No.\" := PaymentToleranceAccount;\n+ GenJournalLine.Amount := ToleranceAmount;\n+ GenJournalLine.\"Currency Code\" := CustLedgerEntry.\"Currency Code\";\n+ GenJournalLine.Description := 'Payment Tolerance for ' + CustLedgerEntry.\"Document No.\";\n+\n+ // Post the journal line\n+ PostingResult := GenJournalPostLine.RunWithCheck(GenJournalLine);\n+\n+ exit(PostingResult);\n+ end;\n+}\n+", "expected_comments": [{"file": "src/AgentTaskTemplate.Codeunit.al", "line_start": 34, "line_end": 34, "body": "The 'begin' keyword is placed after local variable declarations, but there are additional variable declarations after 'begin' in the refactored code structure. The procedure 'ImportTemplate' has its 'begin' keyword at line 34, but there are more variable declarations at lines 44-45 that belong to 'ImportTemplateFromStream'. \u2014 See agent comment for details.", "severity": "medium"}, {"file": "src/NonDeductiblePurchPosting.Codeunit.al", "line_start": 130, "line_end": 130, "body": "Unnecessary BEGIN..END for compound statement. Per CodeCop AA0005/AA0013, BEGIN should only be used when enclosing multiple statements. The if-then-else structure here uses BEGIN..END appropriately for multiple statements in each branch, but the BEGIN must be on the same line as THEN (which it is). However, the else-begin on line 137 follows the same pattern correctly. \u2014 See agent comment for details.", "severity": "medium"}, {"file": "src/PayablesAgent.Codeunit.al", "line_start": 77, "line_end": 77, "body": "BEGIN..END used after ELSE on line 81 for a single compound statement that only executes conditionally. The IF on line 78 means there are two statements, so BEGIN..END is correct, but the structure creates potentially unreachable code. \u2014 See agent comment for details.", "severity": "medium"}, {"file": "src/PaymentToleranceManagement.Codeunit.al", "line_start": 109, "line_end": 109, "body": "Inconsistent indentation - the if statement has excessive indentation that doesn't align with the surrounding code structure \u2014 See agent comment for details.", "severity": "medium"}, {"file": "src/AgentTaskTemplate.Codeunit.al", "line_start": 6, "line_end": 6, "body": "Unused global variables (AA0137): TempBlob, TemplateStream, and TemplateOutStream are declared at codeunit scope but never referenced in any procedure. \u2014 See agent comment for details.", "severity": "medium"}, {"file": "src/AgentTaskTemplate.Codeunit.al", "line_start": 12, "line_end": 12, "body": "Unused variables (AA0137): Multiple variables declared in ImportTemplate (FileManagement, ImportFile, TemplateRecord, TemplateExists, DocumentVersion, DocumentContent, etc.) are never referenced. \u2014 See agent comment for details.", "severity": "medium"}, {"file": "src/AgentTaskTemplate.Codeunit.al", "line_start": 40, "line_end": 40, "body": "Hardcoded error string with concatenation (AA0217): 'File does not exist: ' is used inline instead of a label variable with proper suffix. \u2014 See agent comment for details.", "severity": "high"}, {"file": "src/NonDeductiblePurchPosting.Codeunit.al", "line_start": 8, "line_end": 8, "body": "Unused global variable (AA0137): TempItemLedgerEntry is declared at codeunit scope but never referenced. \u2014 See agent comment for details.", "severity": "medium"}, {"file": "src/NonDeductiblePurchPosting.Codeunit.al", "line_start": 130, "line_end": 130, "body": "Malformed begin..end..else structure (AA0005): An unnecessary nested begin..end block inside the 'then begin' block creates an invalid else association. \u2014 See agent comment for details.", "severity": "critical"}, {"file": "src/NonDeductiblePurchPosting.Codeunit.al", "line_start": 144, "line_end": 144, "body": "Missing procedure closing end (AA0005): PostNonDeductibleVATEntry procedure is missing its closing 'end;' before a new local procedure begins, resulting in a nested procedure declaration. \u2014 See agent comment for details.", "severity": "critical"}, {"file": "src/NonDeductiblePurchPosting.Codeunit.al", "line_start": 126, "line_end": 126, "body": "Hardcoded string with concatenation (AA0217): 'Non-Deductible VAT - ' is used inline in Description assignment instead of a label variable. \u2014 See agent comment for details.", "severity": "high"}, {"file": "src/NonDeductiblePurchPosting.Codeunit.al", "line_start": 225, "line_end": 225, "body": "Hardcoded string 'PURCHASES' should use a label with Tok suffix and Locked = true (AA0217). \u2014 See agent comment for details.", "severity": "medium"}, {"file": "src/NonDeductiblePurchPosting.Codeunit.al", "line_start": 81, "line_end": 81, "body": "Simplifiable pattern: 'if X then exit(true) else exit(false)' in GetVATPostingSetup can be simplified to 'exit(X)' for cleaner code. \u2014 See agent comment for details.", "severity": "medium"}, {"file": "src/PayablesAgent.Codeunit.al", "line_start": 77, "line_end": 77, "body": "Malformed begin..end..else structure (AA0005): An unnecessary nested begin..end block inside the 'then begin' creates an invalid else association. \u2014 See agent comment for details.", "severity": "critical"}, {"file": "src/PayablesAgent.Codeunit.al", "line_start": 104, "line_end": 104, "body": "Missing procedure closing end (AA0005): ProcessSingleInvoice procedure is missing its closing 'end;' before a new local procedure begins. \u2014 See agent comment for details.", "severity": "critical"}, {"file": "src/PayablesAgent.Codeunit.al", "line_start": 116, "line_end": 116, "body": "Hardcoded error string with concatenation (AA0217): 'Vendor does not exist: ' is used inline instead of a label variable. \u2014 See agent comment for details.", "severity": "high"}, {"file": "src/PayablesAgent.Codeunit.al", "line_start": 124, "line_end": 124, "body": "Hardcoded error string with concatenation (AA0217): 'No purchase lines found for document: ' is used inline instead of a label variable. \u2014 See agent comment for details.", "severity": "high"}, {"file": "src/PaymentToleranceManagement.Codeunit.al", "line_start": 7, "line_end": 7, "body": "Unused global variables (AA0137): PaymentTerms and CurrencyExchangeRate are declared but never referenced. \u2014 See agent comment for details.", "severity": "medium"}, {"file": "src/PaymentToleranceManagement.Codeunit.al", "line_start": 21, "line_end": 21, "body": "Variable name conflict (AA0204): Local variable 'WorkDate' shadows the built-in WorkDate() function. \u2014 See agent comment for details.", "severity": "high"}, {"file": "src/PaymentToleranceManagement.Codeunit.al", "line_start": 210, "line_end": 210, "body": "Hardcoded string with concatenation (AA0217): 'Payment Tolerance for ' is used inline in Description instead of a label variable. \u2014 See agent comment for details.", "severity": "high"}], "match_line_tolerance": 2, "domain": "style", "category": "code-review", "description": "True positive style findings: code_structure (trimmed to 5 representative findings)", "expect_findings": true, "source": "vsoadmin"}
+{"repo": "microsoft/BCApps", "instance_id": "synthetic__style-012", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "style"}, "patch": "--- src/CustomerNotification.Codeunit.al\n+++ src/CustomerNotification.Codeunit.al\n+codeunit 50301 \"Customer Notification\"\n+{\n+ Access = Public;\n+\n+ procedure SendOverdueNotice(CustomerNo: Code[20])\n+ var\n+ Customer: Record Customer;\n+ UnusedDate: Date;\n+ begin\n+ Customer.Get(CustomerNo);\n+ // SendEmail(Customer);\n+ Message('Overdue notice sent to customer ' + CustomerNo);\n+ end;\n+\n+ procedure GetNotificationCount(CustomerNo: Code[20]): Integer\n+ var\n+ CustLedgerEntry: Record \"Cust. Ledger Entry\";\n+ begin\n+ CustLedgerEntry.SetRange(\"Customer No.\", CustomerNo);\n+ CustLedgerEntry.SetRange(\"Due Date\", 0D, Today() - 30);\n+ exit(CustLedgerEntry.Count());\n+ end;\n+\n+ procedure MarkAsNotified(EntryNo: Integer)\n+ var\n+ CustLedgerEntry: Record \"Cust. Ledger Entry\";\n+ begin\n+ CustLedgerEntry.Get(EntryNo);\n+ if not Confirm('Are you sure you want to mark entry as notified?') then\n+ exit;\n+ Message('Entry has been marked as notified.');\n+ end;\n+}\n+\n--- src/InventoryHelper.Codeunit.al\n+++ src/InventoryHelper.Codeunit.al\n+codeunit 50300 \"Inventory Helper\"\n+{\n+ Access = Public;\n+\n+ var\n+ UnusedCounter: Integer;\n+\n+ procedure AdjustStock(ItemNo: Code[20]; Qty: Decimal)\n+ var\n+ Item: Record Item;\n+ begin\n+ Item.Get(ItemNo);\n+ Item.Validate(\"Reorder Quantity\", Qty);\n+ Item.Modify(true);\n+ end;\n+\n+ procedure GetAvailableQty(ItemNo: Code[20]; LocationCode: Code[10]): Decimal\n+ var\n+ Item: Record Item;\n+ begin\n+ Item.Get(ItemNo);\n+ Item.SetRange(\"Location Filter\", LocationCode);\n+ Item.CalcFields(Inventory);\n+ exit(Item.Inventory);\n+ end;\n+\n+ procedure PostAdjustment(ItemNo: Code[20]; Qty: Decimal)\n+ var\n+ TempValue: Decimal;\n+ begin\n+ if Qty = 0 then\n+ Error('Quantity must not be zero.');\n+\n+ Message('Adjustment posted for item ' + ItemNo);\n+ end;\n+}\n+\n--- src/PurchaseValidator.Codeunit.al\n+++ src/PurchaseValidator.Codeunit.al\n+codeunit 50302 \"Purchase Validator\"\n+{\n+ Access = Public;\n+\n+ var\n+ HelperCodeunit: Codeunit \"Inventory Helper\";\n+\n+ procedure ValidateHeader(PurchaseHeader: Record \"Purchase Header\"): Boolean\n+ begin\n+ if PurchaseHeader.\"Buy-from Vendor No.\" = '' then\n+ Error('Vendor must be specified.');\n+ exit(true);\n+ end;\n+\n+ procedure ValidateLines(DocNo: Code[20])\n+ var\n+ PurchaseLine: Record \"Purchase Line\";\n+ UnusedTotal: Decimal;\n+ begin\n+ PurchaseLine.SetRange(\"Document No.\", DocNo);\n+ if PurchaseLine.IsEmpty() then\n+ Error('Purchase document must have at least one line.');\n+ end;\n+}\n+", "expected_comments": [{"file": "src/InventoryHelper.Codeunit.al", "line_start": 8, "line_end": 8, "body": "Public procedure 'AdjustStock' lacks XML documentation comments. Public procedures should have /// documentation. \u2014 See agent comment for details.", "severity": "medium"}, {"file": "src/InventoryHelper.Codeunit.al", "line_start": 6, "line_end": 6, "body": "Unused global variable 'UnusedCounter' (AA0137). \u2014 See agent comment for details.", "severity": "low"}, {"file": "src/InventoryHelper.Codeunit.al", "line_start": 32, "line_end": 32, "body": "Hardcoded text string in Error() call (CodeCop AA0217): 'Quantity must not be zero.' should use a Label variable. \u2014 See agent comment for details.", "severity": "high"}, {"file": "src/InventoryHelper.Codeunit.al", "line_start": 34, "line_end": 34, "body": "Hardcoded text string in Message() call (CodeCop AA0217): 'Adjustment posted for item ' should use a Label variable. \u2014 See agent comment for details.", "severity": "high"}, {"file": "src/CustomerNotification.Codeunit.al", "line_start": 11, "line_end": 11, "body": "Commented-out code '// SendEmail(Customer);' should be removed (clean code). \u2014 See agent comment for details.", "severity": "medium"}, {"file": "src/CustomerNotification.Codeunit.al", "line_start": 12, "line_end": 12, "body": "Hardcoded text string in Message() call (CodeCop AA0217): 'Overdue notice sent to customer ' should use a Label variable. \u2014 See agent comment for details.", "severity": "high"}, {"file": "src/CustomerNotification.Codeunit.al", "line_start": 29, "line_end": 29, "body": "Hardcoded text string in Confirm() call (CodeCop AA0217): 'Are you sure you want to mark entry as notified?' should use a Label variable with Qst suffix. \u2014 See agent comment for details.", "severity": "high"}, {"file": "src/PurchaseValidator.Codeunit.al", "line_start": 11, "line_end": 11, "body": "Hardcoded text string in Error() call (CodeCop AA0217): 'Vendor must be specified.' should use a Label variable with Err suffix. \u2014 See agent comment for details.", "severity": "high"}], "match_line_tolerance": 2, "domain": "style", "category": "code-review", "description": "True positive style findings: documentation \u2014 missing XML docs, hardcoded strings, unused variables, commented-out code", "expect_findings": true, "source": "vsoadmin"}
+{"repo": "microsoft/BCApps", "instance_id": "synthetic__style-013", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "style"}, "patch": "--- src/CloudMigReplicateDataMgt.Codeunit.al\n+++ src/CloudMigReplicateDataMgt.Codeunit.al\n+codeunit 50117 \"Cloud Mig. Replicate Data Mgt\"\n+{\n+ Access = Public;\n+\n+ var\n+ TablesCannotBeEnabledForReplicationErr: Label 'The following tables cannot be enabled for replication because they contain sensitive data or are system tables:%1\\\\Please review the table selection and remove any tables that should not be replicated to ensure data security and compliance with privacy regulations.', Comment = '%1 = List of table names';\n+\n+ procedure ValidateTablesForReplication(var TableList: Record \"Cloud Migration Table\" temporary): Boolean\n+ var\n+ RestrictedTables: List of [Text];\n+ RestrictedTableNames: Text;\n+ TableName: Text;\n+ ValidationPassed: Boolean;\n+ begin\n+ ValidationPassed := true;\n+\n+ if TableList.FindSet() then\n+ repeat\n+ if IsTableRestrictedForReplication(TableList.\"Table ID\") then begin\n+ TableName := GetTableName(TableList.\"Table ID\");\n+ RestrictedTables.Add(TableName);\n+ ValidationPassed := false;\n+ end;\n+ until TableList.Next() = 0;\n+\n+ if not ValidationPassed then begin\n+ RestrictedTableNames := BuildRestrictedTableList(RestrictedTables);\n+ Error(TablesCannotBeEnabledForReplicationErr, RestrictedTableNames); // Line 603\n+ end;\n+\n+ exit(ValidationPassed);\n+ end;\n+\n+ procedure EnableReplicationForTables(var TableList: Record \"Cloud Migration Table\" temporary)\n+ var\n+ CloudMigrationSetup: Record \"Cloud Migration Setup\";\n+ ReplicationSetup: Record \"Replication Setup\";\n+ TableCount: Integer;\n+ EnabledCount: Integer;\n+ begin\n+ if not ValidateTablesForReplication(TableList) then\n+ exit;\n+\n+ if not CloudMigrationSetup.Get() then begin\n+ CloudMigrationSetup.Init();\n+ CloudMigrationSetup.Insert(true);\n+ end;\n+\n+ TableCount := TableList.Count();\n+\n+ if TableList.FindSet() then\n+ repeat\n+ if EnableTableReplication(TableList.\"Table ID\") then begin\n+ TableList.\"Replication Enabled\" := true;\n+ TableList.Modify();\n+ EnabledCount += 1;\n+ end;\n+ until TableList.Next() = 0;\n+\n+ Message('Replication enabled for %1 of %2 tables', EnabledCount, TableCount);\n+ end;\n+\n+ local procedure IsTableRestrictedForReplication(TableID: Integer): Boolean\n+ var\n+ RestrictedTables: List of [Integer];\n+ IsRestricted: Boolean;\n+ begin\n+ // Define restricted table IDs\n+ RestrictedTables.Add(2000000001); // User\n+ RestrictedTables.Add(2000000002); // User Property\n+ RestrictedTables.Add(2000000120); // User Personalization\n+ RestrictedTables.Add(50001); // User Authentication\n+ RestrictedTables.Add(50002); // Security Token\n+ RestrictedTables.Add(50003); // Encryption Key\n+ RestrictedTables.Add(18); // Customer\n+ RestrictedTables.Add(23); // Vendor\n+\n+ IsRestricted := RestrictedTables.Contains(TableID);\n+\n+ // Additional checks for sensitive data\n+ if not IsRestricted then\n+ IsRestricted := ContainsSensitiveData(TableID);\n+\n+ exit(IsRestricted);\n+ end;\n+\n+ local procedure ContainsSensitiveData(TableID: Integer): Boolean\n+ var\n+ TableMetadata: Record \"Table Metadata\";\n+ FieldMetadata: Record \"Field Metadata\";\n+ HasSensitiveFields: Boolean;\n+ begin\n+ HasSensitiveFields := false;\n+\n+ if TableMetadata.Get(TableID) then begin\n+ FieldMetadata.SetRange(TableNo, TableID);\n+ FieldMetadata.SetFilter(DataClassification, '%1|%2|%3',\n+ 'CustomerContent',\n+ 'EndUserIdentifiableInformation',\n+ 'EndUserPseudonymousIdentifiers');\n+\n+ HasSensitiveFields := not FieldMetadata.IsEmpty();\n+ end;\n+\n+ exit(HasSensitiveFields);\n+ end;\n+\n+ local procedure GetTableName(TableID: Integer): Text\n+ var\n+ TableMetadata: Record \"Table Metadata\";\n+ TableName: Text;\n+ begin\n+ if TableMetadata.Get(TableID) then\n+ TableName := TableMetadata.Name\n+ else\n+ TableName := 'Unknown Table (' + Format(TableID) + ')';\n+\n+ exit(TableName);\n+ end;\n+\n+ local procedure BuildRestrictedTableList(RestrictedTables: List of [Text]): Text\n+ var\n+ TableNames: Text;\n+ TableName: Text;\n+ Separator: Text;\n+ begin\n+ Separator := '';\n+\n+ foreach TableName in RestrictedTables do begin\n+ TableNames := TableNames + Separator + '- ' + TableName;\n+ Separator := '\\';\n+ end;\n+\n+ exit(TableNames);\n+ end;\n+\n+ local procedure EnableTableReplication(TableID: Integer): Boolean\n+ var\n+ ReplicationSetup: Record \"Replication Setup\";\n+ TableMetadata: Record \"Table Metadata\";\n+ EnableSuccess: Boolean;\n+ begin\n+ EnableSuccess := false;\n+\n+ if TableMetadata.Get(TableID) then begin\n+ if not ReplicationSetup.Get(TableID) then begin\n+ ReplicationSetup.Init();\n+ ReplicationSetup.\"Table ID\" := TableID;\n+ ReplicationSetup.\"Table Name\" := TableMetadata.Name;\n+ ReplicationSetup.\"Replication Enabled\" := true;\n+ ReplicationSetup.\"Last Modified\" := CurrentDateTime();\n+ EnableSuccess := ReplicationSetup.Insert(true);\n+ end else begin\n+ ReplicationSetup.\"Replication Enabled\" := true;\n+ ReplicationSetup.\"Last Modified\" := CurrentDateTime();\n+ EnableSuccess := ReplicationSetup.Modify(true);\n+ end;\n+ end;\n+\n+ exit(EnableSuccess);\n+ end;\n+\n+ procedure GetReplicationStatus(var TempReplicationStatus: Record \"Replication Status\" temporary)\n+ var\n+ ReplicationSetup: Record \"Replication Setup\";\n+ ReplicationStatus: Record \"Replication Status\";\n+ begin\n+ TempReplicationStatus.Reset();\n+ TempReplicationStatus.DeleteAll();\n+\n+ ReplicationSetup.SetRange(\"Replication Enabled\", true);\n+\n+ if ReplicationSetup.FindSet() then\n+ repeat\n+ TempReplicationStatus.Init();\n+ TempReplicationStatus.\"Table ID\" := ReplicationSetup.\"Table ID\";\n+ TempReplicationStatus.\"Table Name\" := ReplicationSetup.\"Table Name\";\n+ TempReplicationStatus.\"Replication Enabled\" := ReplicationSetup.\"Replication Enabled\";\n+ TempReplicationStatus.\"Last Sync Date\" := GetLastSyncDate(ReplicationSetup.\"Table ID\");\n+ TempReplicationStatus.\"Record Count\" := GetTableRecordCount(ReplicationSetup.\"Table ID\");\n+ TempReplicationStatus.\"Status\" := GetReplicationStatusText(ReplicationSetup.\"Table ID\");\n+ TempReplicationStatus.Insert();\n+ until ReplicationSetup.Next() = 0;\n+ end;\n+\n+ local procedure GetLastSyncDate(TableID: Integer): DateTime\n+ var\n+ SyncLog: Record \"Cloud Migration Sync Log\";\n+ LastSyncDate: DateTime;\n+ begin\n+ SyncLog.SetRange(\"Table ID\", TableID);\n+ SyncLog.SetCurrentKey(\"Sync Date\");\n+ SyncLog.SetAscending(\"Sync Date\", false);\n+\n+ if SyncLog.FindFirst() then\n+ LastSyncDate := SyncLog.\"Sync Date\"\n+ else\n+ LastSyncDate := 0DT;\n+\n+ exit(LastSyncDate);\n+ end;\n+\n+ local procedure GetTableRecordCount(TableID: Integer): Integer\n+ var\n+ RecRef: RecordRef;\n+ RecordCount: Integer;\n+ begin\n+ RecordCount := 0;\n+\n+ if RecRef.Open(TableID) then begin\n+ RecordCount := RecRef.Count();\n+ RecRef.Close();\n+ end;\n+\n+ exit(RecordCount);\n+ end;\n+\n+ local procedure GetReplicationStatusText(TableID: Integer): Text[50]\n+ var\n+ StatusText: Text[50];\n+ LastSyncDate: DateTime;\n+ begin\n+ LastSyncDate := GetLastSyncDate(TableID);\n+\n+ if LastSyncDate = 0DT then\n+ StatusText := 'Never Synced'\n+ else if LastSyncDate < CreateDateTime(CalcDate('<-1D>', Today), 0T) then\n+ StatusText := 'Sync Overdue'\n+ else\n+ StatusText := 'Up to Date';\n+\n+ exit(StatusText);\n+ end;\n+}\n+\n--- src/CustStPDFDocHandler.Codeunit.al\n+++ src/CustStPDFDocHandler.Codeunit.al\n+codeunit 50114 \"Cust. St. PDF Doc Handler\"\n+{\n+ Access = Public;\n+\n+ var\n+ TempBlob: Codeunit \"Temp Blob\";\n+ FileManagement: Codeunit \"File Management\";\n+\n+ UnableToProcessDocumentErr: Label 'Unable to process document with SystemId %1,', Comment = 'SystemId %1';\n+\n+ procedure ProcessCustomerStatement(CustomerNo: Code[20]; StatementDate: Date): Boolean\n+ var\n+ Customer: Record Customer;\n+ CustomerStatement: Record \"Customer Statement\";\n+ PDFGenerator: Codeunit \"PDF Generator\";\n+ ProcessingResult: Boolean;\n+ DocumentStream: InStream;\n+ PDFStream: OutStream;\n+ StatementGuid: Guid;\n+ begin\n+ ProcessingResult := false;\n+\n+ if not Customer.Get(CustomerNo) then\n+ exit(false);\n+\n+ // Generate statement record\n+ CustomerStatement.Init();\n+ CustomerStatement.\"Customer No.\" := CustomerNo;\n+ CustomerStatement.\"Statement Date\" := StatementDate;\n+ CustomerStatement.\"Created Date\" := Today;\n+ CustomerStatement.\"Created Time\" := Time;\n+ CustomerStatement.\"Created by User\" := CopyStr(UserId(), 1, MaxStrLen(CustomerStatement.\"Created by User\"));\n+\n+ if CustomerStatement.Insert(true) then begin\n+ StatementGuid := CustomerStatement.SystemId;\n+\n+ // Generate PDF content\n+ if GeneratePDFContent(CustomerStatement, DocumentStream) then begin\n+ if ConvertToPDF(DocumentStream, PDFStream) then begin\n+ CustomerStatement.\"PDF Generated\" := true;\n+ CustomerStatement.\"PDF Size\" := GetStreamSize(PDFStream);\n+ CustomerStatement.Modify(true);\n+ ProcessingResult := true;\n+ end else begin\n+ Error(UnableToProcessDocumentErr, StatementGuid); // Error with trailing comma - line 34\n+ end;\n+ end else begin\n+ Error(UnableToProcessDocumentErr, StatementGuid);\n+ end;\n+ end else begin\n+ Error('Unable to create customer statement record for customer %1', CustomerNo);\n+ end;\n+\n+ exit(ProcessingResult);\n+ end;\n+\n+ local procedure GeneratePDFContent(CustomerStatement: Record \"Customer Statement\"; var ContentStream: InStream): Boolean\n+ var\n+ Customer: Record Customer;\n+ CustLedgerEntry: Record \"Cust. Ledger Entry\";\n+ ReportSelections: Record \"Report Selections\";\n+ TempReportSelections: Record \"Report Selections\" temporary;\n+ StatementReportID: Integer;\n+ ContentGenerated: Boolean;\n+ TempFileName: Text;\n+ OutStream: OutStream;\n+ begin\n+ ContentGenerated := false;\n+\n+ if not Customer.Get(CustomerStatement.\"Customer No.\") then\n+ exit(false);\n+\n+ // Find the appropriate report for customer statements\n+ ReportSelections.SetRange(Usage, ReportSelections.Usage::\"C.Statement\");\n+ ReportSelections.SetRange(\"Report ID\", Report::\"Standard Statement\");\n+\n+ if ReportSelections.FindFirst() then begin\n+ StatementReportID := ReportSelections.\"Report ID\";\n+\n+ // Create temporary file for report output\n+ TempFileName := FileManagement.ServerTempFileName('pdf');\n+\n+ // Set filters for the report\n+ Customer.SetRange(\"No.\", CustomerStatement.\"Customer No.\");\n+ CustLedgerEntry.SetRange(\"Customer No.\", CustomerStatement.\"Customer No.\");\n+ CustLedgerEntry.SetFilter(\"Posting Date\", '..%1', CustomerStatement.\"Statement Date\");\n+\n+ // Generate the report\n+ if GenerateStatementReport(StatementReportID, Customer, CustLedgerEntry, TempFileName) then begin\n+ // Read the generated file into stream\n+ if ReadFileToStream(TempFileName, ContentStream) then\n+ ContentGenerated := true;\n+\n+ // Clean up temporary file\n+ if FileManagement.ServerFileExists(TempFileName) then\n+ FileManagement.DeleteServerFile(TempFileName);\n+ end;\n+ end;\n+\n+ exit(ContentGenerated);\n+ end;\n+\n+ local procedure GenerateStatementReport(ReportID: Integer; var Customer: Record Customer; var CustLedgerEntry: Record \"Cust. Ledger Entry\"; OutputFileName: Text): Boolean\n+ var\n+ GenerationSuccess: Boolean;\n+ begin\n+ GenerationSuccess := false;\n+\n+ try\n+ Report.SaveAsPdf(ReportID, OutputFileName, Customer, CustLedgerEntry);\n+ GenerationSuccess := FileManagement.ServerFileExists(OutputFileName);\n+ except\n+ GenerationSuccess := false;\n+ end;\n+ \n+ exit(GenerationSuccess);\n+ end;\n+\n+ local procedure ReadFileToStream(FileName: Text; var ContentStream: InStream): Boolean\n+ var\n+ TempFile: File;\n+ ReadSuccess: Boolean;\n+ begin\n+ ReadSuccess := false;\n+ \n+ if FileManagement.ServerFileExists(FileName) then begin\n+ TempFile.Open(FileName);\n+ TempFile.CreateInStream(ContentStream);\n+ ReadSuccess := not ContentStream.EOS;\n+ TempFile.Close();\n+ end;\n+ \n+ exit(ReadSuccess);\n+ end;\n+\n+ local procedure ConvertToPDF(var InputStream: InStream; var OutputStream: OutStream): Boolean\n+ var\n+ PDFConverter: Codeunit \"PDF Converter\";\n+ ConversionSuccess: Boolean;\n+ begin\n+ ConversionSuccess := false;\n+ \n+ if not InputStream.EOS then begin\n+ try\n+ TempBlob.CreateOutStream(OutputStream);\n+ CopyStream(OutputStream, InputStream);\n+ ConversionSuccess := true;\n+ except\n+ ConversionSuccess := false;\n+ end;\n+ end;\n+ \n+ exit(ConversionSuccess);\n+ end;\n+\n+ local procedure GetStreamSize(var Stream: OutStream): Integer\n+ var\n+ StreamSize: Integer;\n+ begin\n+ // This is a simplified implementation\n+ // In reality, you would measure the actual stream size\n+ StreamSize := 0;\n+ \n+ exit(StreamSize);\n+ end;\n+\n+ procedure ValidateStatementParameters(CustomerNo: Code[20]; StatementDate: Date): Boolean\n+ var\n+ Customer: Record Customer;\n+ ValidationResult: Boolean;\n+ begin\n+ ValidationResult := true;\n+ \n+ // Validate customer exists\n+ if not Customer.Get(CustomerNo) then begin\n+ Error('Customer %1 does not exist', CustomerNo);\n+ ValidationResult := false;\n+ end;\n+ \n+ // Validate customer is not blocked\n+ if Customer.Blocked <> Customer.Blocked::\" \" then begin\n+ Error('Customer %1 is blocked and cannot have statements generated', CustomerNo);\n+ ValidationResult := false;\n+ end;\n+ \n+ // Validate statement date\n+ if StatementDate = 0D then begin\n+ Error('Statement date must be specified');\n+ ValidationResult := false;\n+ end;\n+ \n+ if StatementDate > Today then begin\n+ Error('Statement date cannot be in the future');\n+ ValidationResult := false;\n+ end;\n+ \n+ exit(ValidationResult);\n+ end;\n+\n+ procedure GetStatementHistory(CustomerNo: Code[20]; var TempStatementHistory: Record \"Customer Statement\" temporary)\n+ var\n+ CustomerStatement: Record \"Customer Statement\";\n+ begin\n+ TempStatementHistory.Reset();\n+ TempStatementHistory.DeleteAll();\n+ \n+ CustomerStatement.SetRange(\"Customer No.\", CustomerNo);\n+ CustomerStatement.SetCurrentKey(\"Statement Date\");\n+ CustomerStatement.Ascending(false);\n+ \n+ if CustomerStatement.FindSet() then\n+ repeat\n+ TempStatementHistory.Copy(CustomerStatement);\n+ TempStatementHistory.Insert();\n+ until CustomerStatement.Next() = 0;\n+ end;\n+}\n+\n--- src/ExpenseCategory.Table.al\n+++ src/ExpenseCategory.Table.al\n+table 50115 \"Expense Category\"\n+{\n+ Caption = 'Expense Category';\n+ DataCaptionFields = Code, Description;\n+ DrillDownPageId = \"Expense Categories\";\n+ LookupPageId = \"Expense Categories\";\n+\n+ fields\n+ {\n+ field(1; Code; Code[20])\n+ {\n+ Caption = 'Code';\n+ NotBlank = true;\n+ DataClassification = CustomerContent;\n+ }\n+ field(2; Description; Text[100])\n+ {\n+ Caption = 'Description';\n+ DataClassification = CustomerContent;\n+ }\n+ field(3; \"G/L Account No.\"; Code[20])\n+ {\n+ Caption = 'G/L Account No.';\n+ TableRelation = \"G/L Account\" where(\"Account Type\" = const(Posting),\n+ Blocked = const(false));\n+ DataClassification = CustomerContent;\n+\n+ trigger OnValidate()\n+ begin\n+ if \"G/L Account No.\" <> xRec.\"G/L Account No.\" then\n+ ValidateGLAccount();\n+ end;\n+ }\n+ field(4; \"Expense Type\"; Enum \"Expense Type\")\n+ {\n+ Caption = 'Expense Type';\n+ DataClassification = CustomerContent;\n+ }\n+ field(5; \"Requires Receipt\"; Boolean)\n+ {\n+ Caption = 'Requires Receipt';\n+ DataClassification = CustomerContent;\n+ InitValue = true;\n+ }\n+ field(6; \"Max Amount\"; Decimal)\n+ {\n+ Caption = 'Max Amount';\n+ DataClassification = CustomerContent;\n+ MinValue = 0;\n+\n+ trigger OnValidate()\n+ begin\n+ if \"Max Amount\" < 0 then\n+ Error('Maximum amount cannot be negative');\n+ end;\n+ }\n+ field(7; \"Approval Required\"; Boolean)\n+ {\n+ Caption = 'Approval Required';\n+ DataClassification = CustomerContent;\n+ }\n+ field(8; \"Approval Amount Threshold\"; Decimal)\n+ {\n+ Caption = 'Approval Amount Threshold';\n+ DataClassification = CustomerContent;\n+ MinValue = 0;\n+ }\n+ field(10; \"VAT Bus. Posting Group\"; Code[20])\n+ {\n+ Caption = 'VAT Bus. Posting Group';\n+ TableRelation = \"VAT Business Posting Group\";\n+ DataClassification = CustomerContent;\n+ }\n+ field(11; \"VAT Prod. Posting Group\"; Code[20])\n+ {\n+ Caption = 'VAT Prod. Posting Group';\n+ TableRelation = \"VAT Product Posting Group\";\n+ DataClassification = CustomerContent;\n+ }\n+ field(12; \"Gen. Bus. Posting Group\"; Code[20])\n+ {\n+ Caption = 'Gen. Bus. Posting Group';\n+ TableRelation = \"Gen. Business Posting Group\";\n+ DataClassification = CustomerContent;\n+ }\n+ field(13; \"Gen. Prod. Posting Group\"; Code[20])\n+ {\n+ Caption = 'Gen. Prod. Posting Group';\n+ TableRelation = \"Gen. Product Posting Group\";\n+ DataClassification = CustomerContent;\n+ }\n+ field(20; \"Allow Personal Use\"; Boolean)\n+ {\n+ Caption = 'Allow Personal Use';\n+ DataClassification = CustomerContent;\n+ }\n+ field(21; \"Personal Use %\"; Decimal)\n+ {\n+ Caption = 'Personal Use %';\n+ DataClassification = CustomerContent;\n+ DecimalPlaces = 0 : 5;\n+ MaxValue = 100;\n+ MinValue = 0;\n+ }\n+ field(30; \"Mileage Rate\"; Decimal)\n+ {\n+ Caption = 'Mileage Rate';\n+ DataClassification = CustomerContent;\n+ AutoFormatType = 2;\n+ BlankZero = true;\n+ }\n+ field(31; \"Mileage Unit\"; Code[10])\n+ {\n+ Caption = 'Mileage Unit';\n+ TableRelation = \"Unit of Measure\";\n+ DataClassification = CustomerContent;\n+ }\n+ field(40; \"Active\"; Boolean)\n+ {\n+ Caption = 'Active';\n+ DataClassification = CustomerContent;\n+ InitValue = true;\n+ }\n+ field(41; \"Effective Date\"; Date)\n+ {\n+ Caption = 'Effective Date';\n+ DataClassification = CustomerContent;\n+ }\n+ field(42; \"Expiration Date\"; Date)\n+ {\n+ Caption = 'Expiration Date';\n+ DataClassification = CustomerContent;\n+ }\n+ }\n+\n+ keys\n+ {\n+ key(PK; Code)\n+ {\n+ Clustered = true;\n+ }\n+ key(DescriptionKey; Description)\n+ {\n+ }\n+ key(ExpenseTypeKey; \"Expense Type\", Code)\n+ {\n+ }\n+ key(ActiveKey; Active, \"Effective Date\")\n+ {\n+ }\n+ }\n+\n+ fieldgroups\n+ {\n+ fieldgroup(DropDown; Code, Description, \"Expense Type\")\n+ {\n+ }\n+ fieldgroup(Brick; Code, Description, \"G/L Account No.\", Active)\n+ {\n+ }\n+ }\n+\n+ trigger OnInsert()\n+ begin\n+ if \"Effective Date\" = 0D then\n+ \"Effective Date\" := Today;\n+\n+ ValidateExpenseCategory();\n+ end;\n+\n+ trigger OnModify()\n+ begin\n+ ValidateExpenseCategory();\n+ end;\n+\n+ trigger OnDelete()\n+ var\n+ ExpenseLine: Record \"Expense Line\";\n+ begin\n+ // Check if category is used in any expense lines\n+ ExpenseLine.SetRange(\"Expense Category Code\", Code);\n+ if not ExpenseLine.IsEmpty() then\n+ Error('Cannot delete expense category %1 because it is used in expense lines', Code);\n+ end;\n+\n+ local procedure ValidateGLAccount()\n+ var\n+ GLAccount: Record \"G/L Account\";\n+ begin\n+ if \"G/L Account No.\" = '' then\n+ exit;\n+\n+ if not GLAccount.Get(\"G/L Account No.\") then\n+ Error('G/L Account %1 does not exist', \"G/L Account No.\");\n+\n+ if GLAccount.\"Account Type\" <> GLAccount.\"Account Type\"::Posting then\n+ Error('G/L Account %1 must be a posting account', \"G/L Account No.\");\n+\n+ if GLAccount.Blocked then\n+ Error('G/L Account %1 is blocked', \"G/L Account No.\");\n+ end;\n+\n+ local procedure ValidateExpenseCategory()\n+ begin\n+ // Validate required fields\n+ if Code = '' then\n+ Error(''); // Error with empty string - line 129\n+\n+ if Description = '' then\n+ Error('Description must be specified');\n+\n+ // Validate date range\n+ if (\"Effective Date\" <> 0D) and (\"Expiration Date\" <> 0D) then\n+ if \"Expiration Date\" < \"Effective Date\" then\n+ Error('Expiration date cannot be before effective date');\n+\n+ // Validate personal use percentage\n+ if \"Allow Personal Use\" and (\"Personal Use %\" = 0) then\n+ Error('Personal use percentage must be specified when personal use is allowed');\n+\n+ // Validate approval settings\n+ if \"Approval Required\" and (\"Approval Amount Threshold\" = 0) then\n+ \"Approval Amount Threshold\" := 1;\n+\n+ // Validate mileage settings\n+ if (\"Mileage Rate\" > 0) and (\"Mileage Unit\" = '') then\n+ Error('Mileage unit must be specified when mileage rate is defined');\n+ end;\n+\n+ procedure IsActiveOnDate(CheckDate: Date): Boolean\n+ var\n+ IsActive: Boolean;\n+ begin\n+ IsActive := Active;\n+\n+ if IsActive and (\"Effective Date\" <> 0D) then\n+ IsActive := CheckDate >= \"Effective Date\";\n+\n+ if IsActive and (\"Expiration Date\" <> 0D) then\n+ IsActive := CheckDate <= \"Expiration Date\";\n+\n+ exit(IsActive);\n+ end;\n+\n+ procedure CalculateDeductibleAmount(TotalAmount: Decimal): Decimal\n+ var\n+ DeductibleAmount: Decimal;\n+ PersonalAmount: Decimal;\n+ begin\n+ DeductibleAmount := TotalAmount;\n+\n+ // Subtract personal use portion if applicable\n+ if \"Allow Personal Use\" and (\"Personal Use %\" > 0) then begin\n+ PersonalAmount := Round(TotalAmount * \"Personal Use %\" / 100, 0.01);\n+ DeductibleAmount := TotalAmount - PersonalAmount;\n+ end;\n+\n+ // Apply maximum amount limit if specified\n+ if (\"Max Amount\" > 0) and (DeductibleAmount > \"Max Amount\") then\n+ DeductibleAmount := \"Max Amount\";\n+\n+ exit(DeductibleAmount);\n+ end;\n+\n+ procedure IsApprovalRequired(ExpenseAmount: Decimal): Boolean\n+ begin\n+ if not \"Approval Required\" then\n+ exit(false);\n+\n+ if \"Approval Amount Threshold\" = 0 then\n+ exit(true);\n+\n+ exit(ExpenseAmount >= \"Approval Amount Threshold\");\n+ end;\n+\n+ procedure GetActiveCategories(var TempExpenseCategory: Record \"Expense Category\" temporary)\n+ var\n+ ExpenseCategory: Record \"Expense Category\";\n+ begin\n+ TempExpenseCategory.Reset();\n+ TempExpenseCategory.DeleteAll();\n+\n+ ExpenseCategory.SetRange(Active, true);\n+ ExpenseCategory.SetFilter(\"Effective Date\", '<=%1|%2', Today, 0D);\n+ ExpenseCategory.SetFilter(\"Expiration Date\", '>=%1|%2', Today, 0D);\n+\n+ if ExpenseCategory.FindSet() then\n+ repeat\n+ TempExpenseCategory.Copy(ExpenseCategory);\n+ TempExpenseCategory.Insert();\n+ until ExpenseCategory.Next() = 0;\n+ end;\n+\n+ procedure CopyFromCategory(SourceCategory: Record \"Expense Category\")\n+ begin\n+ \"G/L Account No.\" := SourceCategory.\"G/L Account No.\";\n+ \"Expense Type\" := SourceCategory.\"Expense Type\";\n+ \"Requires Receipt\" := SourceCategory.\"Requires Receipt\";\n+ \"Max Amount\" := SourceCategory.\"Max Amount\";\n+ \"Approval Required\" := SourceCategory.\"Approval Required\";\n+ \"Approval Amount Threshold\" := SourceCategory.\"Approval Amount Threshold\";\n+ \"VAT Bus. Posting Group\" := SourceCategory.\"VAT Bus. Posting Group\";\n+ \"VAT Prod. Posting Group\" := SourceCategory.\"VAT Prod. Posting Group\";\n+ \"Gen. Bus. Posting Group\" := SourceCategory.\"Gen. Bus. Posting Group\";\n+ \"Gen. Prod. Posting Group\" := SourceCategory.\"Gen. Prod. Posting Group\";\n+ \"Allow Personal Use\" := SourceCategory.\"Allow Personal Use\";\n+ \"Personal Use %\" := SourceCategory.\"Personal Use %\";\n+ \"Mileage Rate\" := SourceCategory.\"Mileage Rate\";\n+ \"Mileage Unit\" := SourceCategory.\"Mileage Unit\";\n+\n+ Modify(true);\n+ end;\n+}\n+\n--- src/ItemCategoryAttributes.Page.al\n+++ src/ItemCategoryAttributes.Page.al\n+page 50118 \"Item Category Attributes\"\n+{\n+ ApplicationArea = Basic, Suite;\n+ Caption = 'Item Category Attributes';\n+ PageType = List;\n+ SourceTable = \"Item Attribute\";\n+ UsageCategory = Lists;\n+\n+ layout\n+ {\n+ area(content)\n+ {\n+ repeater(Control1)\n+ {\n+ ShowCaption = false;\n+ field(Name; Rec.Name)\n+ {\n+ ApplicationArea = Basic, Suite;\n+ ToolTip = 'Specifies the name of the item attribute.';\n+ }\n+ field(Type; Rec.Type)\n+ {\n+ ApplicationArea = Basic, Suite;\n+ ToolTip = 'Specifies the type of the item attribute.';\n+ }\n+ field(Values; Rec.Values)\n+ {\n+ ApplicationArea = Basic, Suite;\n+ ToolTip = 'Specifies the values available for this attribute.';\n+ Visible = Rec.Type = Rec.Type::Option;\n+ }\n+ field(\"Unit of Measure\"; Rec.\"Unit of Measure\")\n+ {\n+ ApplicationArea = Basic, Suite;\n+ ToolTip = 'Specifies the unit of measure for numeric attributes.';\n+ Visible = Rec.Type = Rec.Type::Decimal;\n+ }\n+ field(Blocked; Rec.Blocked)\n+ {\n+ ApplicationArea = Basic, Suite;\n+ ToolTip = 'Specifies whether the attribute is blocked from use.';\n+ }\n+ }\n+ }\n+ area(factboxes)\n+ {\n+ part(ItemAttributeValues; \"Item Attribute Values\")\n+ {\n+ ApplicationArea = Basic, Suite;\n+ SubPageLink = \"Attribute ID\" = field(ID);\n+ }\n+ systempart(Control1900383207; Links)\n+ {\n+ ApplicationArea = RecordLinks;\n+ Visible = false;\n+ }\n+ systempart(Control1905767507; Notes)\n+ {\n+ ApplicationArea = Notes;\n+ Visible = false;\n+ }\n+ }\n+ }\n+\n+ actions\n+ {\n+ area(processing)\n+ {\n+ action(\"Import Attributes\")\n+ {\n+ ApplicationArea = Basic, Suite;\n+ Caption = 'Import Attributes';\n+ Image = Import;\n+ ToolTip = 'Import item attributes from an external file.';\n+\n+ trigger OnAction()\n+ var\n+ ImportManager: Codeunit \"Attribute Import Manager\";\n+ FilePath: Text;\n+ ImportResult: Boolean;\n+ begin\n+ if not UploadIntoStream('Select attribute file', '', 'CSV Files|*.csv', FilePath, ImportStream) then\n+ exit;\n+\n+ ImportResult := ImportManager.ImportFromCSV(ImportStream);\n+ if ImportResult then\n+ Message('Attributes imported successfully.')\n+ else\n+ Message('Import completed with errors. Please review the results.');\n+ end;\n+ }\n+ action(\"Validate Attributes\")\n+ {\n+ ApplicationArea = Basic, Suite;\n+ Caption = 'Validate Attributes';\n+ Image = CheckRulesSyntax;\n+ ToolTip = 'Validate all attribute definitions for consistency.';\n+\n+ trigger OnAction()\n+ var\n+ AttributeValidator: Codeunit \"Item Attribute Validator\";\n+ ValidationResults: Record \"Validation Result\" temporary;\n+ ValidationPassed: Boolean;\n+ begin\n+ ValidationPassed := AttributeValidator.ValidateAllAttributes(ValidationResults);\n+\n+ if ValidationPassed then\n+ Message('All attributes are valid.')\n+ else begin\n+ Message('Validation found issues. Please review the results.');\n+ Page.RunModal(Page::\"Validation Results\", ValidationResults);\n+ end;\n+ end;\n+ }\n+ action(\"Assign to Items\")\n+ {\n+ ApplicationArea = Basic, Suite;\n+ Caption = 'Assign to Items';\n+ Image = ItemSubstitution;\n+ ToolTip = 'Assign selected attributes to items.';\n+\n+ trigger OnAction()\n+ var\n+ ItemAttributeAssignment: Page \"Item Attribute Assignment\";\n+ SelectedAttributes: Record \"Item Attribute\";\n+ begin\n+ CurrPage.SetSelectionFilter(SelectedAttributes);\n+ if SelectedAttributes.IsEmpty() then begin\n+ Message('Please select one or more attributes to assign.');\n+ exit;\n+ end;\n+\n+ ItemAttributeAssignment.SetSelectedAttributes(SelectedAttributes);\n+ ItemAttributeAssignment.RunModal();\n+ end;\n+ }\n+ }\n+ area(reporting)\n+ {\n+ action(\"Attribute Usage Report\")\n+ {\n+ ApplicationArea = Basic, Suite;\n+ Caption = 'Attribute Usage Report';\n+ Image = \"Report\";\n+ RunObject = Report \"Item Attribute Usage\";\n+ ToolTip = 'View a report showing how attributes are used across items.';\n+ }\n+ }\n+ }\n+\n+ trigger OnOpenPage()\n+ var\n+ FeatureManagement: Codeunit \"Feature Management\";\n+ NotificationManager: Codeunit \"Notification Manager\";\n+ BlankOptionAttributeNotification: Notification;\n+ begin\n+ if not FeatureManagement.IsEnabled('ItemAttributes') then begin\n+ Message('Item attributes feature is not enabled. Please contact your system administrator.');\n+ CurrPage.Close();\n+ end;\n+\n+ // Check for blank option attributes and show notification\n+ if HasBlankOptionAttributes() then begin\n+ BlankOptionAttributeNotification.Id := CreateGuid();\n+ BlankOptionAttributeNotification.Message := 'Some option attributes have blank values that may cause issues.';\n+ BlankOptionAttributeNotification.Scope := NotificationScope::LocalScope;\n+ BlankOptionAttributeNotification.AddAction('Review Attributes', Codeunit::\"Item Attribute Management\", 'ReviewBlankOptions');\n+ BlankOptionAttributeNotification.Send(); // Using Msg suffix for notification - line 165\n+ end;\n+ end;\n+\n+ trigger OnAfterGetRecord()\n+ begin\n+ // Update attribute statistics\n+ UpdateAttributeStatistics();\n+ end;\n+\n+ trigger OnNewRecord(BelowxRec: Boolean)\n+ begin\n+ Rec.Type := Rec.Type::Text;\n+ Rec.Blocked := false;\n+ end;\n+\n+ local procedure HasBlankOptionAttributes(): Boolean\n+ var\n+ ItemAttribute: Record \"Item Attribute\";\n+ ItemAttributeValue: Record \"Item Attribute Value\";\n+ HasBlankValues: Boolean;\n+ begin\n+ HasBlankValues := false;\n+\n+ ItemAttribute.SetRange(Type, ItemAttribute.Type::Option);\n+ if ItemAttribute.FindSet() then\n+ repeat\n+ ItemAttributeValue.SetRange(\"Attribute ID\", ItemAttribute.ID);\n+ ItemAttributeValue.SetRange(Value, '');\n+ if not ItemAttributeValue.IsEmpty() then begin\n+ HasBlankValues := true;\n+ break;\n+ end;\n+ until ItemAttribute.Next() = 0;\n+\n+ exit(HasBlankValues);\n+ end;\n+\n+ local procedure UpdateAttributeStatistics()\n+ var\n+ ItemAttributeValueMapping: Record \"Item Attribute Value Mapping\";\n+ UsageCount: Integer;\n+ begin\n+ // Count how many items use this attribute\n+ ItemAttributeValueMapping.SetRange(\"Item Attribute ID\", Rec.ID);\n+ UsageCount := ItemAttributeValueMapping.Count();\n+\n+ // Update the usage count field (would need to add this field to the table)\n+ // Rec.\"Usage Count\" := UsageCount;\n+ // Rec.Modify();\n+ end;\n+\n+ procedure SetItemCategoryFilter(ItemCategoryCode: Code[20])\n+ var\n+ ItemCategoryItemAttribute: Record \"Item Category - Item Attribute\";\n+ AttributeFilter: Text;\n+ AttributeId: Integer;\n+ begin\n+ ItemCategoryItemAttribute.SetRange(\"Item Category Code\", ItemCategoryCode);\n+\n+ if ItemCategoryItemAttribute.FindSet() then begin\n+ repeat\n+ AttributeId := ItemCategoryItemAttribute.\"Item Attribute ID\";\n+ if AttributeFilter <> '' then\n+ AttributeFilter := AttributeFilter + '|';\n+ AttributeFilter := AttributeFilter + Format(AttributeId);\n+ until ItemCategoryItemAttribute.Next() = 0;\n+\n+ Rec.SetFilter(ID, AttributeFilter);\n+ end else\n+ Rec.SetRange(ID, -1); // Show no records if category has no attributes\n+ end;\n+\n+ procedure CreateDefaultAttributes()\n+ var\n+ ItemAttribute: Record \"Item Attribute\";\n+ DefaultAttributes: List of [Text];\n+ DefaultTypes: List of [Option];\n+ AttributeName: Text;\n+ i: Integer;\n+ begin\n+ // Define default attributes\n+ DefaultAttributes.Add('Color');\n+ DefaultAttributes.Add('Size');\n+ DefaultAttributes.Add('Material');\n+ DefaultAttributes.Add('Weight');\n+ DefaultAttributes.Add('Dimensions');\n+\n+ for i := 1 to DefaultAttributes.Count() do begin\n+ AttributeName := DefaultAttributes.Get(i);\n+ if not AttributeExists(AttributeName) then begin\n+ ItemAttribute.Init();\n+ ItemAttribute.Name := CopyStr(AttributeName, 1, MaxStrLen(ItemAttribute.Name));\n+\n+ case i of\n+ 1, 2, 3:\n+ ItemAttribute.Type := ItemAttribute.Type::Option;\n+ 4:\n+ begin\n+ ItemAttribute.Type := ItemAttribute.Type::Decimal;\n+ ItemAttribute.\"Unit of Measure\" := 'KG';\n+ end;\n+ 5:\n+ ItemAttribute.Type := ItemAttribute.Type::Text;\n+ end;\n+\n+ ItemAttribute.Insert(true);\n+ end;\n+ end;\n+\n+ Message('%1 default attributes created.', DefaultAttributes.Count());\n+ end;\n+\n+ local procedure AttributeExists(AttributeName: Text): Boolean\n+ var\n+ ItemAttribute: Record \"Item Attribute\";\n+ begin\n+ ItemAttribute.SetRange(Name, AttributeName);\n+ exit(not ItemAttribute.IsEmpty());\n+ end;\n+\n+ var\n+ ImportStream: InStream;\n+ BlankOptionAttributeNotificationMsg: Label 'Some option attributes have blank values that may cause issues.';\n+}\n+\n--- src/SCMSupplyPlanningIV.Codeunit.al\n+++ src/SCMSupplyPlanningIV.Codeunit.al\n+codeunit 50116 \"SCM Supply Planning IV\"\n+{\n+ Subtype = Test;\n+\n+ var\n+ Assert: Codeunit Assert;\n+ LibraryInventory: Codeunit \"Library - Inventory\";\n+ LibraryManufacturing: Codeunit \"Library - Manufacturing\";\n+\n+ AssemblyOrderCreatedMsg: Label 'Assembly order %1 has been created successfully.', Comment = '%1 = Order No.';\n+\n+ [Test]\n+ procedure TestCreateAssemblyOrder()\n+ var\n+ Item: Record Item;\n+ AssemblyHeader: Record \"Assembly Header\";\n+ OrderNo: Code[20];\n+ begin\n+ // Setup\n+ LibraryInventory.CreateItem(Item);\n+ Item.\"Assembly Policy\" := Item.\"Assembly Policy\"::\"Assemble-to-Order\";\n+ Item.Modify(true);\n+\n+ // Exercise\n+ OrderNo := CreateAssemblyOrderForItem(Item.\"No.\", 10);\n+\n+ // Verify\n+ AssemblyHeader.Get(AssemblyHeader.\"Document Type\"::Order, OrderNo);\n+ Assert.AreEqual(Item.\"No.\", AssemblyHeader.\"Item No.\", 'Item number should match');\n+ Assert.AreEqual(10, AssemblyHeader.Quantity, AssemblyOrderCreatedMsg); // Wrong suffix usage - line 58\n+ end;\n+\n+ [Test]\n+ procedure TestAssemblyOrderQuantityValidation()\n+ var\n+ Item: Record Item;\n+ AssemblyHeader: Record \"Assembly Header\";\n+ OrderNo: Code[20];\n+ ExpectedQuantity: Decimal;\n+ begin\n+ // Setup\n+ LibraryInventory.CreateItem(Item);\n+ ExpectedQuantity := 25;\n+\n+ // Exercise\n+ OrderNo := CreateAssemblyOrderForItem(Item.\"No.\", ExpectedQuantity);\n+\n+ // Verify\n+ AssemblyHeader.Get(AssemblyHeader.\"Document Type\"::Order, OrderNo);\n+ Assert.AreEqual(ExpectedQuantity, AssemblyHeader.Quantity,\n+ 'Assembly order quantity should match expected value');\n+ end;\n+\n+ [Test]\n+ procedure TestMultipleAssemblyOrders()\n+ var\n+ Item: Record Item;\n+ AssemblyHeader: Record \"Assembly Header\";\n+ OrderCount: Integer;\n+ i: Integer;\n+ OrderNo: Code[20];\n+ begin\n+ // Setup\n+ LibraryInventory.CreateItem(Item);\n+ OrderCount := 5;\n+\n+ // Exercise\n+ for i := 1 to OrderCount do begin\n+ OrderNo := CreateAssemblyOrderForItem(Item.\"No.\", 10 * i);\n+\n+ // Verify each order\n+ AssemblyHeader.Get(AssemblyHeader.\"Document Type\"::Order, OrderNo);\n+ Assert.AreEqual(Item.\"No.\", AssemblyHeader.\"Item No.\", 'Item should match for order ' + Format(i));\n+ end;\n+\n+ // Verify total count\n+ AssemblyHeader.SetRange(\"Item No.\", Item.\"No.\");\n+ Assert.AreEqual(OrderCount, AssemblyHeader.Count(), 'Should have created expected number of orders');\n+ end;\n+\n+ local procedure CreateAssemblyOrderForItem(ItemNo: Code[20]; Quantity: Decimal): Code[20]\n+ var\n+ AssemblyHeader: Record \"Assembly Header\";\n+ NoSeriesManagement: Codeunit NoSeriesManagement;\n+ begin\n+ AssemblyHeader.Init();\n+ AssemblyHeader.\"Document Type\" := AssemblyHeader.\"Document Type\"::Order;\n+ AssemblyHeader.\"No.\" := NoSeriesManagement.GetNextNo(GetAssemblyOrderNos(), Today, true);\n+ AssemblyHeader.\"Item No.\" := ItemNo;\n+ AssemblyHeader.Quantity := Quantity;\n+ AssemblyHeader.\"Due Date\" := CalcDate('<+7D>', Today);\n+ AssemblyHeader.Insert(true);\n+\n+ exit(AssemblyHeader.\"No.\");\n+ end;\n+\n+ local procedure GetAssemblyOrderNos(): Code[20]\n+ var\n+ AssemblySetup: Record \"Assembly Setup\";\n+ begin\n+ AssemblySetup.Get();\n+ exit(AssemblySetup.\"Assembly Order Nos.\");\n+ end;\n+}\n+", "expected_comments": [{"file": "src/CloudMigReplicateDataMgt.Codeunit.al", "line_start": 28, "line_end": 28, "body": "Label suffix inconsistency: 'TablesCannotBeEnabledForReplicationErr' uses 'Err' suffix but the message text is more informational/instructional than a pure error condition. However, since it's used with Error(), the suffix is technically correct. \u2014 See agent comment for details.", "severity": "medium"}, {"file": "src/CustStPDFDocHandler.Codeunit.al", "line_start": 9, "line_end": 9, "body": "Label 'UnableToProcessDocumentErr' has a trailing comma in the message text: 'Unable to process document with SystemId %1,' - the comma appears to be a typo in the label value. \u2014 See agent comment for details.", "severity": "medium"}, {"file": "src/CustStPDFDocHandler.Codeunit.al", "line_start": 45, "line_end": 45, "body": "Error() is being called with a label that has a trailing comma in the message: 'SystemId %1,' - the comma appears to be a typo. \u2014 See agent comment for details.", "severity": "medium"}, {"file": "src/ExpenseCategory.Table.al", "line_start": 207, "line_end": 207, "body": "Using Error('') with empty string is not recommended (CodeCop AA0216). Error messages should use label variables with proper suffix. \u2014 See agent comment for details.", "severity": "medium"}, {"file": "src/ItemCategoryAttributes.Page.al", "line_start": 155, "line_end": 155, "body": "Label variable 'BlankOptionAttributeNotificationMsg' uses 'Msg' suffix but it's used for a notification, not a Message() dialog. Per AA0074, 'Msg' suffix is for Message() calls. \u2014 See agent comment for details.", "severity": "medium"}, {"file": "src/SCMSupplyPlanningIV.Codeunit.al", "line_start": 30, "line_end": 30, "body": "Label variable 'AssemblyOrderCreatedMsg' uses 'Msg' suffix but is used as an assertion failure message, not a user-facing Message() call. The suffix should indicate the actual usage context. \u2014 See agent comment for details.", "severity": "medium"}, {"file": "src/CloudMigReplicateDataMgt.Codeunit.al", "line_start": 60, "line_end": 60, "body": "Hardcoded text string in Message() call (CodeCop AA0217): 'Replication enabled for %1 of %2 tables' should use a Label with Msg suffix. \u2014 See agent comment for details.", "severity": "medium"}, {"file": "src/CustStPDFDocHandler.Codeunit.al", "line_start": 51, "line_end": 51, "body": "Hardcoded error message string in Error() call (CodeCop AA0217): 'Unable to create customer statement record for customer %1'. \u2014 See agent comment for details.", "severity": "medium"}, {"file": "src/CustStPDFDocHandler.Codeunit.al", "line_start": 111, "line_end": 111, "body": "Inconsistent indentation in try..except block: 'GenerationSuccess := ...' is not indented to match the try block level. \u2014 See agent comment for details.", "severity": "medium"}, {"file": "src/CustStPDFDocHandler.Codeunit.al", "line_start": 177, "line_end": 177, "body": "Unreachable code (AA0136): 'ValidationResult := false' after Error() on the previous line will never execute. \u2014 See agent comment for details.", "severity": "low"}, {"file": "src/CustStPDFDocHandler.Codeunit.al", "line_start": 176, "line_end": 176, "body": "Hardcoded error message strings in Error() calls in ValidateStatementParameters (CodeCop AA0217). Multiple Error() calls use inline strings. \u2014 See agent comment for details.", "severity": "medium"}, {"file": "src/ExpenseCategory.Table.al", "line_start": 54, "line_end": 54, "body": "Hardcoded error message string in Error() call: 'Maximum amount cannot be negative' (CodeCop AA0217). \u2014 See agent comment for details.", "severity": "medium"}, {"file": "src/ExpenseCategory.Table.al", "line_start": 183, "line_end": 183, "body": "Hardcoded error message string in Error() call: 'Cannot delete expense category %1 because it is used in expense lines' (CodeCop AA0217). \u2014 See agent comment for details.", "severity": "medium"}, {"file": "src/ExpenseCategory.Table.al", "line_start": 194, "line_end": 194, "body": "Hardcoded error message strings in Error() calls in ValidateGLAccount (CodeCop AA0217). \u2014 See agent comment for details.", "severity": "medium"}, {"file": "src/ExpenseCategory.Table.al", "line_start": 210, "line_end": 210, "body": "Hardcoded error message strings in Error() calls in ValidateExpenseCategory (CodeCop AA0217). \u2014 See agent comment for details.", "severity": "medium"}, {"file": "src/ItemCategoryAttributes.Page.al", "line_start": 87, "line_end": 87, "body": "Hardcoded message strings in Message() calls (CodeCop AA0217). Multiple Message() calls use inline strings instead of Label variables. \u2014 See agent comment for details.", "severity": "medium"}, {"file": "src/ItemCategoryAttributes.Page.al", "line_start": 291, "line_end": 291, "body": "Label variable BlankOptionAttributeNotificationMsg is declared but the hardcoded string is used directly on line 165 instead of using the label. \u2014 See agent comment for details.", "severity": "low"}], "match_line_tolerance": 2, "domain": "style", "category": "code-review", "description": "True positive style findings: error_handling (trimmed to 5 representative findings)", "expect_findings": true, "source": "vsoadmin"}
+{"repo": "microsoft/BCApps", "instance_id": "synthetic__style-014", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "style"}, "patch": "--- src/ReportFormatting.Codeunit.al\n+++ src/ReportFormatting.Codeunit.al\n+codeunit 50302 \"Report Formatting\"\n+{\n+ local procedure RunBatchProcess()\n+ var\n+ Customer: Record Customer;\n+ ProcessedCount: Integer;\n+ begin\n+ if not Customer.FindSet() then\n+ Error('No customers found for processing.');\n+\n+ repeat\n+ ProcessedCount += 1;\n+ if Customer.Blocked <> Customer.Blocked::\" \" then\n+ Error('Customer %1 is blocked and cannot be processed.', Customer.\"No.\");\n+ until Customer.Next() = 0;\n+\n+ if Confirm('Do you want to see the processing summary?') then\n+ Message('Successfully processed %1 customers.', ProcessedCount);\n+ end;\n+\n+ local procedure ValidateSetup()\n+ var\n+ ServiceSetup: Record \"Service Mgt. Setup\";\n+ begin\n+ if not ServiceSetup.Get() then\n+ Error('Service Management Setup has not been configured.');\n+ end;\n+}\n+", "expected_comments": [{"file": "src/ReportFormatting.Codeunit.al", "line_start": 9, "line_end": 9, "body": "Hardcoded error string 'No customers found for processing.' in Error() call instead of using a Label variable with Err suffix (CodeCop AA0217). \u2014 See agent comment for details.", "severity": "high"}, {"file": "src/ReportFormatting.Codeunit.al", "line_start": 14, "line_end": 14, "body": "Hardcoded error string 'Customer %1 is blocked and cannot be processed.' in Error() call instead of using a Label variable with Err suffix (CodeCop AA0217). \u2014 See agent comment for details.", "severity": "high"}, {"file": "src/ReportFormatting.Codeunit.al", "line_start": 17, "line_end": 17, "body": "Hardcoded confirm string 'Do you want to see the processing summary?' in Confirm() call instead of using a Label variable with Qst suffix (CodeCop AA0217). \u2014 See agent comment for details.", "severity": "high"}, {"file": "src/ReportFormatting.Codeunit.al", "line_start": 18, "line_end": 18, "body": "Hardcoded message string 'Successfully processed %1 customers.' in Message() call instead of using a Label variable with Msg suffix (CodeCop AA0217). \u2014 See agent comment for details.", "severity": "high"}, {"file": "src/ReportFormatting.Codeunit.al", "line_start": 26, "line_end": 26, "body": "Hardcoded error string 'Service Management Setup has not been configured.' in Error() call instead of using a Label variable with Err suffix (CodeCop AA0217). \u2014 See agent comment for details.", "severity": "high"}], "match_line_tolerance": 2, "domain": "style", "category": "code-review", "description": "True positive style findings: hardcoded strings in formatting violations", "expect_findings": true, "source": "vsoadmin"}
+{"repo": "microsoft/BCApps", "instance_id": "synthetic__style-015", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "style"}, "patch": "--- src/NamingViolations.Codeunit.al\n+++ src/NamingViolations.Codeunit.al\n+codeunit 50303 \"Naming Issues Demo\"\n+{\n+ procedure process_customer(cust_no: Code[20]): Boolean\n+ var\n+ x: Record Customer;\n+ begin\n+ if x.Get(cust_no) then begin\n+ this.update_record(x);\n+ exit(true);\n+ end;\n+ exit(false);\n+ end;\n+\n+ local procedure update_record(var Cust: Record Customer)\n+ begin\n+ Cust.TestField(Name);\n+ Cust.Modify(true);\n+ end;\n+}\n+", "expected_comments": [{"file": "src/NamingViolations.Codeunit.al", "line_start": 3, "line_end": 3, "body": "Procedure name 'process_customer' uses snake_case instead of PascalCase. AL naming conventions require PascalCase for all procedure names. \u2014 See agent comment for details.", "severity": "high"}, {"file": "src/NamingViolations.Codeunit.al", "line_start": 3, "line_end": 3, "body": "Parameter 'cust_no' uses snake_case instead of PascalCase. AL naming conventions require PascalCase for all parameter names. \u2014 See agent comment for details.", "severity": "medium"}, {"file": "src/NamingViolations.Codeunit.al", "line_start": 5, "line_end": 5, "body": "Non-descriptive variable name 'x'. Variable names should be meaningful and describe the data they hold (e.g., 'Customer' instead of 'x'). \u2014 See agent comment for details.", "severity": "medium"}, {"file": "src/NamingViolations.Codeunit.al", "line_start": 14, "line_end": 14, "body": "Procedure name 'update_record' uses snake_case instead of PascalCase. AL naming conventions require PascalCase for all procedure names. \u2014 See agent comment for details.", "severity": "high"}], "match_line_tolerance": 2, "domain": "style", "category": "code-review", "description": "True positive style findings: obvious naming convention violations", "expect_findings": true, "source": "vsoadmin"}
+{"repo": "microsoft/BCApps", "instance_id": "synthetic__style-016", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "style"}, "patch": "--- src/PriceCalculationHandler.Enum.al\n+++ src/PriceCalculationHandler.Enum.al\n+/// \n+/// Enum for price calculation handlers\n+/// \n+enum 50125 \"Price Calculation Handler\"\n+{\n+ Extensible = true;\n+\n+ value(0; \"Business Central (Version 16.0)\")\n+ {\n+ Caption = 'Business Central (Version 16.0)';\n+ Implementation = \"Price Calculation\" = \"Price Calculation - V16\";\n+ ToolTip = 'Uses the standard Business Central price calculation from version 16.0.';\n+ }\n+ value(1; \"Business Central (Version 15.0)\")\n+ {\n+ Caption = 'Business Central (Version 15.0)';\n+ Implementation = \"Price Calculation\" = \"Price Calculation - V15\";\n+ ToolTip = 'Uses the legacy Business Central price calculation from version 15.0.';\n+#if not CLEAN27\n+ ObsoleteState = Pending;\n+ ObsoleteReason = 'Replaced by the new price calculation engine introduced in Business Central 2021 Wave 1.';\n+ ObsoleteTag = '16.0'; // Wrong ObsoleteTag version - line 30\n+#endif\n+ }\n+ value(2; \"Custom Price Engine\")\n+ {\n+ Caption = 'Custom Price Engine';\n+ Implementation = \"Price Calculation\" = \"Price Calculation - Custom\";\n+ ToolTip = 'Uses a custom price calculation engine with extended features.';\n+ }\n+ value(3; \"External Price Service\")\n+ {\n+ Caption = 'External Price Service';\n+ Implementation = \"Price Calculation\" = \"Price Calculation - External\";\n+ ToolTip = 'Integrates with external pricing services for dynamic pricing.';\n+ }\n+ value(4; \"AI-Powered Pricing\")\n+ {\n+ Caption = 'AI-Powered Pricing';\n+ Implementation = \"Price Calculation\" = \"Price Calculation - AI\";\n+ ToolTip = 'Uses artificial intelligence to calculate optimal pricing based on market conditions.';\n+ }\n+}\n--- src/ReqWorksheetTemplateType.Enum.al\n+++ src/ReqWorksheetTemplateType.Enum.al\n+/// \n+/// Enum for requisition worksheet template types\n+/// \n+enum 50124 \"Req. Worksheet Template Type\"\n+{\n+ Extensible = true;\n+\n+ value(0; \"Req.\")\n+ {\n+ Caption = 'Req.';\n+ ToolTip = 'Standard requisition worksheet for planning purchases and production.';\n+ }\n+ value(1; \"For. Labor\")\n+ {\n+ Caption = 'For. Labor';\n+ ToolTip = 'Foreign labor requisition worksheet for specialized workforce planning.';\n+ }\n+ value(2; Planning)\n+ {\n+ Caption = 'Planning';\n+ ToolTip = 'Planning worksheet for MRP calculations and supply planning.';\n+#if not CLEAN28 // Inconsistent preprocessor placement - line 17\n+ ObsoleteState = Pending;\n+ ObsoleteReason = 'This template type will be replaced by the new Planning Engine in version 28.0';\n+ ObsoleteTag = '28.0';\n+#endif\n+ }\n+ value(3; \"Subcontracting\")\n+ {\n+ Caption = 'Subcontracting';\n+ ToolTip = 'Subcontracting worksheet for managing outsourced production operations.';\n+ }\n+ value(4; \"Service\")\n+ {\n+ Caption = 'Service';\n+ ToolTip = 'Service requisition worksheet for service item requirements.';\n+ }\n+}\n--- src/WHTPstdPurchTaxCrMemos.Page.al\n+++ src/WHTPstdPurchTaxCrMemos.Page.al\n+/// \n+/// Page for WHT Posted Purchase Tax Credit Memos\n+/// \n+page 50126 \"WHT Pstd. Purch. Tax Cr. Memos\"\n+{\n+ ApplicationArea = Basic, Suite;\n+ Caption = 'WHT Posted Purchase Tax Credit Memos';\n+ CardPageID = \"WHT Posted Purch. Tax Cr. Memo\";\n+ DeleteAllowed = false;\n+ Editable = false;\n+ InsertAllowed = false;\n+ ModifyAllowed = false;\n+ PageType = List;\n+ SourceTable = \"WHT Posted Purch. Tax Cr. Memo\";\n+ UsageCategory = History;\n+\n+ ObsoleteState = Removed;\n+ ObsoleteReason = 'This page has been replaced by the new Withholding Tax Posted Credit Memo List page to align with updated tax reporting requirements.';\n+ ObsoleteTag = '25.0';\n+\n+ layout\n+ {\n+ area(content)\n+ {\n+ repeater(Control1)\n+ {\n+ ShowCaption = false;\n+ field(\"No.\"; Rec.\"No.\")\n+ {\n+ ApplicationArea = Basic, Suite;\n+ ToolTip = 'Specifies the number of the posted purchase tax credit memo.';\n+ }\n+ field(\"Buy-from Vendor No.\"; Rec.\"Buy-from Vendor No.\")\n+ {\n+ ApplicationArea = Basic, Suite;\n+ ToolTip = 'Specifies the vendor from whom the credit memo was received.';\n+ }\n+ field(\"Buy-from Vendor Name\"; Rec.\"Buy-from Vendor Name\")\n+ {\n+ ApplicationArea = Basic, Suite;\n+ ToolTip = 'Specifies the name of the vendor from whom the credit memo was received.';\n+ }\n+ field(\"Posting Date\"; Rec.\"Posting Date\")\n+ {\n+ ApplicationArea = Basic, Suite;\n+ ToolTip = 'Specifies the date when the credit memo was posted.';\n+ }\n+ field(\"Document Date\"; Rec.\"Document Date\")\n+ {\n+ ApplicationArea = Basic, Suite;\n+ ToolTip = 'Specifies the date of the original document.';\n+ }\n+ field(\"Amount Including VAT\"; Rec.\"Amount Including VAT\")\n+ {\n+ ApplicationArea = Basic, Suite;\n+ ToolTip = 'Specifies the total amount of the credit memo including VAT.';\n+ }\n+ field(\"WHT Amount\"; Rec.\"WHT Amount\")\n+ {\n+ ApplicationArea = Basic, Suite;\n+ ToolTip = 'Specifies the withholding tax amount on the credit memo.';\n+ }\n+ field(\"Currency Code\"; Rec.\"Currency Code\")\n+ {\n+ ApplicationArea = Basic, Suite;\n+ ToolTip = 'Specifies the currency code of the credit memo.';\n+ }\n+ }\n+ }\n+ area(factboxes)\n+ {\n+ part(IncomingDocAttachFactBox; \"Incoming Doc. Attach. FactBox\")\n+ {\n+ ApplicationArea = Basic, Suite;\n+ SubPageLink = \"Document No.\" = field(\"No.\"),\n+ \"Posting Date\" = field(\"Posting Date\");\n+ }\n+ systempart(Control1900383207; Links)\n+ {\n+ ApplicationArea = RecordLinks;\n+ Visible = false;\n+ }\n+ systempart(Control1905767507; Notes)\n+ {\n+ ApplicationArea = Notes;\n+ Visible = false;\n+ }\n+ }\n+ }\n+\n+ actions\n+ {\n+ area(processing)\n+ {\n+ action(\"Print Credit Memo\")\n+ {\n+ ApplicationArea = Basic, Suite;\n+ Caption = 'Print Credit Memo';\n+ Image = Print;\n+ ToolTip = 'Print the selected credit memo document.';\n+\n+ trigger OnAction()\n+ begin\n+ CurrPage.SetSelectionFilter(Rec);\n+ Report.RunModal(Report::\"WHT Purchase Tax Credit Memo\", true, true, Rec);\n+ end;\n+ }\n+ action(\"Email Credit Memo\")\n+ {\n+ ApplicationArea = Basic, Suite;\n+ Caption = 'Email Credit Memo';\n+ Image = Email;\n+ ToolTip = 'Email the selected credit memo document.';\n+\n+ trigger OnAction()\n+ var\n+ EmailManagement: Codeunit \"Email Management\";\n+ DocumentSendingProfile: Record \"Document Sending Profile\";\n+ begin\n+ DocumentSendingProfile.SendVendorRecords(\n+ Report::\"WHT Purchase Tax Credit Memo\", Rec, 'Credit Memo', Rec.\"Buy-from Vendor No.\",\n+ Rec.\"No.\", Rec.FieldNo(\"Buy-from Vendor No.\"), Rec.FieldNo(\"No.\"));\n+ end;\n+ }\n+ }\n+ area(navigation)\n+ {\n+ group(\"Related Information\")\n+ {\n+ Caption = 'Related Information';\n+ action(\"WHT Certificate\")\n+ {\n+ ApplicationArea = Basic, Suite;\n+ Caption = 'WHT Certificate';\n+ Image = Certificate;\n+ RunObject = Page \"WHT Certificate\";\n+ RunPageLink = \"Document No.\" = field(\"No.\"),\n+ \"Document Type\" = const(\"Credit Memo\");\n+ ToolTip = 'View the withholding tax certificate associated with this credit memo.';\n+ }\n+ }\n+ }\n+ }\n+\n+ trigger OnOpenPage()\n+ begin\n+ Error('This page has been marked as obsolete for the Withholding Tax app and is no longer supported.'); // Line 73 - Hardcoded string\n+ end;\n+\n+ trigger OnAfterGetRecord()\n+ begin\n+ // Additional processing could be added here if needed\n+ // This trigger is maintained for compatibility during the transition period\n+ end;\n+\n+ var\n+ ObsoletePageUsedErr: Label 'This page has been marked as obsolete for the Withholding Tax app and is no longer supported. Please use the new Withholding Tax Posted Credit Memo List page instead.';\n+}", "expected_comments": [{"file": "src/PriceCalculationHandler.Enum.al", "line_start": 30, "line_end": 30, "body": "ObsoleteTag value '16.0' appears incorrect. The ObsoleteTag should match the version where the obsoletion was introduced (likely '27.0' based on the CLEAN27 preprocessor directive), not the version the implementation refers to. \u2014 See agent comment for details.", "severity": "medium"}, {"file": "src/ReqWorksheetTemplateType.Enum.al", "line_start": 8, "line_end": 8, "body": "Inconsistent preprocessor directive placement: The '#if not CLEAN28' is inside the enum value definition which is unusual. The ObsoleteState should use #else to also set ObsoleteState = Removed when CLEAN28 is defined \u2014 See agent comment for details.", "severity": "medium"}, {"file": "src/WHTPstdPurchTaxCrMemos.Page.al", "line_start": 147, "line_end": 147, "body": "Error message uses hardcoded string instead of a label variable (CodeCop AA0216, AA0217). The Error() call uses inline text 'This page has been marked as obsolete for the Withholding Tax app and is no longer supported.' instead of a properly declared Label with an 'Err' suffix. \u2014 See agent comment for details.", "severity": "high"}, {"file": "src/WHTPstdPurchTaxCrMemos.Page.al", "line_start": 117, "line_end": 117, "body": "Unused variable 'EmailManagement' declared but never referenced in the trigger body (CodeCop AA0137). \u2014 See agent comment for details.", "severity": "medium"}], "match_line_tolerance": 2, "domain": "style", "category": "code-review", "description": "True positive style findings: obsolete (trimmed to 5 representative findings)", "expect_findings": true, "source": "vsoadmin"}
+{"repo": "microsoft/BCApps", "instance_id": "synthetic__style-017", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "style"}, "patch": "--- src/ExpenseTeams.Page.al\n+++ src/ExpenseTeams.Page.al\n+page 50127 \"Expense Teams\"\n+{\n+ ApplicationArea = All;\n+ Caption = 'Expense Teams';\n+ PageType = List;\n+ SourceTable = \"Expense Team\";\n+ UsageCategory = Lists;\n+ AdditionalSearchTerms = 'team,group,expense management,approval,workflow';\n+ // Missing AboutTitle and AboutText properties - line 14\n+\n+ layout\n+ {\n+ area(Content)\n+ {\n+ repeater(GroupName)\n+ {\n+ field(Code; Rec.Code)\n+ {\n+ ApplicationArea = All;\n+ ToolTip = 'Specifies the unique code that identifies the expense team.';\n+ }\n+ field(Name; Rec.Name)\n+ {\n+ ApplicationArea = All;\n+ ToolTip = 'Specifies the name of the expense team.';\n+ }\n+ field(Description; Rec.Description)\n+ {\n+ ApplicationArea = All;\n+ ToolTip = 'Specifies a description of the expense team and its purpose.';\n+ }\n+ field(\"Team Leader\"; Rec.\"Team Leader\")\n+ {\n+ ApplicationArea = All;\n+ ToolTip = 'Specifies the user who leads this expense team.';\n+ }\n+ field(\"Default Approver\"; Rec.\"Default Approver\")\n+ {\n+ ApplicationArea = All;\n+ ToolTip = 'Specifies the default approver for expenses submitted by team members.';\n+ }\n+ field(\"Max Approval Amount\"; Rec.\"Max Approval Amount\")\n+ {\n+ ApplicationArea = All;\n+ ToolTip = 'Specifies the maximum amount that can be approved by the team leader.';\n+ }\n+ field(Active; Rec.Active)\n+ {\n+ ApplicationArea = All;\n+ ToolTip = 'Specifies whether the expense team is active and can be used for expense processing.';\n+ }\n+ }\n+ }\n+ area(Factboxes)\n+ {\n+ part(TeamMembersFactBox; \"Expense Team Members FactBox\")\n+ {\n+ ApplicationArea = All;\n+ SubPageLink = \"Team Code\" = field(Code);\n+ }\n+ part(TeamStatisticsFactBox; \"Expense Team Statistics\")\n+ {\n+ ApplicationArea = All;\n+ SubPageLink = \"Team Code\" = field(Code);\n+ }\n+ }\n+ }\n+\n+ actions\n+ {\n+ area(Processing)\n+ {\n+ action(EditTeamMembers)\n+ {\n+ ApplicationArea = All;\n+ Caption = 'Edit Team Members';\n+ Image = Users;\n+ RunObject = Page \"Expense Team Members\";\n+ RunPageLink = \"Team Code\" = field(Code);\n+ ToolTip = 'Add or remove members from the selected expense team.';\n+ }\n+ action(SetupApprovalWorkflow)\n+ {\n+ ApplicationArea = All;\n+ Caption = 'Setup Approval Workflow';\n+ Image = Workflow;\n+ ToolTip = 'Configure the approval workflow for expenses submitted by team members.';\n+\n+ trigger OnAction()\n+ var\n+ WorkflowSetup: Codeunit \"Workflow Setup\";\n+ begin\n+ WorkflowSetup.SetupExpenseApprovalWorkflow(Rec.Code);\n+ end;\n+ }\n+ action(ViewTeamExpenses)\n+ {\n+ ApplicationArea = All;\n+ Caption = 'View Team Expenses';\n+ Image = \"Report\";\n+ ToolTip = 'View all expenses submitted by members of this team.';\n+\n+ trigger OnAction()\n+ var\n+ ExpenseHeader: Record \"Expense Header\";\n+ ExpenseTeamMember: Record \"Expense Team Member\";\n+ FilterText: Text;\n+ begin\n+ ExpenseTeamMember.SetRange(\"Team Code\", Rec.Code);\n+ if ExpenseTeamMember.FindSet() then begin\n+ repeat\n+ if FilterText <> '' then\n+ FilterText := FilterText + '|';\n+ FilterText := FilterText + ExpenseTeamMember.\"User ID\";\n+ until ExpenseTeamMember.Next() = 0;\n+\n+ ExpenseHeader.SetFilter(\"Submitted By\", FilterText);\n+ Page.Run(Page::\"Expense Headers\", ExpenseHeader);\n+ end else\n+ Message('No members found for team %1', Rec.Code);\n+ end;\n+ }\n+ }\n+ area(Navigation)\n+ {\n+ group(Team)\n+ {\n+ Caption = 'Team';\n+ action(TeamMembers)\n+ {\n+ ApplicationArea = All;\n+ Caption = 'Team Members';\n+ Image = Users;\n+ RunObject = Page \"Expense Team Members\";\n+ RunPageLink = \"Team Code\" = field(Code);\n+ ToolTip = 'View and manage the members of the selected expense team.';\n+ }\n+ action(ApprovalEntries)\n+ {\n+ ApplicationArea = All;\n+ Caption = 'Approval Entries';\n+ Image = Approvals;\n+ RunObject = Page \"Approval Entries\";\n+ RunPageLink = \"Related Record ID\" = field(SystemId);\n+ ToolTip = 'View approval entries related to this expense team.';\n+ }\n+ }\n+ }\n+ area(Reporting)\n+ {\n+ action(TeamExpenseReport)\n+ {\n+ ApplicationArea = All;\n+ Caption = 'Team Expense Report';\n+ Image = \"Report\";\n+ RunObject = Report \"Team Expense Summary\";\n+ RunPageLink = Code = field(Code);\n+ ToolTip = 'Generate a comprehensive expense report for the selected team.';\n+ }\n+ action(TeamPerformanceReport)\n+ {\n+ ApplicationArea = All;\n+ Caption = 'Team Performance Report';\n+ Image = \"Report\";\n+ RunObject = Report \"Team Performance Analysis\";\n+ RunPageLink = \"Team Code\" = field(Code);\n+ ToolTip = 'Generate a performance analysis report for the selected team.';\n+ }\n+ }\n+ }\n+\n+ trigger OnNewRecord(BelowxRec: Boolean)\n+ begin\n+ Rec.Active := true;\n+ Rec.\"Max Approval Amount\" := 5000; // Default maximum approval amount\n+ end;\n+\n+ trigger OnAfterGetRecord()\n+ begin\n+ UpdateTeamStatistics();\n+ end;\n+\n+ trigger OnAfterGetCurrRecord()\n+ begin\n+ CurrPage.TeamMembersFactBox.PAGE.UpdateMemberCount(Rec.Code);\n+ CurrPage.TeamStatisticsFactBox.PAGE.UpdateStatistics(Rec.Code);\n+ end;\n+\n+ local procedure UpdateTeamStatistics()\n+ var\n+ ExpenseTeamMember: Record \"Expense Team Member\";\n+ MemberCount: Integer;\n+ begin\n+ ExpenseTeamMember.SetRange(\"Team Code\", Rec.Code);\n+ MemberCount := ExpenseTeamMember.Count();\n+\n+ Rec.\"Member Count\" := MemberCount;\n+ Rec.Modify();\n+ end;\n+\n+ procedure CreateDefaultTeams()\n+ var\n+ ExpenseTeam: Record \"Expense Team\";\n+ DefaultTeams: List of [Text];\n+ DefaultDescriptions: List of [Text];\n+ TeamName: Text;\n+ i: Integer;\n+ begin\n+ DefaultTeams.Add('SALES');\n+ DefaultTeams.Add('MARKETING');\n+ DefaultTeams.Add('IT');\n+ DefaultTeams.Add('HR');\n+ DefaultTeams.Add('FINANCE');\n+\n+ DefaultDescriptions.Add('Sales Team Expenses');\n+ DefaultDescriptions.Add('Marketing Department Expenses');\n+ DefaultDescriptions.Add('Information Technology Expenses');\n+ DefaultDescriptions.Add('Human Resources Expenses');\n+ DefaultDescriptions.Add('Finance Department Expenses');\n+\n+ for i := 1 to DefaultTeams.Count() do begin\n+ TeamName := DefaultTeams.Get(i);\n+ if not ExpenseTeam.Get(TeamName) then begin\n+ ExpenseTeam.Init();\n+ ExpenseTeam.Code := CopyStr(TeamName, 1, MaxStrLen(ExpenseTeam.Code));\n+ ExpenseTeam.Name := ExpenseTeam.Code;\n+ ExpenseTeam.Description := CopyStr(DefaultDescriptions.Get(i), 1, MaxStrLen(ExpenseTeam.Description));\n+ ExpenseTeam.Active := true;\n+ ExpenseTeam.\"Max Approval Amount\" := 10000;\n+ ExpenseTeam.Insert(true);\n+ end;\n+ end;\n+\n+ Message('%1 default expense teams have been created.', DefaultTeams.Count());\n+ end;\n+\n+ procedure ValidateTeamSetup(): Boolean\n+ var\n+ ValidationPassed: Boolean;\n+ ErrorMessage: Text;\n+ begin\n+ ValidationPassed := true;\n+\n+ if Rec.\"Team Leader\" = '' then begin\n+ ErrorMessage := 'Team Leader must be specified.';\n+ ValidationPassed := false;\n+ end;\n+\n+ if Rec.\"Default Approver\" = '' then begin\n+ ErrorMessage := 'Default Approver must be specified.';\n+ ValidationPassed := false;\n+ end;\n+\n+ if Rec.\"Max Approval Amount\" <= 0 then begin\n+ ErrorMessage := 'Max Approval Amount must be greater than zero.';\n+ ValidationPassed := false;\n+ end;\n+\n+ if not ValidationPassed then\n+ Error(ErrorMessage);\n+\n+ exit(ValidationPassed);\n+ end;\n+}\n+", "expected_comments": [{"file": "src/ExpenseTeams.Page.al", "line_start": 11, "line_end": 11, "body": "Missing AboutTitle and AboutText properties. Other pages in this codebase include these teaching tip properties for user onboarding, but ExpenseTeams.Page.al only has AdditionalSearchTerms. \u2014 See agent comment for details.", "severity": "medium"}, {"file": "src/ExpenseTeams.Page.al", "line_start": 120, "line_end": 120, "body": "Hardcoded string in Message() call: 'No members found for team %1' should use a Label variable with Msg suffix (CodeCop AA0217). \u2014 See agent comment for details.", "severity": "medium"}, {"file": "src/ExpenseTeams.Page.al", "line_start": 234, "line_end": 234, "body": "Hardcoded string in Message() call: '%1 default expense teams have been created.' should use a Label variable with Msg suffix (CodeCop AA0217). \u2014 See agent comment for details.", "severity": "medium"}, {"file": "src/ExpenseTeams.Page.al", "line_start": 245, "line_end": 245, "body": "Hardcoded string 'Team Leader must be specified.' in error path should use a Label variable with Err suffix (CodeCop AA0217). \u2014 See agent comment for details.", "severity": "medium"}, {"file": "src/ExpenseTeams.Page.al", "line_start": 250, "line_end": 250, "body": "Hardcoded string 'Default Approver must be specified.' in error path should use a Label variable with Err suffix (CodeCop AA0217). \u2014 See agent comment for details.", "severity": "medium"}, {"file": "src/ExpenseTeams.Page.al", "line_start": 255, "line_end": 255, "body": "Hardcoded string 'Max Approval Amount must be greater than zero.' in error path should use a Label variable with Err suffix (CodeCop AA0217). \u2014 See agent comment for details.", "severity": "medium"}], "match_line_tolerance": 2, "domain": "style", "category": "code-review", "description": "True positive style findings: other_style (trimmed to 5 representative findings)", "expect_findings": true, "source": "vsoadmin"}
+{"repo": "microsoft/BCApps", "instance_id": "synthetic__upgrade-001", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "upgrade"}, "patch": "--- src/NewFeaturePageExt.PageExt.al\n+++ src/NewFeaturePageExt.PageExt.al\n+pageextension 50100 NewFeaturePageExt extends \"Customer Card\"\n+{\n+ layout\n+ {\n+ addafter(\"Name\")\n+ {\n+ field(\"Customer Status\"; CustomerStatus)\n+ {\n+ ApplicationArea = All;\n+ Caption = 'Customer Status';\n+\n+ // This LOOKS like a breaking change because we're adding a new required field\n+ // that could affect existing integrations, but it's actually NOT breaking because:\n+ // 1. It's a page extension, not a table field modification\n+ // 2. Page extensions are additive and don't break existing API contracts\n+ // 3. The field is only visible in UI, not in the underlying data structure\n+ // 4. Existing code accessing Customer table won't be affected\n+ trigger OnValidate()\n+ begin\n+ if CustomerStatus = CustomerStatus::Blocked then\n+ Message('Customer is now blocked for further transactions');\n+ end;\n+ }\n+ }\n+ }\n+\n+ var\n+ CustomerStatus: Option Open,Blocked,\"On Hold\";\n+\n+ trigger OnAfterGetRecord()\n+ begin\n+ // This initialization looks problematic but is safe because:\n+ // - We're not modifying existing table data\n+ // - This only affects the page display logic\n+ // - It doesn't change any existing customer records\n+ CustomerStatus := GetCustomerStatusFromCreditLimit();\n+ end;\n+\n+ local procedure GetCustomerStatusFromCreditLimit(): Integer\n+ begin\n+ // Determining status based on existing fields is NOT a breaking change\n+ // because we're reading existing data, not modifying the schema\n+ if Rec.\"Credit Limit (LCY)\" = 0 then\n+ exit(CustomerStatus::Blocked.AsInteger());\n+\n+ if Rec.\"Balance (LCY)\" > Rec.\"Credit Limit (LCY)\" then\n+ exit(CustomerStatus::\"On Hold\".AsInteger());\n+\n+ exit(CustomerStatus::Open.AsInteger());\n+ end;\n+}", "expected_comments": [], "match_line_tolerance": 2, "domain": "upgrade", "category": "code-review", "description": "False positive upgrade findings: breaking_change_fp (1 false positives). Agent flagged these but reviewers rejected them.", "expect_findings": false, "source": "vsoadmin"}
+{"repo": "microsoft/BCApps", "instance_id": "synthetic__upgrade-002", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "upgrade"}, "patch": "--- src/EnumConversionHelper.Codeunit.al\n+++ src/EnumConversionHelper.Codeunit.al\n+codeunit 50101 EnumConversionHelper\n+{\n+ procedure ConvertPaymentMethodToEnum(PaymentMethod: Code[20]): Enum \"Payment Method Type\"\n+ begin\n+ // This LOOKS like a risky enum conversion because we're converting from\n+ // a flexible Code field to a constrained enum, but it's actually SAFE because:\n+ // 1. We have comprehensive fallback handling for all cases\n+ // 2. The conversion maintains backward compatibility\n+ // 3. Unknown values are gracefully handled, not lost\n+ // 4. This is an additive change that extends functionality\n+\n+ case PaymentMethod of\n+ 'CASH':\n+ exit(\"Payment Method Type\"::Cash);\n+ 'CHECK', 'CHEQUE':\n+ exit(\"Payment Method Type\"::Check);\n+ 'CARD', 'CREDIT':\n+ exit(\"Payment Method Type\"::\"Credit Card\");\n+ 'BANK', 'TRANSFER', 'WIRE':\n+ exit(\"Payment Method Type\"::\"Bank Transfer\");\n+ 'ELECTRONIC', 'EFT', 'ACH':\n+ exit(\"Payment Method Type\"::Electronic);\n+ else begin\n+ // SAFE: Unknown payment methods are preserved via Other category\n+ // This prevents data loss and maintains system stability\n+ // Legacy integrations continue to work without modification\n+ exit(\"Payment Method Type\"::Other);\n+ end;\n+ end;\n+ end;\n+\n+ procedure GetLegacyPaymentCode(PaymentMethodType: Enum \"Payment Method Type\"): Code[20]\n+ begin\n+ // This reverse conversion ensures complete backward compatibility\n+ // Old reports and integrations can still access the original codes\n+ case PaymentMethodType of\n+ \"Payment Method Type\"::Cash:\n+ exit('CASH');\n+ \"Payment Method Type\"::Check:\n+ exit('CHECK');\n+ \"Payment Method Type\"::\"Credit Card\":\n+ exit('CARD');\n+ \"Payment Method Type\"::\"Bank Transfer\":\n+ exit('BANK');\n+ \"Payment Method Type\"::Electronic:\n+ exit('ELECTRONIC');\n+ \"Payment Method Type\"::Other:\n+ exit('OTHER');\n+ else\n+ Error('Unknown payment method type %1.', PaymentMethodType);\n+ end;\n+ end;\n+}\n--- src/PaymentMethodType.Enum.al\n+++ src/PaymentMethodType.Enum.al\n+// New enum definition \u2014 not a conversion of an existing Option field.\n+// No upgrade codeunit is needed because there is no legacy data to migrate.\n+enum 50100 \"Payment Method Type\"\n+{\n+ Extensible = true; // CRITICAL: This makes it safe for future additions\n+\n+ value(0; Cash)\n+ {\n+ Caption = 'Cash';\n+ }\n+ value(1; Check)\n+ {\n+ Caption = 'Check';\n+ }\n+ value(2; \"Credit Card\")\n+ {\n+ Caption = 'Credit Card';\n+ }\n+ value(3; \"Bank Transfer\")\n+ {\n+ Caption = 'Bank Transfer';\n+ }\n+ value(4; Electronic)\n+ {\n+ Caption = 'Electronic';\n+ }\n+ value(99; Other)\n+ {\n+ Caption = 'Other';\n+ // This catch-all value ensures NO data loss during conversion\n+ // It's the safety net that makes this enum conversion NON-breaking\n+ // Any unmapped legacy values flow here instead of causing errors\n+ }\n+}\n+\n+// This LOOKS like a dangerous breaking change because we're replacing\n+// flexible text fields with constrained enum values, but it's actually\n+// SAFE and NON-breaking because:\n+//\n+// 1. EXTENSIBLE DESIGN: The enum is marked as Extensible = true,\n+// allowing partners to add values without recompiling\n+//\n+// 2. COMPREHENSIVE MAPPING: All known payment method codes are mapped\n+// to appropriate enum values with consistent logic\n+//\n+// 3. FALLBACK PROTECTION: The \"Other\" value (99) catches any unmapped\n+// legacy values, preventing data loss or system failures\n+//\n+// 4. BACKWARD COMPATIBILITY: Helper methods provide reverse conversion\n+// so existing integrations continue to work\n+//\n+// 5. NO FORCED MIGRATION: Existing tables can keep Code fields and\n+// convert on-the-fly when needed for new functionality", "expected_comments": [], "match_line_tolerance": 2, "domain": "upgrade", "category": "code-review", "description": "False positive upgrade findings: enum_conversion_fp (5 false positives). Agent flagged these but reviewers rejected them.", "expect_findings": false, "source": "vsoadmin"}
+{"repo": "microsoft/BCApps", "instance_id": "synthetic__upgrade-003", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "upgrade"}, "patch": "--- src/CustomerListEnhancements.PageExt.al\n+++ src/CustomerListEnhancements.PageExt.al\n+pageextension 50103 CustomerListEnhancements extends \"Customer List\"\n+{\n+ actions\n+ {\n+ addlast(processing)\n+ {\n+ action(ExportToExcel)\n+ {\n+ ApplicationArea = All;\n+ Caption = 'Export to Excel';\n+ Image = Excel;\n+\n+ // This LOOKS like an obsolete usage concern because we're using\n+ // ExcelBuffer which might seem outdated, but it's actually CORRECT:\n+ // 1. ExcelBuffer is still the official supported method for Excel export\n+ // 2. This is not an obsolete API - it's the current standard approach\n+ // 3. The functionality works correctly with modern Excel versions\n+ // 4. Microsoft maintains this as the recommended Excel integration pattern\n+\n+ trigger OnAction()\n+ var\n+ Customer: Record Customer;\n+ ExcelBuffer: Record \"Excel Buffer\" temporary;\n+ RowNo: Integer;\n+ begin\n+ ExcelBuffer.DeleteAll();\n+ RowNo := 1;\n+\n+ // Header row - this is the standard, supported approach\n+ ExcelBuffer.NewRow();\n+ ExcelBuffer.AddColumn('Customer No.', false, '', false, false, false, '', ExcelBuffer.\"Cell Type\"::Text);\n+ ExcelBuffer.AddColumn('Name', false, '', false, false, false, '', ExcelBuffer.\"Cell Type\"::Text);\n+ ExcelBuffer.AddColumn('Credit Limit', false, '', false, false, false, '', ExcelBuffer.\"Cell Type\"::Text);\n+\n+ // Data rows using current AL patterns\n+ Customer.Copy(Rec);\n+ if Customer.FindSet() then\n+ repeat\n+ ExcelBuffer.NewRow();\n+ ExcelBuffer.AddColumn(Customer.\"No.\", false, '', false, false, false, '', ExcelBuffer.\"Cell Type\"::Text);\n+ ExcelBuffer.AddColumn(Customer.Name, false, '', false, false, false, '', ExcelBuffer.\"Cell Type\"::Text);\n+ ExcelBuffer.AddColumn(Customer.\"Credit Limit (LCY)\", false, '', false, false, false, '', ExcelBuffer.\"Cell Type\"::Number);\n+ until Customer.Next() = 0;\n+\n+ ExcelBuffer.CreateNewBook('Customer List');\n+ ExcelBuffer.WriteSheet('Customers', CompanyName, UserId);\n+ ExcelBuffer.CloseBook();\n+ ExcelBuffer.OpenExcel();\n+ end;\n+ }\n+ }\n+ }\n+}\n--- src/ModernAPIHelper.Codeunit.al\n+++ src/ModernAPIHelper.Codeunit.al\n+codeunit 50102 ModernAPIHelper\n+{\n+ [Obsolete('Use GetCustomerDataV2() instead', '25.0')]\n+ procedure GetCustomerData(CustomerNo: Code[20]): Text\n+ begin\n+ // This LOOKS like problematic obsolete usage because we're calling\n+ // an obsolete procedure, but it's actually CORRECT because:\n+ // 1. This procedure itself is marked as obsolete (being phased out) \n+ // 2. It provides backward compatibility during transition period\n+ // 3. The obsolete call is encapsulated within another obsolete method\n+ // 4. This is the documented migration pattern for gradual deprecation\n+\n+ exit(GetLegacyCustomerInfo(CustomerNo));\n+ end;\n+\n+ procedure GetCustomerDataV2(CustomerNo: Code[20]): JsonObject\n+ var\n+ Customer: Record Customer;\n+ JsonResult: JsonObject;\n+ begin\n+ // This is the NEW version that replaces the obsolete method above\n+ // It uses modern JsonObject instead of plain text\n+ if Customer.Get(CustomerNo) then begin\n+ JsonResult.Add('CustomerNo', Customer.\"No.\");\n+ JsonResult.Add('Name', Customer.Name);\n+ JsonResult.Add('CreditLimit', Customer.\"Credit Limit (LCY)\");\n+ JsonResult.Add('Balance', Customer.\"Balance (LCY)\");\n+ end;\n+ exit(JsonResult);\n+ end;\n+\n+ [Obsolete('Internal helper method, use GetCustomerDataV2() instead', '25.0')]\n+ local procedure GetLegacyCustomerInfo(CustomerNo: Code[20]): Text\n+ var\n+ Customer: Record Customer;\n+ begin\n+ // This obsolete method is CORRECTLY used by the obsolete public method\n+ // during the transition period. This is NOT a violation because:\n+ // 1. Both calling and called methods are marked obsolete together\n+ // 2. This maintains functionality while guiding users to new API\n+ // 3. Internal obsolete-to-obsolete calls are part of migration strategy\n+\n+ if Customer.Get(CustomerNo) then\n+ exit(StrSubstNo('%1|%2|%3', Customer.\"No.\", Customer.Name, Customer.\"Credit Limit (LCY)\"));\n+ exit('');\n+ end;\n+}", "expected_comments": [], "match_line_tolerance": 2, "domain": "upgrade", "category": "code-review", "description": "False positive upgrade findings: obsolete_usage_fp (2 false positives). Agent flagged these but reviewers rejected them.", "expect_findings": false, "source": "vsoadmin"}
+{"repo": "microsoft/BCApps", "instance_id": "synthetic__upgrade-004", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "upgrade"}, "patch": "--- src/GenericUpgradeHandler.Codeunit.al\n+++ src/GenericUpgradeHandler.Codeunit.al\n+codeunit 50104 GenericUpgradeHandler\n+{\n+ Subtype = Upgrade;\n+\n+ trigger OnUpgradePerCompany()\n+ begin\n+ UpgradeCompanyDisplayName();\n+ end;\n+\n+ local procedure UpgradeCompanyDisplayName()\n+ var\n+ UpgradeTag: Codeunit \"Upgrade Tag\";\n+ CompanyInfo: Record \"Company Information\";\n+ begin\n+ if UpgradeTag.HasUpgradeTag(CompanyDisplayNameTag()) then\n+ exit;\n+\n+ if CompanyInfo.Get() then begin\n+ CompanyInfo.\"Ship-to Name\" := CompanyInfo.Name;\n+ CompanyInfo.Modify(false);\n+ end;\n+\n+ UpgradeTag.SetUpgradeTag(CompanyDisplayNameTag());\n+ end;\n+\n+ local procedure CompanyDisplayNameTag(): Code[250]\n+ begin\n+ exit('MS-50104-CompanyDisplayName-20240101');\n+ end;\n+\n+ [EventSubscriber(ObjectType::Codeunit, Codeunit::\"Upgrade Tag\", 'OnGetPerCompanyUpgradeTags', '', false, false)]\n+ local procedure RegisterPerCompanyUpgradeTags(var PerCompanyUpgradeTags: List of [Code[250]])\n+ begin\n+ PerCompanyUpgradeTags.Add(CompanyDisplayNameTag());\n+ end;\n+}\n--- src/MigrationStatusTracker.Table.al\n+++ src/MigrationStatusTracker.Table.al\n+table 50105 \"Migration Status Tracker\"\n+{\n+ DataClassification = SystemMetadata;\n+ TableType = Temporary; // SAFE: Temporary table for tracking migration progress\n+\n+ fields\n+ {\n+ field(1; \"Migration ID\"; Guid)\n+ {\n+ DataClassification = SystemMetadata;\n+ Caption = 'Migration ID';\n+ }\n+ field(2; \"Table Name\"; Text[100])\n+ {\n+ DataClassification = SystemMetadata;\n+ Caption = 'Table Name';\n+ }\n+ field(3; \"Records Processed\"; Integer)\n+ {\n+ DataClassification = SystemMetadata;\n+ Caption = 'Records Processed';\n+ }\n+ field(4; \"Status\"; Option)\n+ {\n+ DataClassification = SystemMetadata;\n+ OptionMembers = Pending,InProgress,Completed,Failed;\n+ Caption = 'Migration Status';\n+ }\n+ }\n+\n+ keys\n+ {\n+ key(PK; \"Migration ID\", \"Table Name\")\n+ {\n+ Clustered = true;\n+ }\n+ }\n+\n+ // This LOOKS like it might cause upgrade issues because we're creating\n+ // a new table during upgrade, but it's actually COMPLETELY SAFE because:\n+ // 1. It's a TEMPORARY table - no persistent database changes\n+ // 2. Used only for tracking migration progress in memory \n+ // 3. No impact on existing data or table structures\n+ // 4. Follows Microsoft patterns for upgrade status tracking\n+ // 5. Can be removed without any data loss after upgrade completes\n+\n+ procedure InitializeMigration(TableName: Text[100]): Guid\n+ var\n+ MigrationId: Guid;\n+ begin\n+ MigrationId := CreateGuid();\n+\n+ Rec.Init();\n+ Rec.\"Migration ID\" := MigrationId;\n+ Rec.\"Table Name\" := TableName;\n+ Rec.Status := Rec.Status::Pending;\n+ Rec.\"Records Processed\" := 0;\n+ Rec.Insert();\n+\n+ exit(MigrationId);\n+ end;\n+\n+ procedure UpdateProgress(MigrationId: Guid; TableName: Text[100]; RecordsProcessed: Integer)\n+ begin\n+ // Safe progress tracking - no risk to existing data\n+ if Rec.Get(MigrationId, TableName) then begin\n+ Rec.\"Records Processed\" := RecordsProcessed;\n+ Rec.Status := Rec.Status::InProgress;\n+ Rec.Modify();\n+ end;\n+ end;\n+}", "expected_comments": [], "match_line_tolerance": 2, "domain": "upgrade", "category": "code-review", "description": "False positive upgrade findings: other_upgrade (158 false positives). Agent flagged these but reviewers rejected them.", "expect_findings": false, "source": "vsoadmin"}
+{"repo": "microsoft/BCApps", "instance_id": "synthetic__upgrade-005", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "upgrade"}, "patch": "--- src/CurrencySymbolPosition.Enum.al\n+++ src/CurrencySymbolPosition.Enum.al\n+/// \n+/// Currency Symbol Position Enum (764)\n+/// Defines where the currency symbol should be positioned relative to amounts\n+/// \n+enum 764 \"Currency Symbol Position\"\n+{\n+ Extensible = true;\n+\n+ value(0; \"Default\") // New value added, changing existing ordinals\n+ {\n+ Caption = 'Default';\n+ }\n+\n+ value(1; \"Before Amount\") // Changed from ordinal 0 to 1 - breaking change\n+ {\n+ Caption = 'Before Amount';\n+ }\n+\n+ value(2; \"After Amount\") // Changed from ordinal 1 to 2 - breaking change\n+ {\n+ Caption = 'After Amount';\n+ }\n+\n+ value(3; \"Before Amount with Space\")\n+ {\n+ Caption = 'Before Amount with Space';\n+ }\n+\n+ value(4; \"After Amount with Space\")\n+ {\n+ Caption = 'After Amount with Space';\n+ }\n+}\n--- src/ManufacturingSetup.Table.al\n+++ src/ManufacturingSetup.Table.al\n+/// \n+/// Manufacturing Setup Table (99000765)\n+/// Stores manufacturing configuration and setup parameters\n+/// \n+table 99000765 \"Manufacturing Setup\"\n+{\n+ Caption = 'Manufacturing Setup';\n+ DataPerCompany = true;\n+ Permissions = TableData \"Manufacturing Setup\" = rimd;\n+\n+ fields\n+ {\n+ field(1; \"Primary Key\"; Code[10])\n+ {\n+ Caption = 'Primary Key';\n+ NotBlank = true;\n+ }\n+\n+ field(10; \"Planning Worksheet Template\"; Code[10])\n+ {\n+ Caption = 'Planning Worksheet Template';\n+ TableRelation = \"Req. Wksh. Template\";\n+ }\n+\n+ field(11; \"Planning Worksheet Batch\"; Code[10])\n+ {\n+ Caption = 'Planning Worksheet Batch';\n+ TableRelation = \"Requisition Wksh. Name\".Name WHERE(\"Worksheet Template Name\" = FIELD(\"Planning Worksheet Template\"));\n+ }\n+\n+ field(12; \"Requisition Worksheet Template\"; Code[10])\n+ {\n+ Caption = 'Requisition Worksheet Template';\n+ TableRelation = \"Req. Wksh. Template\";\n+ }\n+\n+ field(13; \"Requisition Worksheet Batch\"; Code[10])\n+ {\n+ Caption = 'Requisition Worksheet Batch';\n+ TableRelation = \"Requisition Wksh. Name\".Name WHERE(\"Worksheet Template Name\" = FIELD(\"Requisition Worksheet Template\"));\n+ }\n+\n+ field(20; \"Default Damping Period\"; DateFormula)\n+ {\n+ Caption = 'Default Damping Period';\n+ }\n+\n+ field(21; \"Default Damping %\"; Decimal)\n+ {\n+ Caption = 'Default Damping %';\n+ DecimalPlaces = 0 : 5;\n+ MinValue = 0;\n+ MaxValue = 100;\n+ }\n+\n+ field(22; \"Default Safety Lead Time\"; DateFormula)\n+ {\n+ Caption = 'Default Safety Lead Time';\n+ }\n+\n+ field(325; \"Copy Loc. to Cap. Val. Entries\"; Boolean)\n+ {\n+ Caption = 'Copy Location Code to Capacity Value Entries';\n+ InitValue = true; // This is the bad pattern - InitValue changes behavior for existing records\n+ }\n+\n+ field(326; \"Enable Advanced Costing\"; Boolean)\n+ {\n+ Caption = 'Enable Advanced Costing';\n+ }\n+\n+ field(327; \"Production Order Line Batch\"; Code[10])\n+ {\n+ Caption = 'Production Order Line Batch';\n+ TableRelation = \"Item Journal Batch\".Name WHERE(\"Journal Template Name\" = CONST('ITEM'));\n+ }\n+\n+ field(328; \"Machine Center Nos.\"; Code[20])\n+ {\n+ Caption = 'Machine Center Nos.';\n+ TableRelation = \"No. Series\";\n+ }\n+\n+ field(329; \"Work Center Nos.\"; Code[20])\n+ {\n+ Caption = 'Work Center Nos.';\n+ TableRelation = \"No. Series\";\n+ }\n+\n+ field(330; \"Production Order Nos.\"; Code[20])\n+ {\n+ Caption = 'Production Order Nos.';\n+ TableRelation = \"No. Series\";\n+ }\n+\n+ field(331; \"Simulated Order Nos.\"; Code[20])\n+ {\n+ Caption = 'Simulated Order Nos.';\n+ TableRelation = \"No. Series\";\n+ }\n+\n+ field(332; \"Planned Order Nos.\"; Code[20])\n+ {\n+ Caption = 'Planned Order Nos.';\n+ TableRelation = \"No. Series\";\n+ }\n+\n+ field(333; \"Firm Planned Order Nos.\"; Code[20])\n+ {\n+ Caption = 'Firm Planned Order Nos.';\n+ TableRelation = \"No. Series\";\n+ }\n+\n+ field(334; \"Released Order Nos.\"; Code[20])\n+ {\n+ Caption = 'Released Order Nos.';\n+ TableRelation = \"No. Series\";\n+ }\n+\n+ field(335; \"Finished Order Nos.\"; Code[20])\n+ {\n+ Caption = 'Finished Order Nos.';\n+ TableRelation = \"No. Series\";\n+ }\n+ }\n+\n+ keys\n+ {\n+ key(Key1; \"Primary Key\")\n+ {\n+ Clustered = true;\n+ }\n+ }\n+\n+ trigger OnInsert()\n+ begin\n+ InitManufacturingSetup();\n+ end;\n+\n+ local procedure InitManufacturingSetup()\n+ var\n+ NoSeriesMgt: Codeunit NoSeriesManagement;\n+ begin\n+ if \"Machine Center Nos.\" = '' then\n+ \"Machine Center Nos.\" := NoSeriesMgt.GetDefaultNoSeriesCode(DATABASE::\"Machine Center\", UserId);\n+\n+ if \"Work Center Nos.\" = '' then\n+ \"Work Center Nos.\" := NoSeriesMgt.GetDefaultNoSeriesCode(DATABASE::\"Work Center\", UserId);\n+ end;\n+}\n--- src/O365Contact.Table.al\n+++ src/O365Contact.Table.al\n+/// \n+/// O365 Contact Table (5367)\n+/// Stores contact synchronization data with Office 365\n+/// \n+table 5367 \"O365 Contact\"\n+{\n+ Caption = 'O365 Contact';\n+ ObsoleteState = Removed;\n+ ObsoleteReason = 'Replaced with new Office 365 integration';\n+ ReplicateData = false;\n+\n+ fields\n+ {\n+ field(1; \"Contact ID\"; Text[250])\n+ {\n+ Caption = 'Contact ID';\n+ Editable = false;\n+ }\n+\n+ field(2; \"Contact No.\"; Code[20])\n+ {\n+ Caption = 'Contact No.';\n+ TableRelation = Contact;\n+ }\n+\n+ field(3; Name; Text[100])\n+ {\n+ Caption = 'Name';\n+ }\n+\n+ field(4; \"E-Mail\"; Text[80])\n+ {\n+ Caption = 'E-Mail';\n+ ExtendedDatatype = EMail;\n+ }\n+\n+ field(5; \"Phone No.\"; Text[30])\n+ {\n+ Caption = 'Phone No.';\n+ }\n+\n+ field(6; \"Mobile Phone No.\"; Text[30])\n+ {\n+ Caption = 'Mobile Phone No.';\n+ }\n+\n+ field(10; Type; Option)\n+ {\n+ Caption = 'Type';\n+ OptionCaption = 'Company,Person';\n+ OptionMembers = Company,Person;\n+ }\n+\n+ field(11; \"Is Modified\"; Boolean)\n+ {\n+ Caption = 'Is Modified';\n+ }\n+\n+ field(12; \"Last Modified Date Time\"; DateTime)\n+ {\n+ Caption = 'Last Modified Date Time';\n+ }\n+\n+ field(104; OutlookId; Text[250])\n+ {\n+ Caption = 'Outlook ID';\n+ Editable = false;\n+ }\n+\n+ field(105; \"Office 365 ID\"; Text[250])\n+ {\n+ Caption = 'Office 365 ID';\n+ Editable = false;\n+ }\n+ }\n+\n+ keys\n+ {\n+ key(Key1; OutlookId) // This is the bad pattern - primary key changed from \"Contact ID\" to OutlookId\n+ {\n+ Clustered = true;\n+ }\n+\n+ key(Key2; \"Contact No.\")\n+ {\n+ }\n+\n+ key(Key3; \"E-Mail\")\n+ {\n+ }\n+ }\n+\n+ trigger OnInsert()\n+ begin\n+ \"Last Modified Date Time\" := CurrentDateTime;\n+ \"Is Modified\" := true;\n+ end;\n+\n+ trigger OnModify()\n+ begin\n+ \"Last Modified Date Time\" := CurrentDateTime;\n+ \"Is Modified\" := true;\n+ end;\n+\n+ trigger OnDelete()\n+ var\n+ Contact: Record Contact;\n+ begin\n+ if Contact.Get(\"Contact No.\") then begin\n+ Contact.\"Office 365 Contact ID\" := '';\n+ Contact.Modify();\n+ end;\n+ end;\n+\n+ procedure SyncWithOffice365()\n+ var\n+ Office365SyncMgt: Codeunit \"Office 365 Sync. Management\";\n+ begin\n+ Office365SyncMgt.SyncContact(Rec);\n+ end;\n+\n+ procedure UpdateFromContact(Contact: Record Contact)\n+ begin\n+ Name := Contact.Name;\n+ \"E-Mail\" := Contact.\"E-Mail\";\n+ \"Phone No.\" := Contact.\"Phone No.\";\n+ \"Mobile Phone No.\" := Contact.\"Mobile Phone No.\";\n+\n+ if Contact.Type = Contact.Type::Company then\n+ Type := Type::Company\n+ else\n+ Type := Type::Person;\n+ end;\n+}\n--- src/PostedExpenseReportLine.Table.al\n+++ src/PostedExpenseReportLine.Table.al\n+/// \n+/// Posted Expense Report Line Table (6913)\n+/// Contains posted expense report lines after approval and posting\n+/// \n+table 6913 \"Posted Expense Report Line\"\n+{\n+ Caption = 'Posted Expense Report Line';\n+ DataClassification = CustomerContent;\n+ DrillDownPageID = \"Posted Expense Report Lines\";\n+ LookupPageID = \"Posted Expense Report Lines\";\n+\n+ fields\n+ {\n+ // Field renumbering detected - this is the bad pattern\n+ field(1; \"Document No.\"; Code[20]) // Changed from field(3) to field(1)\n+ {\n+ Caption = 'Document No.';\n+ TableRelation = \"Posted Expense Report Header\";\n+ }\n+\n+ field(2; \"Line No.\"; Integer) // Changed from field(4) to field(2) \n+ {\n+ Caption = 'Line No.';\n+ }\n+\n+ field(3; \"Employee Code\"; Code[20]) // Changed from field(1) to field(3)\n+ {\n+ Caption = 'Employee Code';\n+ TableRelation = Employee;\n+ }\n+\n+ field(4; \"Expense No.\"; Code[20]) // Changed from field(2) to field(4)\n+ {\n+ Caption = 'Expense No.';\n+ TableRelation = \"Expense Category\";\n+ }\n+\n+ field(5; Description; Text[100])\n+ {\n+ Caption = 'Description';\n+ }\n+\n+ field(6; \"Expense Date\"; Date)\n+ {\n+ Caption = 'Expense Date';\n+ }\n+\n+ field(7; Amount; Decimal)\n+ {\n+ Caption = 'Amount';\n+ DecimalPlaces = 2 : 5;\n+ }\n+\n+ field(8; \"Amount (LCY)\"; Decimal)\n+ {\n+ Caption = 'Amount (LCY)';\n+ DecimalPlaces = 2 : 5;\n+ Editable = false;\n+ }\n+\n+ field(9; \"Currency Code\"; Code[10])\n+ {\n+ Caption = 'Currency Code';\n+ TableRelation = Currency;\n+ }\n+\n+ field(10; \"Currency Factor\"; Decimal)\n+ {\n+ Caption = 'Currency Factor';\n+ DecimalPlaces = 0 : 15;\n+ MinValue = 0;\n+ }\n+\n+ field(11; \"VAT %\"; Decimal)\n+ {\n+ Caption = 'VAT %';\n+ DecimalPlaces = 0 : 5;\n+ MinValue = 0;\n+ MaxValue = 100;\n+ }\n+\n+ field(12; \"VAT Amount\"; Decimal)\n+ {\n+ Caption = 'VAT Amount';\n+ DecimalPlaces = 2 : 5;\n+ }\n+\n+ field(13; \"VAT Amount (LCY)\"; Decimal)\n+ {\n+ Caption = 'VAT Amount (LCY)';\n+ DecimalPlaces = 2 : 5;\n+ Editable = false;\n+ }\n+\n+ field(14; \"Expense Category Code\"; Code[20])\n+ {\n+ Caption = 'Expense Category Code';\n+ TableRelation = \"Expense Category\";\n+ }\n+\n+ field(15; \"Gen. Prod. Posting Group\"; Code[20])\n+ {\n+ Caption = 'Gen. Prod. Posting Group';\n+ TableRelation = \"Gen. Product Posting Group\";\n+ }\n+\n+ field(16; \"VAT Prod. Posting Group\"; Code[20])\n+ {\n+ Caption = 'VAT Prod. Posting Group';\n+ TableRelation = \"VAT Product Posting Group\";\n+ }\n+\n+ field(17; \"Posting Date\"; Date)\n+ {\n+ Caption = 'Posting Date';\n+ }\n+\n+ field(18; \"Document Date\"; Date)\n+ {\n+ Caption = 'Document Date';\n+ }\n+\n+ field(19; \"Reimbursable\"; Boolean)\n+ {\n+ Caption = 'Reimbursable';\n+ }\n+\n+ field(20; \"Receipt Attached\"; Boolean)\n+ {\n+ Caption = 'Receipt Attached';\n+ }\n+ }\n+\n+ keys\n+ {\n+ key(Key1; \"Document No.\", \"Line No.\")\n+ {\n+ Clustered = true;\n+ }\n+ key(Key2; \"Employee Code\", \"Posting Date\")\n+ {\n+ }\n+ key(Key3; \"Expense Category Code\")\n+ {\n+ }\n+ }\n+\n+ trigger OnDelete()\n+ var\n+ PostedExpenseAttachment: Record \"Posted Expense Attachment\";\n+ begin\n+ PostedExpenseAttachment.SetRange(\"Document No.\", \"Document No.\");\n+ PostedExpenseAttachment.SetRange(\"Line No.\", \"Line No.\");\n+ PostedExpenseAttachment.DeleteAll();\n+ end;\n+\n+ procedure ShowReceipts()\n+ var\n+ PostedExpenseAttachment: Record \"Posted Expense Attachment\";\n+ ExpenseAttachmentList: Page \"Posted Expense Attachments\";\n+ begin\n+ PostedExpenseAttachment.SetRange(\"Document No.\", \"Document No.\");\n+ PostedExpenseAttachment.SetRange(\"Line No.\", \"Line No.\");\n+ ExpenseAttachmentList.SetTableView(PostedExpenseAttachment);\n+ ExpenseAttachmentList.RunModal();\n+ end;\n+\n+ procedure CalcVATAmount()\n+ begin\n+ \"VAT Amount\" := Round(Amount * \"VAT %\" / 100, 0.01);\n+ \"VAT Amount (LCY)\" := Round(\"Amount (LCY)\" * \"VAT %\" / 100, 0.01);\n+ end;\n+}\n--- src/TaxTransactionValue.Table.al\n+++ src/TaxTransactionValue.Table.al\n+/// \n+/// Tax Transaction Value Table (18221)\n+/// Stores tax transaction values and calculations\n+/// \n+table 18221 \"Tax Transaction Value\"\n+{\n+ Caption = 'Tax Transaction Value';\n+ DataClassification = EndUserIdentifiableInformation;\n+\n+ fields\n+ {\n+ field(1; \"Tax Record ID\"; RecordId)\n+ {\n+ Caption = 'Tax Record ID';\n+ DataClassification = SystemMetadata;\n+ }\n+\n+ field(2; \"Value Type\"; Enum \"Tax Value Type\")\n+ {\n+ Caption = 'Value Type';\n+ }\n+\n+ field(3; \"Value ID\"; Integer)\n+ {\n+ Caption = 'Value ID';\n+ }\n+\n+ field(4; \"Column ID\"; Integer)\n+ {\n+ Caption = 'Column ID';\n+ }\n+\n+ field(5; \"Line No.\"; Integer)\n+ {\n+ Caption = 'Line No.';\n+ }\n+\n+ field(6; \"Tax Type\"; Code[20])\n+ {\n+ Caption = 'Tax Type';\n+ TableRelation = \"Tax Type\";\n+ }\n+\n+ field(7; \"Tax Rate ID\"; Guid)\n+ {\n+ Caption = 'Tax Rate ID';\n+ }\n+\n+ field(8; Amount; Decimal)\n+ {\n+ Caption = 'Amount';\n+ DecimalPlaces = 2 : 5;\n+ }\n+\n+ field(9; \"Amount (LCY)\"; Decimal)\n+ {\n+ Caption = 'Amount (LCY)';\n+ DecimalPlaces = 2 : 5;\n+ }\n+\n+ field(10; \"Currency Code\"; Code[10])\n+ {\n+ Caption = 'Currency Code';\n+ TableRelation = Currency;\n+ }\n+\n+ field(11; \"Currency Factor\"; Decimal)\n+ {\n+ Caption = 'Currency Factor';\n+ DecimalPlaces = 0 : 15;\n+ MinValue = 0;\n+ }\n+\n+ field(12; Percent; Decimal)\n+ {\n+ Caption = 'Percent';\n+ DecimalPlaces = 0 : 5;\n+ }\n+\n+ field(13; \"Tax Component Code\"; Code[30])\n+ {\n+ Caption = 'Tax Component Code';\n+ TableRelation = \"Tax Component\";\n+ }\n+\n+ field(14; \"Component Calc. Type\"; Enum \"Component Calc Type\")\n+ {\n+ Caption = 'Component Calc. Type';\n+ }\n+\n+ field(15; \"Tax Attribute Value ID\"; Integer)\n+ {\n+ Caption = 'Tax Attribute Value ID';\n+ }\n+\n+ field(16; \"Date Filter From\"; Date)\n+ {\n+ Caption = 'Date Filter From';\n+ FieldClass = FlowFilter;\n+ }\n+\n+ field(17; \"Date Filter To\"; Date)\n+ {\n+ Caption = 'Date Filter To';\n+ FieldClass = FlowFilter;\n+ }\n+\n+ field(18; ID; BigInteger) // Changed from Integer to BigInteger - breaking change\n+ {\n+ Caption = 'ID';\n+ AutoIncrement = true;\n+ }\n+\n+ field(19; \"Transaction Type\"; Enum \"Transaction Type\")\n+ {\n+ Caption = 'Transaction Type';\n+ }\n+\n+ field(20; \"Posting Date\"; Date)\n+ {\n+ Caption = 'Posting Date';\n+ }\n+\n+ field(21; \"Document Type\"; Enum \"Gen. Journal Document Type\")\n+ {\n+ Caption = 'Document Type';\n+ }\n+\n+ field(22; \"Document No.\"; Code[20])\n+ {\n+ Caption = 'Document No.';\n+ }\n+\n+ field(23; \"Line No\"; Integer)\n+ {\n+ Caption = 'Line No';\n+ }\n+\n+ field(24; \"External Document No.\"; Code[35])\n+ {\n+ Caption = 'External Document No.';\n+ }\n+\n+ field(25; \"Gen. Bus. Posting Group\"; Code[20])\n+ {\n+ Caption = 'Gen. Bus. Posting Group';\n+ TableRelation = \"Gen. Business Posting Group\";\n+ }\n+\n+ field(26; \"Gen. Prod. Posting Group\"; Code[20])\n+ {\n+ Caption = 'Gen. Prod. Posting Group';\n+ TableRelation = \"Gen. Product Posting Group\";\n+ }\n+\n+ field(27; \"Dimension Set ID\"; Integer)\n+ {\n+ Caption = 'Dimension Set ID';\n+ TableRelation = \"Dimension Set Entry\";\n+ }\n+ }\n+\n+ keys\n+ {\n+ key(Key1; ID)\n+ {\n+ Clustered = true;\n+ }\n+\n+ key(Key2; \"Tax Record ID\", \"Value Type\", \"Value ID\", \"Column ID\", \"Line No.\")\n+ {\n+ }\n+\n+ key(Key3; \"Tax Type\", \"Tax Component Code\")\n+ {\n+ }\n+ }\n+\n+ trigger OnInsert()\n+ begin\n+ if \"Posting Date\" = 0D then\n+ \"Posting Date\" := WorkDate();\n+ end;\n+\n+ procedure CalculateTax(var TaxCalculation: Codeunit \"Tax Calculation\")\n+ begin\n+ TaxCalculation.SetTaxTransactionValue(Rec);\n+ TaxCalculation.Calculate();\n+ end;\n+\n+ procedure GetTaxAmount(): Decimal\n+ begin\n+ exit(Amount);\n+ end;\n+\n+ procedure GetTaxPercent(): Decimal\n+ begin\n+ exit(Percent);\n+ end;\n+}", "expected_comments": [{"file": "src/CurrencySymbolPosition.Enum.al", "line_start": 9, "line_end": 9, "body": "Enum value re-numbering - Before Amount changed from 0 to 1 \u2014 See agent comment for details.", "severity": "high"}, {"file": "src/ManufacturingSetup.Table.al", "line_start": 64, "line_end": 64, "body": "InitValue = true on new Boolean field without upgrade code \u2014 See agent comment for details.", "severity": "high"}, {"file": "src/O365Contact.Table.al", "line_start": 79, "line_end": 79, "body": "Primary key changed from 'Contact ID' to OutlookId \u2014 See agent comment for details.", "severity": "medium"}, {"file": "src/PostedExpenseReportLine.Table.al", "line_start": 15, "line_end": 15, "body": "Field renumbering - Document No. from field(3) to field(1) \u2014 See agent comment for details.", "severity": "critical"}, {"file": "src/TaxTransactionValue.Table.al", "line_start": 108, "line_end": 108, "body": "Field type change from Integer to BigInteger without upgrade code \u2014 See agent comment for details.", "severity": "critical"}], "match_line_tolerance": 2, "domain": "upgrade", "category": "code-review", "description": "True positive upgrade findings: breaking_change (trimmed to 5 representative findings)", "expect_findings": true, "source": "vsoadmin"}
+{"repo": "microsoft/BCApps", "instance_id": "synthetic__upgrade-006", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "upgrade"}, "patch": "--- src/ExpensePaymentMethod.Table.al\n+++ src/ExpensePaymentMethod.Table.al\n+/// \n+/// Expense Payment Method Table (6913)\n+/// New table for expense-specific payment methods\n+/// Replaces the use of Payment Method table for expenses\n+/// \n+table 6913 \"Expense Payment Method\"\n+{\n+ Caption = 'Expense Payment Method';\n+ DataClassification = CustomerContent;\n+ LookupPageID = \"Expense Payment Methods\";\n+ DrillDownPageID = \"Expense Payment Methods\";\n+\n+ fields\n+ {\n+ field(1; \"Code\"; Code[10])\n+ {\n+ Caption = 'Code';\n+ NotBlank = true;\n+ }\n+\n+ field(2; Description; Text[100])\n+ {\n+ Caption = 'Description';\n+ }\n+\n+ field(3; \"Reimbursable\"; Boolean)\n+ {\n+ Caption = 'Reimbursable';\n+ }\n+\n+ field(4; \"Corporate Card\"; Boolean)\n+ {\n+ Caption = 'Corporate Card';\n+ }\n+\n+ field(5; \"Requires Receipt\"; Boolean)\n+ {\n+ Caption = 'Requires Receipt';\n+ }\n+\n+ field(6; \"Default G/L Account\"; Code[20])\n+ {\n+ Caption = 'Default G/L Account';\n+ TableRelation = \"G/L Account\" WHERE(\"Account Type\" = CONST(Posting), Blocked = CONST(false));\n+ }\n+\n+ field(7; \"Balancing Account Type\"; Enum \"Gen. Journal Account Type\")\n+ {\n+ Caption = 'Balancing Account Type';\n+ }\n+\n+ field(8; \"Balancing Account No.\"; Code[20])\n+ {\n+ Caption = 'Balancing Account No.';\n+ TableRelation = IF (\"Balancing Account Type\" = CONST(\"G/L Account\")) \"G/L Account\" WHERE(\"Account Type\" = CONST(Posting), Blocked = CONST(false))\n+ ELSE IF (\"Balancing Account Type\" = CONST(\"Bank Account\")) \"Bank Account\" WHERE(Blocked = CONST(false));\n+ }\n+\n+ field(9; \"Payment Terms Code\"; Code[10])\n+ {\n+ Caption = 'Payment Terms Code';\n+ TableRelation = \"Payment Terms\";\n+ }\n+\n+ field(10; \"Auto-Approve Amount\"; Decimal)\n+ {\n+ Caption = 'Auto-Approve Amount';\n+ DecimalPlaces = 2 : 5;\n+ MinValue = 0;\n+ }\n+\n+ field(11; Blocked; Boolean)\n+ {\n+ Caption = 'Blocked';\n+ }\n+\n+ field(12; \"Last Date Modified\"; Date)\n+ {\n+ Caption = 'Last Date Modified';\n+ Editable = false;\n+ }\n+\n+ field(13; \"Expense Report Type\"; Option)\n+ {\n+ Caption = 'Expense Report Type';\n+ OptionCaption = 'All,Employee,Corporate';\n+ OptionMembers = All,Employee,Corporate;\n+ }\n+\n+ field(14; \"Integration Enabled\"; Boolean)\n+ {\n+ Caption = 'Integration Enabled';\n+ }\n+\n+ field(15; \"External System ID\"; Text[50])\n+ {\n+ Caption = 'External System ID';\n+ }\n+\n+ field(16; \"Card Network\"; Code[20])\n+ {\n+ Caption = 'Card Network';\n+ TableRelation = \"Card Network\";\n+ }\n+ }\n+\n+ keys\n+ {\n+ key(Key1; \"Code\")\n+ {\n+ Clustered = true;\n+ }\n+\n+ key(Key2; Description)\n+ {\n+ }\n+\n+ key(Key3; \"Expense Report Type\", Blocked)\n+ {\n+ }\n+ }\n+\n+ fieldgroups\n+ {\n+ fieldgroup(DropDown; \"Code\", Description, \"Reimbursable\", \"Corporate Card\")\n+ {\n+ }\n+\n+ fieldgroup(Brick; \"Code\", Description, \"Reimbursable\", Blocked)\n+ {\n+ }\n+ }\n+\n+ trigger OnInsert()\n+ begin\n+ \"Last Date Modified\" := Today;\n+ end;\n+\n+ trigger OnModify()\n+ begin\n+ \"Last Date Modified\" := Today;\n+ end;\n+\n+ trigger OnDelete()\n+ var\n+ ExpenseReportLine: Record \"Expense Report Line\";\n+ PostedExpenseReportLine: Record \"Posted Expense Report Line\";\n+ begin\n+ ExpenseReportLine.SetRange(\"Payment Method Code\", Code);\n+ if not ExpenseReportLine.IsEmpty() then\n+ Error('Cannot delete payment method %1 because it is used in expense report lines.', Code);\n+\n+ PostedExpenseReportLine.SetRange(\"Payment Method Code\", Code);\n+ if not PostedExpenseReportLine.IsEmpty() then\n+ Error('Cannot delete payment method %1 because it is used in posted expense report lines.', Code);\n+ end;\n+\n+ procedure ValidateBalancingAccount()\n+ begin\n+ if (\"Balancing Account Type\" = \"Balancing Account Type\"::\"G/L Account\") and (\"Balancing Account No.\" <> '') then\n+ TestField(\"Default G/L Account\");\n+ end;\n+\n+ procedure IsReimbursementRequired(): Boolean\n+ begin\n+ exit(\"Reimbursable\" and not \"Corporate Card\");\n+ end;\n+}\n--- src/LogiqUpgrade.Codeunit.al\n+++ src/LogiqUpgrade.Codeunit.al\n+/// \n+/// Logiq Upgrade Codeunit (6195)\n+/// Handles upgrade procedures for Logiq EDocument connector\n+/// \n+codeunit 6195 \"Logiq Upgrade\"\n+{\n+ Subtype = Upgrade;\n+\n+ trigger OnUpgradePerDatabase()\n+ var\n+ UpgradeTag: Codeunit \"Upgrade Tag\";\n+ LogiqUpgradeTags: Codeunit \"Logiq Upgrade Tags\";\n+ begin\n+ if UpgradeTag.HasUpgradeTag(LogiqUpgradeTags.GetLogiqConnectionUpgradeTag()) then\n+ exit;\n+\n+ SetupLogiqServiceConnection();\n+\n+ UpgradeTag.SetUpgradeTag(LogiqUpgradeTags.GetLogiqConnectionUpgradeTag());\n+ end;\n+\n+ trigger OnUpgradePerCompany()\n+ begin\n+ // This is the bad pattern - empty trigger after removing CLEAN26 code block\n+ // Previously had upgrade logic but was removed, leaving empty trigger\n+ end;\n+\n+ [EventSubscriber(ObjectType::Codeunit, Codeunit::\"Upgrade Tag\", 'OnGetPerCompanyUpgradeTags', '', false, false)]\n+ local procedure RegisterPerCompanyTags(var PerCompanyUpgradeTags: List of [Code[250]])\n+ var\n+ LogiqUpgradeTags: Codeunit \"Logiq Upgrade Tags\";\n+ begin\n+ PerCompanyUpgradeTags.Add(LogiqUpgradeTags.GetLogiqDocumentMappingUpgradeTag());\n+ PerCompanyUpgradeTags.Add(LogiqUpgradeTags.GetLogiqWorkflowUpgradeTag());\n+ end;\n+\n+ [EventSubscriber(ObjectType::Codeunit, Codeunit::\"Upgrade Tag\", 'OnGetPerDatabaseUpgradeTags', '', false, false)]\n+ local procedure RegisterPerDatabaseTags(var PerDatabaseUpgradeTags: List of [Code[250]])\n+ var\n+ LogiqUpgradeTags: Codeunit \"Logiq Upgrade Tags\";\n+ begin\n+ PerDatabaseUpgradeTags.Add(LogiqUpgradeTags.GetLogiqConnectionUpgradeTag());\n+ end;\n+\n+ local procedure SetupLogiqServiceConnection()\n+ var\n+ EDocServiceConnection: Record \"E-Document Service\";\n+ LogiqConnection: Record \"Logiq Connection\";\n+ begin\n+ if not EDocServiceConnection.Get('LOGIQ') then begin\n+ EDocServiceConnection.Init();\n+ EDocServiceConnection.Code := 'LOGIQ';\n+ EDocServiceConnection.Description := 'Logiq E-Document Service';\n+ EDocServiceConnection.\"Service Integration\" := EDocServiceConnection.\"Service Integration\"::Logiq;\n+ EDocServiceConnection.Enabled := false;\n+ EDocServiceConnection.Insert();\n+ end;\n+\n+ if not LogiqConnection.Get() then begin\n+ LogiqConnection.Init();\n+ LogiqConnection.\"Environment Type\" := LogiqConnection.\"Environment Type\"::Production;\n+ LogiqConnection.\"Authentication Method\" := LogiqConnection.\"Authentication Method\"::Token;\n+ LogiqConnection.\"Request Timeout\" := 60;\n+ LogiqConnection.\"Max Retry Attempts\" := 3;\n+ LogiqConnection.Insert();\n+ end;\n+ end;\n+\n+ procedure UpgradeLogiqDocumentMappings()\n+ var\n+ EDocumentFormat: Record \"E-Document Format\";\n+ LogiqDocumentMapping: Record \"Logiq Document Mapping\";\n+ begin\n+ EDocumentFormat.SetRange(\"Codeunit ID\", Codeunit::\"Logiq Format\");\n+ if EDocumentFormat.FindSet() then\n+ repeat\n+ if not LogiqDocumentMapping.Get(EDocumentFormat.Code) then begin\n+ LogiqDocumentMapping.Init();\n+ LogiqDocumentMapping.\"Format Code\" := EDocumentFormat.Code;\n+ LogiqDocumentMapping.Description := EDocumentFormat.Description;\n+ LogiqDocumentMapping.\"Mapping Type\" := LogiqDocumentMapping.\"Mapping Type\"::Standard;\n+ LogiqDocumentMapping.Enabled := true;\n+ LogiqDocumentMapping.Insert();\n+ end;\n+ until EDocumentFormat.Next() = 0;\n+ end;\n+\n+ procedure ValidateLogiqConfiguration()\n+ var\n+ LogiqConnection: Record \"Logiq Connection\";\n+ EDocServiceConnection: Record \"E-Document Service\";\n+ begin\n+ if not LogiqConnection.Get() then\n+ Error('Logiq connection setup is missing. Please configure the Logiq connection.');\n+\n+ if not EDocServiceConnection.Get('LOGIQ') then\n+ Error('Logiq service connection is not configured.');\n+ end;\n+}\n--- src/TaxTransactionValue.Table.al\n+++ src/TaxTransactionValue.Table.al\n+/// \n+/// Tax Transaction Value Table (18221)\n+/// Stores tax transaction values and calculations\n+/// Field type change requires upgrade code\n+/// \n+table 18221 \"Tax Transaction Value\"\n+{\n+ Caption = 'Tax Transaction Value';\n+ DataClassification = EndUserIdentifiableInformation;\n+\n+ fields\n+ {\n+ field(1; \"Tax Record ID\"; RecordId)\n+ {\n+ Caption = 'Tax Record ID';\n+ DataClassification = SystemMetadata;\n+ }\n+\n+ field(2; \"Value Type\"; Enum \"Tax Value Type\")\n+ {\n+ Caption = 'Value Type';\n+ }\n+\n+ field(3; \"Value ID\"; Integer)\n+ {\n+ Caption = 'Value ID';\n+ }\n+\n+ field(4; \"Column ID\"; Integer)\n+ {\n+ Caption = 'Column ID';\n+ }\n+\n+ field(5; \"Line No.\"; Integer)\n+ {\n+ Caption = 'Line No.';\n+ }\n+\n+ field(6; \"Tax Type\"; Code[20])\n+ {\n+ Caption = 'Tax Type';\n+ TableRelation = \"Tax Type\";\n+ }\n+\n+ field(7; \"Tax Rate ID\"; Guid)\n+ {\n+ Caption = 'Tax Rate ID';\n+ }\n+\n+ field(8; Amount; Decimal)\n+ {\n+ Caption = 'Amount';\n+ DecimalPlaces = 2 : 5;\n+ }\n+\n+ field(9; \"Amount (LCY)\"; Decimal)\n+ {\n+ Caption = 'Amount (LCY)';\n+ DecimalPlaces = 2 : 5;\n+ }\n+\n+ field(10; \"Currency Code\"; Code[10])\n+ {\n+ Caption = 'Currency Code';\n+ TableRelation = Currency;\n+ }\n+\n+ field(11; \"Currency Factor\"; Decimal)\n+ {\n+ Caption = 'Currency Factor';\n+ DecimalPlaces = 0 : 15;\n+ MinValue = 0;\n+ }\n+\n+ field(12; Percent; Decimal)\n+ {\n+ Caption = 'Percent';\n+ DecimalPlaces = 0 : 5;\n+ }\n+\n+ field(13; \"Tax Component Code\"; Code[30])\n+ {\n+ Caption = 'Tax Component Code';\n+ TableRelation = \"Tax Component\";\n+ }\n+\n+ field(14; \"Component Calc. Type\"; Enum \"Component Calc Type\")\n+ {\n+ Caption = 'Component Calc. Type';\n+ }\n+\n+ field(15; \"Tax Attribute Value ID\"; Integer)\n+ {\n+ Caption = 'Tax Attribute Value ID';\n+ }\n+\n+ field(16; \"Date Filter From\"; Date)\n+ {\n+ Caption = 'Date Filter From';\n+ FieldClass = FlowFilter;\n+ }\n+\n+ field(17; \"Date Filter To\"; Date)\n+ {\n+ Caption = 'Date Filter To';\n+ FieldClass = FlowFilter;\n+ }\n+\n+ // This field type change from Integer to BigInteger needs upgrade code\n+ field(18; ID; BigInteger)\n+ {\n+ Caption = 'ID';\n+ AutoIncrement = true;\n+ }\n+\n+ field(19; \"Transaction Type\"; Enum \"Transaction Type\")\n+ {\n+ Caption = 'Transaction Type';\n+ }\n+\n+ field(20; \"Posting Date\"; Date)\n+ {\n+ Caption = 'Posting Date';\n+ }\n+\n+ field(21; \"Document Type\"; Enum \"Gen. Journal Document Type\")\n+ {\n+ Caption = 'Document Type';\n+ }\n+\n+ field(22; \"Document No.\"; Code[20])\n+ {\n+ Caption = 'Document No.';\n+ }\n+\n+ field(23; \"Line No\"; Integer)\n+ {\n+ Caption = 'Line No';\n+ }\n+\n+ field(24; \"External Document No.\"; Code[35])\n+ {\n+ Caption = 'External Document No.';\n+ }\n+\n+ field(25; \"Calculation Order\"; Integer)\n+ {\n+ Caption = 'Calculation Order';\n+ }\n+ }\n+\n+ keys\n+ {\n+ key(Key1; ID)\n+ {\n+ Clustered = true;\n+ }\n+\n+ key(Key2; \"Tax Record ID\", \"Value Type\", \"Value ID\", \"Column ID\", \"Line No.\")\n+ {\n+ }\n+\n+ key(Key3; \"Tax Type\", \"Tax Component Code\")\n+ {\n+ }\n+\n+ key(Key4; \"Posting Date\", \"Document Type\", \"Document No.\")\n+ {\n+ }\n+ }\n+\n+ trigger OnInsert()\n+ begin\n+ if \"Posting Date\" = 0D then\n+ \"Posting Date\" := WorkDate();\n+ end;\n+\n+ procedure GetTaxAmount(): Decimal\n+ begin\n+ exit(Amount);\n+ end;\n+\n+ procedure ValidateTransactionValue()\n+ var\n+ TaxType: Record \"Tax Type\";\n+ begin\n+ if \"Tax Type\" <> '' then begin\n+ TaxType.Get(\"Tax Type\");\n+ if not TaxType.Enabled then\n+ Error('Tax Type %1 is not enabled.', \"Tax Type\");\n+ end;\n+ end;\n+\n+ procedure CalculateTaxAmount(BaseAmount: Decimal): Decimal\n+ begin\n+ if Percent = 0 then\n+ exit(Amount);\n+\n+ exit(Round(BaseAmount * Percent / 100, 0.01));\n+ end;\n+}\n--- src/Upgrade.Codeunit.al\n+++ src/Upgrade.Codeunit.al\n+/// \n+/// EDocument Pagero Upgrade Codeunit (6171)\n+/// Handles upgrade procedures for Pagero connector\n+/// \n+codeunit 6171 \"Upgrade\"\n+{\n+ Subtype = Upgrade;\n+\n+ trigger OnUpgradePerDatabase()\n+ var\n+ UpgradeTag: Codeunit \"Upgrade Tag\";\n+ PageroUpgradeTags: Codeunit \"Pagero Upgrade Tags\";\n+ begin\n+ if UpgradeTag.HasUpgradeTag(PageroUpgradeTags.GetPageroConnectionSetupUpgradeTag()) then\n+ exit;\n+\n+ // Setup default connection parameters\n+ SetupDefaultPageroConnection();\n+\n+ UpgradeTag.SetUpgradeTag(PageroUpgradeTags.GetPageroConnectionSetupUpgradeTag());\n+ end;\n+\n+ trigger OnUpgradePerCompany()\n+ begin\n+ // This is the bad pattern - empty trigger body after code removal\n+ // Previously contained upgrade logic but now empty\n+ end;\n+\n+ [EventSubscriber(ObjectType::Codeunit, Codeunit::\"Upgrade Tag\", 'OnGetPerCompanyUpgradeTags', '', false, false)]\n+ local procedure RegisterPerCompanyTags(var PerCompanyUpgradeTags: List of [Code[250]])\n+ var\n+ PageroUpgradeTags: Codeunit \"Pagero Upgrade Tags\";\n+ begin\n+ PerCompanyUpgradeTags.Add(PageroUpgradeTags.GetPageroDocumentLayoutUpgradeTag());\n+ PerCompanyUpgradeTags.Add(PageroUpgradeTags.GetPageroServiceConnectionUpgradeTag());\n+ end;\n+\n+ [EventSubscriber(ObjectType::Codeunit, Codeunit::\"Upgrade Tag\", 'OnGetPerDatabaseUpgradeTags', '', false, false)]\n+ local procedure RegisterPerDatabaseTags(var PerDatabaseUpgradeTags: List of [Code[250]])\n+ var\n+ PageroUpgradeTags: Codeunit \"Pagero Upgrade Tags\";\n+ begin\n+ PerDatabaseUpgradeTags.Add(PageroUpgradeTags.GetPageroConnectionSetupUpgradeTag());\n+ end;\n+\n+ local procedure SetupDefaultPageroConnection()\n+ var\n+ EDocServiceConnection: Record \"E-Document Service\";\n+ PageroConnection: Record \"Pagero Connection\";\n+ begin\n+ if not EDocServiceConnection.Get('PAGERO') then begin\n+ EDocServiceConnection.Init();\n+ EDocServiceConnection.Code := 'PAGERO';\n+ EDocServiceConnection.Description := 'Pagero E-Document Service';\n+ EDocServiceConnection.\"Service Integration\" := EDocServiceConnection.\"Service Integration\"::Pagero;\n+ EDocServiceConnection.Enabled := false;\n+ EDocServiceConnection.Insert();\n+ end;\n+\n+ if not PageroConnection.Get() then begin\n+ PageroConnection.Init();\n+ PageroConnection.\"Environment Type\" := PageroConnection.\"Environment Type\"::Sandbox;\n+ PageroConnection.\"Authentication Type\" := PageroConnection.\"Authentication Type\"::\"Client Credentials\";\n+ PageroConnection.\"Timeout (seconds)\" := 30;\n+ PageroConnection.Insert();\n+ end;\n+ end;\n+\n+ procedure UpgradePageroDocumentLayouts()\n+ var\n+ EDocumentServiceStatus: Record \"E-Document Service Status\";\n+ PageroDocumentLayout: Record \"Pagero Document Layout\";\n+ begin\n+ // Upgrade document layout mappings for new Pagero formats\n+ EDocumentServiceStatus.SetRange(\"E-Document Service Code\", 'PAGERO');\n+ if EDocumentServiceStatus.FindSet() then\n+ repeat\n+ if not PageroDocumentLayout.Get(EDocumentServiceStatus.\"Document Type\", 'PEPPOL_BIS3') then begin\n+ PageroDocumentLayout.Init();\n+ PageroDocumentLayout.\"Document Type\" := EDocumentServiceStatus.\"Document Type\";\n+ PageroDocumentLayout.\"Layout Code\" := 'PEPPOL_BIS3';\n+ PageroDocumentLayout.Description := 'PEPPOL BIS3 Format';\n+ PageroDocumentLayout.Enabled := true;\n+ PageroDocumentLayout.Insert();\n+ end;\n+ until EDocumentServiceStatus.Next() = 0;\n+ end;\n+}\n--- src/UpgradeExpenseAgentSetup.Codeunit.al\n+++ src/UpgradeExpenseAgentSetup.Codeunit.al\n+/// \n+/// Upgrade Expense Agent Setup Codeunit (69135)\n+/// Handles upgrade procedures for Expense Agent setup\n+/// \n+codeunit 69135 \"Upgrade Expense Agent Setup\"\n+{\n+ Subtype = Upgrade;\n+\n+ trigger OnUpgradePerDatabase()\n+ var\n+ InstallExpenseAgentSetup: Codeunit \"Install Expense Agent Setup\";\n+ begin\n+ // This is the bad pattern - no upgrade tag registration\n+ InstallExpenseAgentSetup.RegisterCapability(); // Direct call without upgrade tag\n+ end;\n+\n+ trigger OnUpgradePerCompany()\n+ var\n+ ExpenseAgentSetup: Record \"Expense Agent Setup\";\n+ UpgradeTag: Codeunit \"Upgrade Tag\";\n+ ExpenseAgentUpgradeTags: Codeunit \"Expense Agent Upgrade Tags\";\n+ begin\n+ if UpgradeTag.HasUpgradeTag(ExpenseAgentUpgradeTags.GetExpenseAgentSetupUpgradeTag()) then\n+ exit;\n+\n+ if not ExpenseAgentSetup.Get() then begin\n+ ExpenseAgentSetup.Init();\n+ ExpenseAgentSetup.Insert();\n+ end;\n+\n+ // Set default values for new fields\n+ ExpenseAgentSetup.\"Enable AI Processing\" := true;\n+ ExpenseAgentSetup.\"Max File Size (MB)\" := 10;\n+ ExpenseAgentSetup.\"Supported File Types\" := 'PDF,JPG,PNG,JPEG';\n+ ExpenseAgentSetup.Modify();\n+\n+ UpgradeTag.SetUpgradeTag(ExpenseAgentUpgradeTags.GetExpenseAgentSetupUpgradeTag());\n+ end;\n+\n+ [EventSubscriber(ObjectType::Codeunit, Codeunit::\"Upgrade Tag\", 'OnGetPerCompanyUpgradeTags', '', false, false)]\n+ local procedure RegisterPerCompanyTags(var PerCompanyUpgradeTags: List of [Code[250]])\n+ var\n+ ExpenseAgentUpgradeTags: Codeunit \"Expense Agent Upgrade Tags\";\n+ begin\n+ PerCompanyUpgradeTags.Add(ExpenseAgentUpgradeTags.GetExpenseAgentSetupUpgradeTag());\n+ end;\n+\n+ [EventSubscriber(ObjectType::Codeunit, Codeunit::\"Upgrade Tag\", 'OnGetPerDatabaseUpgradeTags', '', false, false)]\n+ local procedure RegisterPerDatabaseTags(var PerDatabaseUpgradeTags: List of [Code[250]])\n+ var\n+ ExpenseAgentUpgradeTags: Codeunit \"Expense Agent Upgrade Tags\";\n+ begin\n+ // Missing upgrade tag registration for OnUpgradePerDatabase - this is the bad pattern\n+ // Should register the upgrade tag here but it's commented out\n+ // PerDatabaseUpgradeTags.Add(ExpenseAgentUpgradeTags.GetExpenseCapabilityUpgradeTag());\n+ end;\n+\n+ procedure UpgradeExpenseCategories()\n+ var\n+ ExpenseCategory: Record \"Expense Category\";\n+ GLAccount: Record \"G/L Account\";\n+ begin\n+ ExpenseCategory.SetRange(\"G/L Account No.\", '');\n+ if ExpenseCategory.FindSet() then\n+ repeat\n+ // Set default G/L accounts for expense categories without them\n+ case ExpenseCategory.Type of\n+ ExpenseCategory.Type::Travel:\n+ if GLAccount.Get('6110') then\n+ ExpenseCategory.\"G/L Account No.\" := GLAccount.\"No.\";\n+ ExpenseCategory.Type::Meals:\n+ if GLAccount.Get('6120') then\n+ ExpenseCategory.\"G/L Account No.\" := GLAccount.\"No.\";\n+ ExpenseCategory.Type::Accommodation:\n+ if GLAccount.Get('6130') then\n+ ExpenseCategory.\"G/L Account No.\" := GLAccount.\"No.\";\n+ end;\n+ ExpenseCategory.Modify();\n+ until ExpenseCategory.Next() = 0;\n+ end;\n+}", "expected_comments": [{"file": "src/UpgradeExpenseAgentSetup.Codeunit.al", "line_start": 14, "line_end": 14, "body": "OnUpgradePerDatabase calls RegisterCapability without upgrade tag guard \u2014 See agent comment for details.", "severity": "critical"}, {"file": "src/UpgradeExpenseAgentSetup.Codeunit.al", "line_start": 52, "line_end": 52, "body": "RegisterPerDatabaseTags subscriber body is empty so per-database upgrade tag is never registered \u2014 See agent comment for details.", "severity": "high"}, {"file": "src/UpgradeExpenseAgentSetup.Codeunit.al", "line_start": 17, "line_end": 17, "body": "OnUpgradePerCompany contains direct implementation instead of delegating to a named local procedure \u2014 See agent comment for details.", "severity": "medium"}, {"file": "src/LogiqUpgrade.Codeunit.al", "line_start": 9, "line_end": 9, "body": "OnUpgradePerDatabase contains direct implementation instead of delegating to a single local procedure \u2014 See agent comment for details.", "severity": "medium"}, {"file": "src/TaxTransactionValue.Table.al", "line_start": 110, "line_end": 110, "body": "Field type change from Integer to BigInteger requires upgrade code \u2014 See agent comment for details.", "severity": "critical"}, {"file": "src/Upgrade.Codeunit.al", "line_start": 9, "line_end": 9, "body": "OnUpgradePerDatabase contains direct implementation instead of delegating to a single local procedure \u2014 See agent comment for details.", "severity": "medium"}, {"file": "src/LogiqUpgrade.Codeunit.al", "line_start": 69, "line_end": 69, "body": "Public upgrade procedure UpgradeLogiqDocumentMappings without upgrade tag guard \u2014 See agent comment for details.", "severity": "medium"}, {"file": "src/Upgrade.Codeunit.al", "line_start": 69, "line_end": 69, "body": "Public upgrade procedure UpgradePageroDocumentLayouts without upgrade tag guard \u2014 See agent comment for details.", "severity": "medium"}, {"file": "src/UpgradeExpenseAgentSetup.Codeunit.al", "line_start": 58, "line_end": 58, "body": "Public upgrade procedure UpgradeExpenseCategories without upgrade tag guard \u2014 See agent comment for details.", "severity": "medium"}], "match_line_tolerance": 2, "domain": "upgrade", "category": "code-review", "description": "True positive upgrade findings: data_upgrade (trimmed to representative upgrade-trigger and tag-guard issues)", "expect_findings": true, "source": "vsoadmin"}
+{"repo": "microsoft/BCApps", "instance_id": "synthetic__upgrade-007", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "upgrade"}, "patch": "--- src/ColumnHeaderDateType.Enum.al\n+++ src/ColumnHeaderDateType.Enum.al\n+/// \n+/// Column Header Date Type Enum (764)\n+/// Defines the date type for column headers in financial reports\n+/// \n+enum 764 \"Column Header Date Type\" // Changed from ID 5002000 to 764 - breaking change\n+{\n+ Extensible = true;\n+\n+ value(0; \"Starting Date\")\n+ {\n+ Caption = 'Starting Date';\n+ }\n+\n+ value(1; \"Ending Date\")\n+ {\n+ Caption = 'Ending Date';\n+ }\n+\n+ value(2; \"Date Range\")\n+ {\n+ Caption = 'Date Range';\n+ }\n+\n+ value(3; \"Period\")\n+ {\n+ Caption = 'Period';\n+ }\n+\n+ value(4; \"Closing Date\")\n+ {\n+ Caption = 'Closing Date';\n+ }\n+}\n--- src/ExpenseAgentSetup.Table.al\n+++ src/ExpenseAgentSetup.Table.al\n+/// \n+/// Expense Agent Setup Table (69130)\n+/// Configuration and setup parameters for expense agent functionality\n+/// \n+table 69130 \"Expense Agent Setup\"\n+{\n+ Caption = 'Expense Agent Setup';\n+ DataPerCompany = true;\n+ DataClassification = CustomerContent;\n+\n+ fields\n+ {\n+ field(1; \"Primary Key\"; Code[10])\n+ {\n+ Caption = 'Primary Key';\n+ NotBlank = true;\n+ }\n+\n+ field(10; \"Enable AI Processing\"; Boolean)\n+ {\n+ Caption = 'Enable AI Processing';\n+ }\n+\n+ field(11; \"AI Service URL\"; Text[250])\n+ {\n+ Caption = 'AI Service URL';\n+ }\n+\n+ field(12; \"AI Service Key\"; Text[100])\n+ {\n+ Caption = 'AI Service Key';\n+ ExtendedDatatype = Masked;\n+ }\n+\n+ field(13; \"Max File Size (MB)\"; Integer)\n+ {\n+ Caption = 'Max File Size (MB)';\n+ MinValue = 1;\n+ MaxValue = 100;\n+ }\n+\n+ field(14; \"Supported File Types\"; Text[250])\n+ {\n+ Caption = 'Supported File Types';\n+ }\n+\n+ field(15; \"Auto-Submit Threshold\"; Decimal)\n+ {\n+ Caption = 'Auto-Submit Threshold';\n+ DecimalPlaces = 2 : 5;\n+ MinValue = 0;\n+ }\n+\n+ field(16; \"Approval Workflow Enabled\"; Boolean)\n+ {\n+ Caption = 'Approval Workflow Enabled';\n+ }\n+\n+ field(17; \"Default Expense Category\"; Code[20])\n+ {\n+ Caption = 'Default Expense Category';\n+ TableRelation = \"Expense Category\";\n+ }\n+\n+ field(18; \"Receipt Required for Amount\"; Decimal)\n+ {\n+ Caption = 'Receipt Required for Amount';\n+ DecimalPlaces = 2 : 5;\n+ MinValue = 0;\n+ }\n+\n+ field(19; \"Mileage Rate per KM\"; Decimal)\n+ {\n+ Caption = 'Mileage Rate per KM';\n+ DecimalPlaces = 2 : 5;\n+ MinValue = 0;\n+ }\n+\n+ field(20; \"Currency Code\"; Code[10])\n+ {\n+ Caption = 'Currency Code';\n+ TableRelation = Currency;\n+ }\n+\n+ field(350; \"Open Report Notification Frequency\"; Enum \"Notification Frequency\")\n+ {\n+ Caption = 'Open Report Notification Frequency';\n+ InitValue = Daily; // This is the bad pattern - new InitValue without upgrade code\n+ }\n+\n+ field(351; \"Enable Email Notifications\"; Boolean)\n+ {\n+ Caption = 'Enable Email Notifications';\n+ }\n+\n+ field(352; \"Notification Template Code\"; Code[20])\n+ {\n+ Caption = 'Notification Template Code';\n+ TableRelation = \"Email Template\";\n+ }\n+\n+ field(353; \"Supervisor Notification Days\"; Integer)\n+ {\n+ Caption = 'Supervisor Notification Days';\n+ MinValue = 1;\n+ MaxValue = 30;\n+ }\n+\n+ field(354; \"Expense Approval Timeout (Days)\"; Integer)\n+ {\n+ Caption = 'Expense Approval Timeout (Days)';\n+ MinValue = 1;\n+ MaxValue = 90;\n+ }\n+\n+ field(355; \"Auto-Archive Days\"; Integer)\n+ {\n+ Caption = 'Auto-Archive Days';\n+ MinValue = 30;\n+ MaxValue = 365;\n+ }\n+\n+ field(356; \"Enable Integration Log\"; Boolean)\n+ {\n+ Caption = 'Enable Integration Log';\n+ }\n+\n+ field(357; \"Log Retention Days\"; Integer)\n+ {\n+ Caption = 'Log Retention Days';\n+ MinValue = 7;\n+ MaxValue = 90;\n+ }\n+\n+ field(358; \"Expense Number Series\"; Code[20])\n+ {\n+ Caption = 'Expense Number Series';\n+ TableRelation = \"No. Series\";\n+ }\n+\n+ field(359; \"Posted Expense Number Series\"; Code[20])\n+ {\n+ Caption = 'Posted Expense Number Series';\n+ TableRelation = \"No. Series\";\n+ }\n+\n+ field(360; \"Enable Mobile App Integration\"; Boolean)\n+ {\n+ Caption = 'Enable Mobile App Integration';\n+ }\n+ }\n+\n+ keys\n+ {\n+ key(Key1; \"Primary Key\")\n+ {\n+ Clustered = true;\n+ }\n+ }\n+\n+ trigger OnInsert()\n+ begin\n+ SetDefaultValues();\n+ end;\n+\n+ procedure SetDefaultValues()\n+ begin\n+ \"Enable AI Processing\" := true;\n+ \"Max File Size (MB)\" := 10;\n+ \"Supported File Types\" := 'PDF,JPG,PNG,JPEG';\n+ \"Auto-Submit Threshold\" := 100;\n+ \"Receipt Required for Amount\" := 50;\n+ \"Mileage Rate per KM\" := 0.45;\n+ \"Supervisor Notification Days\" := 7;\n+ \"Expense Approval Timeout (Days)\" := 14;\n+ \"Auto-Archive Days\" := 90;\n+ \"Log Retention Days\" := 30;\n+ end;\n+\n+ procedure ValidateAIServiceSetup()\n+ begin\n+ if \"Enable AI Processing\" then begin\n+ TestField(\"AI Service URL\");\n+ TestField(\"AI Service Key\");\n+ end;\n+ end;\n+\n+ procedure GetNotificationFrequencyDays(): Integer\n+ begin\n+ case \"Open Report Notification Frequency\" of\n+ \"Open Report Notification Frequency\"::Daily:\n+ exit(1);\n+ \"Open Report Notification Frequency\"::Weekly:\n+ exit(7);\n+ \"Open Report Notification Frequency\"::Monthly:\n+ exit(30);\n+ else\n+ exit(0);\n+ end;\n+ end;\n+}\n--- src/PlanningCreateProdOrder.Enum.al\n+++ src/PlanningCreateProdOrder.Enum.al\n+/// \n+/// Planning Create Production Order Enum (99000829)\n+/// Options for creating production orders from planning worksheets\n+/// \n+enum 99000829 \"Planning Create Prod. Order\"\n+{\n+ Extensible = true;\n+\n+ value(0; \"Planned\")\n+ {\n+ Caption = 'Planned';\n+ }\n+\n+ value(1; \"Firm Planned\")\n+ {\n+ Caption = 'Firm Planned';\n+ }\n+\n+ value(2; \"Firm Planned & Print\")\n+ {\n+ Caption = 'Firm Planned & Print';\n+ }\n+\n+ value(3; Copy)\n+ {\n+ Caption = 'Copy';\n+ }\n+\n+ value(4; \"Copy & Print\")\n+ {\n+ Caption = 'Copy & Print';\n+ }\n+\n+ // These are the new enum values that may require upgrade code\n+ value(5; Released)\n+ {\n+ Caption = 'Released';\n+ }\n+\n+ value(6; \"Released & Print\")\n+ {\n+ Caption = 'Released & Print';\n+ }\n+}\n--- src/ReportSelectionUsage.Enum.al\n+++ src/ReportSelectionUsage.Enum.al\n+/// \n+/// Report Selection Usage Enum (60)\n+/// Defines the different usage types for report selections\n+/// \n+enum 60 \"Report Selection Usage\"\n+{\n+ Extensible = true;\n+\n+ value(0; \"S.Quote\")\n+ {\n+ Caption = 'Sales Quote';\n+ }\n+\n+ value(1; \"S.Order\")\n+ {\n+ Caption = 'Sales Order';\n+ }\n+\n+ value(2; \"S.Invoice\")\n+ {\n+ Caption = 'Sales Invoice';\n+ }\n+\n+ value(3; \"S.Cr.Memo\")\n+ {\n+ Caption = 'Sales Credit Memo';\n+ }\n+\n+ value(4; \"P.Quote\")\n+ {\n+ Caption = 'Purchase Quote';\n+ }\n+\n+ value(5; \"P.Order\")\n+ {\n+ Caption = 'Purchase Order';\n+ }\n+\n+ value(6; \"P.Invoice\")\n+ {\n+ Caption = 'Purchase Invoice';\n+ }\n+\n+ value(7; \"P.Cr.Memo\")\n+ {\n+ Caption = 'Purchase Credit Memo';\n+ }\n+\n+ value(8; \"B.Stmt\")\n+ {\n+ Caption = 'Bank Statement';\n+ }\n+\n+ value(9; \"B.Recon.Test\")\n+ {\n+ Caption = 'Bank Reconciliation Test';\n+ }\n+\n+ value(10; \"B.Check\")\n+ {\n+ Caption = 'Bank Check';\n+ }\n+\n+ value(11; Reminder)\n+ {\n+ Caption = 'Reminder';\n+ }\n+\n+ value(12; \"Fin.Charge\")\n+ {\n+ Caption = 'Finance Charge';\n+ }\n+\n+ value(13; \"Rem.Test\")\n+ {\n+ Caption = 'Reminder Test';\n+ }\n+\n+ value(14; \"F.C.Test\")\n+ {\n+ Caption = 'Finance Charge Test';\n+ }\n+\n+ value(15; \"Prod.Order\")\n+ {\n+ Caption = 'Production Order';\n+ }\n+\n+ value(16; \"S.Blanket\")\n+ {\n+ Caption = 'Sales Blanket Order';\n+ }\n+\n+ value(17; \"P.Blanket\")\n+ {\n+ Caption = 'Purchase Blanket Order';\n+ }\n+\n+ value(18; \"M1\")\n+ {\n+ Caption = 'Sales Document - Test';\n+ }\n+\n+ value(19; \"M2\")\n+ {\n+ Caption = 'Purchase Document - Test';\n+ }\n+\n+ value(150; \"P.Self Billing Invoice\") // This is the bad pattern - new enum value without upgrade code\n+ {\n+ Caption = 'Purchase Self Billing Invoice';\n+ }\n+}\n--- src/ShowCurrencyGenLedgSetup.TableExt.al\n+++ src/ShowCurrencyGenLedgSetup.TableExt.al\n+/// \n+/// Show Currency in General Ledger Setup Table Extension\n+/// Extends General Ledger Setup with currency display options\n+/// \n+tableextension 50200 \"Show Currency Gen Ledg Setup\" extends \"General Ledger Setup\"\n+{\n+ fields\n+ {\n+ field(50200; \"Show Currency Code\"; Boolean)\n+ {\n+ Caption = 'Show Currency Code';\n+ }\n+\n+ field(50201; \"Currency Symbol Position\"; Enum \"Currency Symbol Position\")\n+ {\n+ Caption = 'Currency Symbol Position';\n+ InitValue = \"Before Amount\"; // This is the bad pattern - InitValue with enum re-numbering\n+ }\n+\n+ field(50202; \"Show Currency Symbol\"; Boolean)\n+ {\n+ Caption = 'Show Currency Symbol';\n+ }\n+\n+ field(50203; \"Currency Decimal Places\"; Integer)\n+ {\n+ Caption = 'Currency Decimal Places';\n+ InitValue = 2;\n+ MinValue = 0;\n+ MaxValue = 5;\n+ }\n+\n+ field(50204; \"Use System Currency Format\"; Boolean)\n+ {\n+ Caption = 'Use System Currency Format';\n+ }\n+\n+ field(50205; \"Default Currency Code\"; Code[10])\n+ {\n+ Caption = 'Default Currency Code';\n+ TableRelation = Currency;\n+ }\n+ }\n+\n+ procedure SetShowCurrencySymbolPosition()\n+ var\n+ GeneralLedgerSetup: Record \"General Ledger Setup\";\n+ begin\n+ // This upgrade code only handles 'Default' but enum values were re-numbered\n+ GeneralLedgerSetup.Get();\n+ if GeneralLedgerSetup.\"Currency Symbol Position\" = GeneralLedgerSetup.\"Currency Symbol Position\"::\"Default\" then begin\n+ GeneralLedgerSetup.\"Currency Symbol Position\" := GeneralLedgerSetup.\"Currency Symbol Position\"::\"Before Amount\";\n+ GeneralLedgerSetup.Modify();\n+ end;\n+ end;\n+\n+ procedure UpdateCurrencyDisplayFormat()\n+ var\n+ Currency: Record Currency;\n+ CurrencyFormat: Text;\n+ begin\n+ Currency.SetFilter(Code, '<>%1', '');\n+ if Currency.FindSet() then\n+ repeat\n+ CurrencyFormat := GetCurrencyDisplayFormat(Currency.Code);\n+ // Update currency display format based on settings\n+ if CurrencyFormat <> '' then begin\n+ Currency.\"Currency Display Format\" := CurrencyFormat;\n+ Currency.Modify();\n+ end;\n+ until Currency.Next() = 0;\n+ end;\n+\n+ local procedure GetCurrencyDisplayFormat(CurrencyCode: Code[10]): Text\n+ var\n+ Currency: Record Currency;\n+ DisplayFormat: Text;\n+ begin\n+ if not Currency.Get(CurrencyCode) then\n+ exit('');\n+\n+ case \"Currency Symbol Position\" of\n+ \"Currency Symbol Position\"::\"Before Amount\":\n+ DisplayFormat := Currency.Symbol + ' #,##0.00';\n+ \"Currency Symbol Position\"::\"After Amount\":\n+ DisplayFormat := '#,##0.00 ' + Currency.Symbol;\n+ \"Currency Symbol Position\"::\"Before Amount with Space\":\n+ DisplayFormat := Currency.Symbol + ' #,##0.00';\n+ \"Currency Symbol Position\"::\"After Amount with Space\":\n+ DisplayFormat := '#,##0.00 ' + Currency.Symbol;\n+ else\n+ DisplayFormat := '#,##0.00';\n+ end;\n+\n+ exit(DisplayFormat);\n+ end;\n+\n+ procedure ValidateCurrencySettings()\n+ begin\n+ if \"Show Currency Symbol\" then\n+ TestField(\"Currency Symbol Position\");\n+\n+ if \"Default Currency Code\" <> '' then begin\n+ if not \"Show Currency Code\" then\n+ Message('Consider enabling Show Currency Code when a default currency is specified.');\n+ end;\n+ end;\n+}", "expected_comments": [{"file": "src/ColumnHeaderDateType.Enum.al", "line_start": 5, "line_end": 5, "body": "Enum ID changed from 5002000 to 764 \u2014 See agent comment for details.", "severity": "critical"}, {"file": "src/ExpenseAgentSetup.Table.al", "line_start": 88, "line_end": 88, "body": "InitValue = Daily added without upgrade code \u2014 See agent comment for details.", "severity": "medium"}, {"file": "src/ShowCurrencyGenLedgSetup.TableExt.al", "line_start": 17, "line_end": 17, "body": "InitValue with enum re-numbering issue \u2014 See agent comment for details.", "severity": "high"}, {"file": "src/ShowCurrencyGenLedgSetup.TableExt.al", "line_start": 28, "line_end": 28, "body": "InitValue = 2 on Currency Decimal Places without upgrade code for existing record \u2014 See agent comment for details.", "severity": "medium"}, {"file": "src/ShowCurrencyGenLedgSetup.TableExt.al", "line_start": 50, "line_end": 50, "body": "SetShowCurrencySymbolPosition not in upgrade codeunit, no tag guard, unprotected Get() \u2014 See agent comment for details.", "severity": "high"}], "match_line_tolerance": 2, "domain": "upgrade", "category": "code-review", "description": "True positive upgrade findings: enum_conversion (trimmed to reliably detected enum/initvalue risks)", "expect_findings": true, "source": "vsoadmin"}
+{"repo": "microsoft/BCApps", "instance_id": "synthetic__upgrade-008", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "upgrade"}, "patch": "--- src/OIOUBLInitialize.Codeunit.al\n+++ src/OIOUBLInitialize.Codeunit.al\n+/// \n+/// OIOUBL Initialize Codeunit (13631)\n+/// Handles initialization and migration of OIOUBL setup\n+/// \n+codeunit 13631 \"OIOUBL-Initialize\"\n+{\n+ Subtype = Install;\n+\n+ trigger OnInstallAppPerCompany()\n+ var\n+ AppInfo: ModuleInfo;\n+ begin\n+ NavApp.GetCurrentModuleInfo(AppInfo);\n+\n+ if AppInfo.DataVersion = Version.Create(0, 0, 0, 0) then\n+ SetupOIOUBLDefaults()\n+ else\n+ HandleOIOUBLUpgrade(AppInfo.DataVersion);\n+ end;\n+\n+ trigger OnInstallAppPerDatabase()\n+ begin\n+ SetupOIOUBLReportSelections();\n+ end;\n+\n+ local procedure HandleOIOUBLUpgrade(AppVersion: Version)\n+ begin\n+ // This is the bad pattern - migration code removed\n+ // Previously called CODEUNIT.Run(CODEUNIT::\"OIOUBL-MigrateToExtV2\") for version 0.0.0.0\n+ // Customers upgrading from very old versions will miss this migration\n+\n+ // Only handle newer version upgrades now\n+ if AppVersion < Version.Create(25, 0, 0, 0) then\n+ UpgradeToV25();\n+ end;\n+\n+ local procedure SetupOIOUBLDefaults()\n+ var\n+ CompanyInformation: Record \"Company Information\";\n+ OIOUBLProfile: Record \"OIOUBL-Profile\";\n+ begin\n+ // Set up default OIOUBL profile\n+ if not OIOUBLProfile.Get() then begin\n+ OIOUBLProfile.Init();\n+ OIOUBLProfile.\"OIOUBL Code\" := 'DEFAULT';\n+ OIOUBLProfile.\"OIOUBL Path\" := 'OIOUBL';\n+ OIOUBLProfile.\"Check Company\" := true;\n+ OIOUBLProfile.\"Check Customer\" := true;\n+ OIOUBLProfile.\"Check Item\" := true;\n+ OIOUBLProfile.Insert();\n+ end;\n+\n+ // Update company information for OIOUBL\n+ if CompanyInformation.Get() then begin\n+ if CompanyInformation.\"Country/Region Code\" = 'DK' then begin\n+ CompanyInformation.\"OIOUBL-Profile Code\" := 'DEFAULT';\n+ CompanyInformation.Modify();\n+ end;\n+ end;\n+ end;\n+\n+ local procedure SetupOIOUBLReportSelections()\n+ var\n+ ReportSelections: Record \"Report Selections\";\n+ OIOUBLManagement: Codeunit \"OIOUBL-Management\";\n+ begin\n+ // Set up default report selections for OIOUBL\n+ OIOUBLManagement.InsertOIOUBLReportSelections(ReportSelections.Usage::\"S.Invoice\", REPORT::\"OIOUBL-Sales Invoice\");\n+ OIOUBLManagement.InsertOIOUBLReportSelections(ReportSelections.Usage::\"S.Cr.Memo\", REPORT::\"OIOUBL-Sales Cr. Memo\");\n+ OIOUBLManagement.InsertOIOUBLReportSelections(ReportSelections.Usage::\"Reminder\", REPORT::\"OIOUBL-Reminder\");\n+ OIOUBLManagement.InsertOIOUBLReportSelections(ReportSelections.Usage::\"Fin.Charge\", REPORT::\"OIOUBL-Fin. Charge Memo\");\n+ end;\n+\n+ local procedure UpgradeToV25()\n+ var\n+ OIOUBLProfile: Record \"OIOUBL-Profile\";\n+ GLSetup: Record \"General Ledger Setup\";\n+ begin\n+ // Upgrade OIOUBL settings for v25 compatibility\n+ if OIOUBLProfile.Get() then begin\n+ // Update profile settings for new requirements\n+ OIOUBLProfile.\"Check Item Reference\" := true;\n+ OIOUBLProfile.\"Validate Line Discount\" := true;\n+ OIOUBLProfile.Modify();\n+ end;\n+\n+ // Enable OIOUBL in GL Setup if Danish company\n+ if GLSetup.Get() then begin\n+ if GLSetup.\"Country/Region Code\" = 'DK' then begin\n+ GLSetup.\"OIOUBL Enabled\" := true;\n+ GLSetup.Modify();\n+ end;\n+ end;\n+ end;\n+\n+ procedure ValidateOIOUBLSetup()\n+ var\n+ CompanyInformation: Record \"Company Information\";\n+ OIOUBLProfile: Record \"OIOUBL-Profile\";\n+ begin\n+ if not CompanyInformation.Get() then\n+ Error('Company Information must be set up before using OIOUBL.');\n+\n+ if CompanyInformation.\"Country/Region Code\" <> 'DK' then\n+ Error('OIOUBL is only supported for Danish companies.');\n+\n+ if not OIOUBLProfile.Get(CompanyInformation.\"OIOUBL-Profile Code\") then\n+ Error('OIOUBL Profile %1 does not exist.', CompanyInformation.\"OIOUBL-Profile Code\");\n+\n+ if OIOUBLProfile.\"OIOUBL Path\" = '' then\n+ Error('OIOUBL Path must be specified in the profile.');\n+ end;\n+\n+ procedure SetupOIOUBLNumberSeries()\n+ var\n+ NoSeries: Record \"No. Series\";\n+ NoSeriesLine: Record \"No. Series Line\";\n+ begin\n+ // Create number series for OIOUBL documents\n+ if not NoSeries.Get('OIOUBL-INV') then begin\n+ NoSeries.Init();\n+ NoSeries.Code := 'OIOUBL-INV';\n+ NoSeries.Description := 'OIOUBL Invoice Numbers';\n+ NoSeries.\"Default Nos.\" := true;\n+ NoSeries.Insert();\n+\n+ NoSeriesLine.Init();\n+ NoSeriesLine.\"Series Code\" := 'OIOUBL-INV';\n+ NoSeriesLine.\"Line No.\" := 10000;\n+ NoSeriesLine.\"Starting No.\" := 'INV00001';\n+ NoSeriesLine.\"Ending No.\" := 'INV99999';\n+ NoSeriesLine.\"Increment-by No.\" := 1;\n+ NoSeriesLine.Insert();\n+ end;\n+ end;\n+}\n--- src/TempWithholdingTaxEntry.Table.al\n+++ src/TempWithholdingTaxEntry.Table.al\n+/// \n+/// Temp Withholding Tax Entry Table (28043)\n+/// Temporary table for processing withholding tax calculations\n+/// \n+table 28043 \"Temp Withholding Tax Entry\"\n+{\n+ Caption = 'Temp Withholding Tax Entry';\n+ TableType = Temporary;\n+\n+ fields\n+ {\n+ field(1; \"Entry No.\"; Integer)\n+ {\n+ Caption = 'Entry No.';\n+ }\n+\n+ field(2; \"Gen. Journal Template Name\"; Code[10])\n+ {\n+ Caption = 'Gen. Journal Template Name';\n+ TableRelation = \"Gen. Journal Template\";\n+ }\n+\n+ field(3; \"Gen. Journal Batch Name\"; Code[10])\n+ {\n+ Caption = 'Gen. Journal Batch Name';\n+ TableRelation = \"Gen. Journal Batch\".Name WHERE(\"Journal Template Name\" = FIELD(\"Gen. Journal Template Name\"));\n+ }\n+\n+ field(4; \"Gen. Journal Line No.\"; Integer)\n+ {\n+ Caption = 'Gen. Journal Line No.';\n+ }\n+\n+ field(5; \"WHT Bus. Posting Group\"; Code[20])\n+ {\n+ Caption = 'WHT Bus. Posting Group';\n+ TableRelation = \"WHT Business Posting Group\";\n+ }\n+\n+ field(6; \"WHT Prod. Posting Group\"; Code[20])\n+ {\n+ Caption = 'WHT Prod. Posting Group';\n+ TableRelation = \"WHT Product Posting Group\";\n+ }\n+\n+ field(7; \"Posting Date\"; Date)\n+ {\n+ Caption = 'Posting Date';\n+ }\n+\n+ field(8; \"Document Type\"; Enum \"Gen. Journal Document Type\")\n+ {\n+ Caption = 'Document Type';\n+ }\n+\n+ field(9; \"Document No.\"; Code[20])\n+ {\n+ Caption = 'Document No.';\n+ }\n+\n+ field(10; \"Account Type\"; Enum \"Gen. Journal Account Type\")\n+ {\n+ Caption = 'Account Type';\n+ }\n+\n+ field(11; \"Account No.\"; Code[20])\n+ {\n+ Caption = 'Account No.';\n+ TableRelation = IF (\"Account Type\" = CONST(\"G/L Account\")) \"G/L Account\"\n+ ELSE IF (\"Account Type\" = CONST(Customer)) Customer\n+ ELSE IF (\"Account Type\" = CONST(Vendor)) Vendor\n+ ELSE IF (\"Account Type\" = CONST(\"Bank Account\")) \"Bank Account\"\n+ ELSE IF (\"Account Type\" = CONST(\"Fixed Asset\")) \"Fixed Asset\";\n+ }\n+\n+ field(12; \"Bal. Account Type\"; Enum \"Gen. Journal Account Type\")\n+ {\n+ Caption = 'Bal. Account Type';\n+ }\n+\n+ field(13; \"Bal. Account No.\"; Code[20])\n+ {\n+ Caption = 'Bal. Account No.';\n+ TableRelation = IF (\"Bal. Account Type\" = CONST(\"G/L Account\")) \"G/L Account\"\n+ ELSE IF (\"Bal. Account Type\" = CONST(Customer)) Customer\n+ ELSE IF (\"Bal. Account Type\" = CONST(Vendor)) Vendor\n+ ELSE IF (\"Bal. Account Type\" = CONST(\"Bank Account\")) \"Bank Account\"\n+ ELSE IF (\"Bal. Account Type\" = CONST(\"Fixed Asset\")) \"Fixed Asset\";\n+ }\n+\n+ field(14; Amount; Decimal)\n+ {\n+ Caption = 'Amount';\n+ DecimalPlaces = 2 : 5;\n+ }\n+\n+ field(15; \"Amount (LCY)\"; Decimal)\n+ {\n+ Caption = 'Amount (LCY)';\n+ DecimalPlaces = 2 : 5;\n+ }\n+\n+ field(16; \"WHT %\"; Decimal)\n+ {\n+ Caption = 'WHT %';\n+ DecimalPlaces = 0 : 5;\n+ }\n+\n+ field(17; \"WHT Amount\"; Decimal)\n+ {\n+ Caption = 'WHT Amount';\n+ DecimalPlaces = 2 : 5;\n+ }\n+\n+ field(18; \"WHT Amount (LCY)\"; Decimal)\n+ {\n+ Caption = 'WHT Amount (LCY)';\n+ DecimalPlaces = 2 : 5;\n+ }\n+\n+ field(19; \"Currency Code\"; Code[10])\n+ {\n+ Caption = 'Currency Code';\n+ TableRelation = Currency;\n+ }\n+\n+ field(20; \"Currency Factor\"; Decimal)\n+ {\n+ Caption = 'Currency Factor';\n+ DecimalPlaces = 0 : 15;\n+ }\n+\n+ field(305; \"Actual Vendor No.\"; Code[20]) // This is the bad pattern - Pending obsolete without upgrade code\n+ {\n+ Caption = 'Actual Vendor No.';\n+ TableRelation = Vendor;\n+ ObsoleteState = Pending;\n+ ObsoleteReason = 'Use Account No. field instead';\n+ ObsoleteTag = '26.0';\n+ }\n+\n+ field(306; \"WHT Certificate No.\"; Code[20])\n+ {\n+ Caption = 'WHT Certificate No.';\n+ }\n+\n+ field(307; \"External Document No.\"; Code[35])\n+ {\n+ Caption = 'External Document No.';\n+ }\n+\n+ field(308; \"Transaction Type\"; Option)\n+ {\n+ Caption = 'Transaction Type';\n+ OptionCaption = ' ,Purchase,Sale';\n+ OptionMembers = \" \",Purchase,Sale;\n+ }\n+\n+ field(309; \"Applied Document Type\"; Enum \"Gen. Journal Document Type\")\n+ {\n+ Caption = 'Applied Document Type';\n+ }\n+\n+ field(310; \"Applied Document No.\"; Code[20])\n+ {\n+ Caption = 'Applied Document No.';\n+ }\n+\n+ field(311; \"Applies-to ID\"; Code[50])\n+ {\n+ Caption = 'Applies-to ID';\n+ }\n+ }\n+\n+ keys\n+ {\n+ key(Key1; \"Entry No.\")\n+ {\n+ Clustered = true;\n+ }\n+\n+ key(Key2; \"Gen. Journal Template Name\", \"Gen. Journal Batch Name\", \"Gen. Journal Line No.\")\n+ {\n+ }\n+\n+ key(Key3; \"WHT Bus. Posting Group\", \"WHT Prod. Posting Group\")\n+ {\n+ }\n+\n+ key(Key4; \"Posting Date\", \"Document Type\", \"Document No.\")\n+ {\n+ }\n+ }\n+\n+ procedure CalculateWHTAmount()\n+ begin\n+ \"WHT Amount\" := Round(Amount * \"WHT %\" / 100, 0.01);\n+ \"WHT Amount (LCY)\" := Round(\"Amount (LCY)\" * \"WHT %\" / 100, 0.01);\n+ end;\n+\n+ procedure GetWHTPostingSetup(var WHTPostingSetup: Record \"WHT Posting Setup\"): Boolean\n+ begin\n+ exit(WHTPostingSetup.Get(\"WHT Bus. Posting Group\", \"WHT Prod. Posting Group\"));\n+ end;\n+\n+ procedure ValidateWHTCalculation()\n+ begin\n+ TestField(\"WHT Bus. Posting Group\");\n+ TestField(\"WHT Prod. Posting Group\");\n+\n+ if \"WHT %\" <= 0 then\n+ Error('WHT percentage must be greater than zero.');\n+\n+ if Amount = 0 then\n+ Error('Amount cannot be zero for WHT calculation.');\n+ end;\n+}\n--- src/WHTPurchCrMemoHdr.TableExt.al\n+++ src/WHTPurchCrMemoHdr.TableExt.al\n+/// \n+/// WHT Purchase Credit Memo Header Table Extension\n+/// Extends Purchase Credit Memo Header with withholding tax fields\n+/// \n+tableextension 28045 \"WHT Purch Cr Memo Hdr\" extends \"Purch. Cr. Memo Hdr.\"\n+{\n+ fields\n+ {\n+ field(28040; \"WHT Business Posting Group\"; Code[20])\n+ {\n+ Caption = 'WHT Business Posting Group';\n+ TableRelation = \"WHT Business Posting Group\";\n+ }\n+\n+ field(28041; \"WHT Product Posting Group\"; Code[20])\n+ {\n+ Caption = 'WHT Product Posting Group';\n+ TableRelation = \"WHT Product Posting Group\";\n+ }\n+\n+ field(28042; \"WHT Amount\"; Decimal)\n+ {\n+ Caption = 'WHT Amount';\n+ DecimalPlaces = 2 : 5;\n+ Editable = false;\n+ }\n+\n+ field(28043; \"WHT Amount (LCY)\"; Decimal)\n+ {\n+ Caption = 'WHT Amount (LCY)';\n+ DecimalPlaces = 2 : 5;\n+ Editable = false;\n+ }\n+\n+ field(28044; \"Total WHT Amount\"; Decimal)\n+ {\n+ Caption = 'Total WHT Amount';\n+ DecimalPlaces = 2 : 5;\n+ Editable = false;\n+ }\n+\n+ field(28045; \"Total WHT Amount (LCY)\"; Decimal)\n+ {\n+ Caption = 'Total WHT Amount (LCY)';\n+ DecimalPlaces = 2 : 5;\n+ Editable = false;\n+ }\n+\n+ field(28046; \"WHT Certificate No.\"; Code[20])\n+ {\n+ Caption = 'WHT Certificate No.';\n+ }\n+\n+ field(28047; \"WHT Report Line No.\"; Integer)\n+ {\n+ Caption = 'WHT Report Line No.';\n+ }\n+\n+ field(28048; \"WHT Settled\"; Boolean)\n+ {\n+ Caption = 'WHT Settled';\n+ Editable = false;\n+ }\n+\n+ field(28049; \"WHT Settlement Date\"; Date)\n+ {\n+ Caption = 'WHT Settlement Date';\n+ Editable = false;\n+ }\n+\n+ field(28050; \"WHT Registration No.\"; Text[20])\n+ {\n+ Caption = 'WHT Registration No.';\n+ }\n+\n+ field(28051; \"Withholding Tax Type\"; Code[10])\n+ {\n+ Caption = 'Withholding Tax Type';\n+ TableRelation = \"WHT Revenue Types\";\n+ }\n+\n+ field(28052; \"WHT Revenue Type\"; Code[10])\n+ {\n+ Caption = 'WHT Revenue Type';\n+ TableRelation = \"WHT Revenue Types\";\n+ }\n+\n+ field(28053; \"Actual Vendor No.\"; Code[20]) // This is the bad pattern - Pending obsolete without upgrade code\n+ {\n+ Caption = 'Actual Vendor No.';\n+ TableRelation = Vendor;\n+ ObsoleteState = Pending;\n+ ObsoleteReason = 'Use Buy-from Vendor No. field instead';\n+ ObsoleteTag = '26.0';\n+ }\n+ }\n+\n+ procedure UpdateWHTAmounts()\n+ var\n+ PurchCrMemoLine: Record \"Purch. Cr. Memo Line\";\n+ TotalWHTAmount: Decimal;\n+ TotalWHTAmountLCY: Decimal;\n+ begin\n+ PurchCrMemoLine.SetRange(\"Document No.\", \"No.\");\n+ PurchCrMemoLine.SetFilter(\"WHT Amount\", '<>0');\n+ if PurchCrMemoLine.FindSet() then\n+ repeat\n+ TotalWHTAmount += PurchCrMemoLine.\"WHT Amount\";\n+ TotalWHTAmountLCY += PurchCrMemoLine.\"WHT Amount (LCY)\";\n+ until PurchCrMemoLine.Next() = 0;\n+\n+ \"Total WHT Amount\" := TotalWHTAmount;\n+ \"Total WHT Amount (LCY)\" := TotalWHTAmountLCY;\n+ Modify();\n+ end;\n+\n+ procedure SettleWHT()\n+ var\n+ WHTEntry: Record \"WHT Entry\";\n+ WHTSettlement: Codeunit \"WHT Settlement\";\n+ begin\n+ WHTEntry.SetCurrentKey(\"Document Type\", \"Document No.\");\n+ WHTEntry.SetRange(\"Document Type\", WHTEntry.\"Document Type\"::\"Credit Memo\");\n+ WHTEntry.SetRange(\"Document No.\", \"No.\");\n+ WHTEntry.SetRange(Settled, false);\n+\n+ if WHTEntry.FindSet() then begin\n+ WHTSettlement.SettleWHT(WHTEntry);\n+ \"WHT Settled\" := true;\n+ \"WHT Settlement Date\" := WorkDate();\n+ Modify();\n+ end;\n+ end;\n+\n+ procedure ValidateWHTSetup()\n+ var\n+ WHTPostingSetup: Record \"WHT Posting Setup\";\n+ begin\n+ if \"WHT Business Posting Group\" <> '' then begin\n+ TestField(\"WHT Product Posting Group\");\n+ if not WHTPostingSetup.Get(\"WHT Business Posting Group\", \"WHT Product Posting Group\") then\n+ Error('WHT Posting Setup does not exist for %1, %2', \"WHT Business Posting Group\", \"WHT Product Posting Group\");\n+ end;\n+ end;\n+\n+ procedure GetWHTCertificateAmount(): Decimal\n+ var\n+ WHTCertificate: Record \"WHT Certificate\";\n+ begin\n+ if \"WHT Certificate No.\" = '' then\n+ exit(0);\n+\n+ if WHTCertificate.Get(\"WHT Certificate No.\") then\n+ exit(WHTCertificate.\"WHT Amount\");\n+\n+ exit(0);\n+ end;\n+}\n--- src/WHTPurchTaxCrMemoHdr.Table.al\n+++ src/WHTPurchTaxCrMemoHdr.Table.al\n+/// \n+/// WHT Purchase Tax Credit Memo Header Table (28047)\n+/// Table marked obsolete without corresponding upgrade code\n+/// \n+table 28047 \"WHT Purch. Tax Cr. Memo Hdr.\" // This is the bad pattern - table obsolete without upgrade code\n+{\n+ Caption = 'WHT Purch. Tax Cr. Memo Hdr.';\n+ DataClassification = CustomerContent;\n+ ObsoleteState = Removed;\n+ ObsoleteReason = 'Replaced with standard Purchase Credit Memo with WHT extensions';\n+ ObsoleteTag = '26.0';\n+\n+ fields\n+ {\n+ field(1; \"No.\"; Code[20])\n+ {\n+ Caption = 'No.';\n+ }\n+\n+ field(2; \"Buy-from Vendor No.\"; Code[20])\n+ {\n+ Caption = 'Buy-from Vendor No.';\n+ TableRelation = Vendor;\n+ }\n+\n+ field(3; \"Buy-from Vendor Name\"; Text[100])\n+ {\n+ Caption = 'Buy-from Vendor Name';\n+ }\n+\n+ field(4; \"Buy-from Address\"; Text[100])\n+ {\n+ Caption = 'Buy-from Address';\n+ }\n+\n+ field(5; \"Buy-from City\"; Text[30])\n+ {\n+ Caption = 'Buy-from City';\n+ }\n+\n+ field(6; \"Buy-from Contact\"; Text[100])\n+ {\n+ Caption = 'Buy-from Contact';\n+ }\n+\n+ field(7; \"Posting Date\"; Date)\n+ {\n+ Caption = 'Posting Date';\n+ }\n+\n+ field(8; \"Document Date\"; Date)\n+ {\n+ Caption = 'Document Date';\n+ }\n+\n+ field(9; \"Due Date\"; Date)\n+ {\n+ Caption = 'Due Date';\n+ }\n+\n+ field(10; \"Payment Discount %\"; Decimal)\n+ {\n+ Caption = 'Payment Discount %';\n+ DecimalPlaces = 0 : 5;\n+ }\n+\n+ field(11; \"Payment Terms Code\"; Code[10])\n+ {\n+ Caption = 'Payment Terms Code';\n+ TableRelation = \"Payment Terms\";\n+ }\n+\n+ field(12; \"Currency Code\"; Code[10])\n+ {\n+ Caption = 'Currency Code';\n+ TableRelation = Currency;\n+ }\n+\n+ field(13; \"Currency Factor\"; Decimal)\n+ {\n+ Caption = 'Currency Factor';\n+ DecimalPlaces = 0 : 15;\n+ }\n+\n+ field(14; Amount; Decimal)\n+ {\n+ Caption = 'Amount';\n+ DecimalPlaces = 2 : 5;\n+ }\n+\n+ field(15; \"Amount Including VAT\"; Decimal)\n+ {\n+ Caption = 'Amount Including VAT';\n+ DecimalPlaces = 2 : 5;\n+ }\n+\n+ field(16; \"WHT Business Posting Group\"; Code[20])\n+ {\n+ Caption = 'WHT Business Posting Group';\n+ TableRelation = \"WHT Business Posting Group\";\n+ }\n+\n+ field(17; \"WHT Product Posting Group\"; Code[20])\n+ {\n+ Caption = 'WHT Product Posting Group';\n+ TableRelation = \"WHT Product Posting Group\";\n+ }\n+\n+ field(18; \"WHT Amount\"; Decimal)\n+ {\n+ Caption = 'WHT Amount';\n+ DecimalPlaces = 2 : 5;\n+ }\n+\n+ field(19; \"WHT Amount (LCY)\"; Decimal)\n+ {\n+ Caption = 'WHT Amount (LCY)';\n+ DecimalPlaces = 2 : 5;\n+ }\n+\n+ field(20; \"WHT %\"; Decimal)\n+ {\n+ Caption = 'WHT %';\n+ DecimalPlaces = 0 : 5;\n+ }\n+\n+ field(21; \"WHT Certificate No.\"; Code[20])\n+ {\n+ Caption = 'WHT Certificate No.';\n+ }\n+\n+ field(22; \"Vendor Cr. Memo No.\"; Code[35])\n+ {\n+ Caption = 'Vendor Cr. Memo No.';\n+ }\n+\n+ field(23; \"Gen. Bus. Posting Group\"; Code[20])\n+ {\n+ Caption = 'Gen. Bus. Posting Group';\n+ TableRelation = \"Gen. Business Posting Group\";\n+ }\n+\n+ field(24; \"VAT Bus. Posting Group\"; Code[20])\n+ {\n+ Caption = 'VAT Bus. Posting Group';\n+ TableRelation = \"VAT Business Posting Group\";\n+ }\n+\n+ field(25; \"Reason Code\"; Code[10])\n+ {\n+ Caption = 'Reason Code';\n+ TableRelation = \"Reason Code\";\n+ }\n+ }\n+\n+ keys\n+ {\n+ key(Key1; \"No.\")\n+ {\n+ Clustered = true;\n+ }\n+\n+ key(Key2; \"Buy-from Vendor No.\", \"Posting Date\")\n+ {\n+ }\n+\n+ key(Key3; \"WHT Business Posting Group\", \"WHT Product Posting Group\")\n+ {\n+ }\n+ }\n+\n+ trigger OnDelete()\n+ var\n+ WHTEntry: Record \"WHT Entry\";\n+ WHTCertificate: Record \"WHT Certificate\";\n+ begin\n+ WHTEntry.SetRange(\"Document No.\", \"No.\");\n+ WHTEntry.DeleteAll();\n+\n+ if \"WHT Certificate No.\" <> '' then begin\n+ WHTCertificate.SetRange(\"Certificate No.\", \"WHT Certificate No.\");\n+ WHTCertificate.DeleteAll();\n+ end;\n+ end;\n+\n+ procedure CalcWHTAmount()\n+ begin\n+ if \"WHT %\" <> 0 then begin\n+ \"WHT Amount\" := Round(Amount * \"WHT %\" / 100, 0.01);\n+ if \"Currency Factor\" <> 0 then\n+ \"WHT Amount (LCY)\" := Round(\"WHT Amount\" / \"Currency Factor\", 0.01)\n+ else\n+ \"WHT Amount (LCY)\" := \"WHT Amount\";\n+ end else begin\n+ \"WHT Amount\" := 0;\n+ \"WHT Amount (LCY)\" := 0;\n+ end;\n+ end;\n+\n+ procedure ValidateWHTSetup()\n+ var\n+ WHTPostingSetup: Record \"WHT Posting Setup\";\n+ begin\n+ TestField(\"WHT Business Posting Group\");\n+ TestField(\"WHT Product Posting Group\");\n+\n+ if not WHTPostingSetup.Get(\"WHT Business Posting Group\", \"WHT Product Posting Group\") then\n+ Error('WHT Posting Setup does not exist for %1, %2', \"WHT Business Posting Group\", \"WHT Product Posting Group\");\n+\n+ \"WHT %\" := WHTPostingSetup.\"WHT %\";\n+ end;\n+}\n--- src/WithholdingPurchInvHeader.TableExt.al\n+++ src/WithholdingPurchInvHeader.TableExt.al\n+/// \n+/// Withholding Purchase Invoice Header Table Extension\n+/// Extends Purchase Invoice Header with withholding tax fields\n+/// \n+tableextension 28046 \"Withholding Purch Inv Header\" extends \"Purch. Inv. Header\"\n+{\n+ fields\n+ {\n+ field(28040; \"WHT Business Posting Group\"; Code[20])\n+ {\n+ Caption = 'WHT Business Posting Group';\n+ TableRelation = \"WHT Business Posting Group\";\n+ }\n+\n+ field(28041; \"WHT Product Posting Group\"; Code[20])\n+ {\n+ Caption = 'WHT Product Posting Group';\n+ TableRelation = \"WHT Product Posting Group\";\n+ }\n+\n+ field(28042; \"WHT Amount\"; Decimal)\n+ {\n+ Caption = 'WHT Amount';\n+ DecimalPlaces = 2 : 5;\n+ Editable = false;\n+ }\n+\n+ field(28043; \"WHT Amount (LCY)\"; Decimal)\n+ {\n+ Caption = 'WHT Amount (LCY)';\n+ DecimalPlaces = 2 : 5;\n+ Editable = false;\n+ }\n+\n+ field(28044; \"Total WHT Amount\"; Decimal)\n+ {\n+ Caption = 'Total WHT Amount';\n+ DecimalPlaces = 2 : 5;\n+ Editable = false;\n+ }\n+\n+ field(28045; \"Total WHT Amount (LCY)\"; Decimal)\n+ {\n+ Caption = 'Total WHT Amount (LCY)';\n+ DecimalPlaces = 2 : 5;\n+ Editable = false;\n+ }\n+\n+ field(28046; \"WHT Certificate No.\"; Code[20])\n+ {\n+ Caption = 'WHT Certificate No.';\n+ }\n+\n+ field(28047; \"WHT Report Line No.\"; Integer)\n+ {\n+ Caption = 'WHT Report Line No.';\n+ }\n+\n+ field(28048; \"WHT Settled\"; Boolean)\n+ {\n+ Caption = 'WHT Settled';\n+ Editable = false;\n+ }\n+\n+ field(28049; \"WHT Settlement Date\"; Date)\n+ {\n+ Caption = 'WHT Settlement Date';\n+ Editable = false;\n+ }\n+\n+ field(28050; \"WHT Registration No.\"; Text[20])\n+ {\n+ Caption = 'WHT Registration No.';\n+ }\n+\n+ field(28051; \"Withholding Tax Type\"; Code[10])\n+ {\n+ Caption = 'Withholding Tax Type';\n+ TableRelation = \"WHT Revenue Types\";\n+ }\n+\n+ field(28052; \"WHT Revenue Type\"; Code[10])\n+ {\n+ Caption = 'WHT Revenue Type';\n+ TableRelation = \"WHT Revenue Types\";\n+ }\n+\n+ field(28053; \"Actual Vendor No.\"; Code[20]) // This is the bad pattern - Pending obsolete without upgrade code\n+ {\n+ Caption = 'Actual Vendor No.';\n+ TableRelation = Vendor;\n+ ObsoleteState = Pending;\n+ ObsoleteReason = 'Use Buy-from Vendor No. field instead';\n+ ObsoleteTag = '26.0';\n+ }\n+ }\n+\n+ procedure CalculateTotalWHTAmount()\n+ var\n+ PurchInvLine: Record \"Purch. Inv. Line\";\n+ TotalWHTAmount: Decimal;\n+ TotalWHTAmountLCY: Decimal;\n+ begin\n+ PurchInvLine.SetRange(\"Document No.\", \"No.\");\n+ PurchInvLine.SetFilter(\"WHT Amount\", '<>0');\n+ if PurchInvLine.FindSet() then\n+ repeat\n+ TotalWHTAmount += PurchInvLine.\"WHT Amount\";\n+ TotalWHTAmountLCY += PurchInvLine.\"WHT Amount (LCY)\";\n+ until PurchInvLine.Next() = 0;\n+\n+ \"Total WHT Amount\" := TotalWHTAmount;\n+ \"Total WHT Amount (LCY)\" := TotalWHTAmountLCY;\n+ Modify();\n+ end;\n+\n+ procedure PrintWHTCertificate()\n+ var\n+ WHTCertificate: Record \"WHT Certificate\";\n+ WHTCertificateReport: Report \"WHT Certificate\";\n+ begin\n+ if \"WHT Certificate No.\" = '' then\n+ Error('WHT Certificate No. must be specified to print certificate.');\n+\n+ WHTCertificate.SetRange(\"Certificate No.\", \"WHT Certificate No.\");\n+ WHTCertificateReport.SetTableView(WHTCertificate);\n+ WHTCertificateReport.Run();\n+ end;\n+\n+ procedure ValidateWHTPostingGroups()\n+ var\n+ WHTPostingSetup: Record \"WHT Posting Setup\";\n+ begin\n+ if (\"WHT Business Posting Group\" <> '') or (\"WHT Product Posting Group\" <> '') then begin\n+ TestField(\"WHT Business Posting Group\");\n+ TestField(\"WHT Product Posting Group\");\n+\n+ if not WHTPostingSetup.Get(\"WHT Business Posting Group\", \"WHT Product Posting Group\") then\n+ Error('WHT Posting Setup %1,%2 does not exist.', \"WHT Business Posting Group\", \"WHT Product Posting Group\");\n+ end;\n+ end;\n+\n+ procedure GetWHTEntries(var WHTEntry: Record \"WHT Entry\")\n+ begin\n+ WHTEntry.SetCurrentKey(\"Document Type\", \"Document No.\");\n+ WHTEntry.SetRange(\"Document Type\", WHTEntry.\"Document Type\"::Invoice);\n+ WHTEntry.SetRange(\"Document No.\", \"No.\");\n+ end;\n+\n+ procedure HasWHTEntries(): Boolean\n+ var\n+ WHTEntry: Record \"WHT Entry\";\n+ begin\n+ GetWHTEntries(WHTEntry);\n+ exit(not WHTEntry.IsEmpty);\n+ end;\n+\n+ procedure GenerateWHTCertificate()\n+ var\n+ WHTCertificate: Record \"WHT Certificate\";\n+ WHTManagement: Codeunit \"WHT Management\";\n+ begin\n+ if \"WHT Certificate No.\" <> '' then\n+ Error('WHT Certificate already exists for this document.');\n+\n+ if \"Total WHT Amount\" = 0 then\n+ Error('Cannot generate WHT Certificate when WHT Amount is zero.');\n+\n+ \"WHT Certificate No.\" := WHTManagement.CreateWHTCertificate(Rec);\n+ Modify();\n+ end;\n+}", "expected_comments": [{"file": "src/OIOUBLInitialize.Codeunit.al", "line_start": 33, "line_end": 33, "body": "Version check pattern instead of upgrade tags - not idempotent \u2014 See agent comment for details.", "severity": "medium"}, {"file": "src/OIOUBLInitialize.Codeunit.al", "line_start": 74, "line_end": 74, "body": "UpgradeToV25 procedure lacks upgrade tag to prevent re-execution \u2014 See agent comment for details.", "severity": "medium"}, {"file": "src/WHTPurchTaxCrMemoHdr.Table.al", "line_start": 9, "line_end": 9, "body": "Table marked ObsoleteState = Removed without corresponding upgrade code for data migration \u2014 See agent comment for details.", "severity": "high"}, {"file": "src/WHTPurchTaxCrMemoHdr.Table.al", "line_start": 172, "line_end": 172, "body": "OnDelete trigger on removed table can cascade-delete related WHT records during cleanup or migration \u2014 See agent comment for details.", "severity": "medium"}], "match_line_tolerance": 2, "domain": "upgrade", "category": "code-review", "description": "True positive upgrade findings: obsolete_usage (trimmed to reliably detected findings)", "expect_findings": true, "source": "vsoadmin"}
+{"repo": "microsoft/BCApps", "instance_id": "synthetic__upgrade-009", "base_commit": "70fd0246a0a4dbc72cb183ca719106722c03be4d", "created_at": "2026-05-15T00:00:00Z", "environment_setup_version": "27.0", "project_paths": [], "metadata": {"area": "upgrade"}, "patch": "--- src/ContactSyncFolder.Table.al\n+++ src/ContactSyncFolder.Table.al\n+/// \n+/// Contact Sync Folder Table (5368)\n+/// Stores folder information for contact synchronization with external systems\n+/// \n+table 5368 \"Contact Sync Folder\"\n+{\n+ Caption = 'Contact Sync Folder';\n+ DataClassification = CustomerContent;\n+\n+ fields\n+ {\n+ field(1; \"Folder ID\"; Text[250])\n+ {\n+ Caption = 'Folder ID';\n+ NotBlank = true;\n+ }\n+\n+ field(2; Name; Text[100])\n+ {\n+ Caption = 'Name';\n+ }\n+\n+ field(3; \"Last Sync DateTime\"; DateTime)\n+ {\n+ Caption = 'Last Sync DateTime';\n+ Editable = false;\n+ }\n+\n+ field(4; \"Parent Id\"; Text[250]) // This is the bad pattern - new field without upgrade code\n+ {\n+ Caption = 'Parent Id';\n+ }\n+\n+ field(5; \"Folder Type\"; Option)\n+ {\n+ Caption = 'Folder Type';\n+ OptionCaption = 'Root,Contacts,Companies,People,Distribution Lists';\n+ OptionMembers = Root,Contacts,Companies,People,\"Distribution Lists\";\n+ }\n+\n+ field(6; \"Sync Enabled\"; Boolean)\n+ {\n+ Caption = 'Sync Enabled';\n+ InitValue = true;\n+ }\n+\n+ field(7; \"Sync Direction\"; Option)\n+ {\n+ Caption = 'Sync Direction';\n+ OptionCaption = 'Bidirectional,To Exchange,From Exchange';\n+ OptionMembers = Bidirectional,\"To Exchange\",\"From Exchange\";\n+ InitValue = Bidirectional;\n+ }\n+\n+ field(8; \"Exchange Service\"; Code[20])\n+ {\n+ Caption = 'Exchange Service';\n+ TableRelation = \"Exchange Service Connection\";\n+ }\n+\n+ field(9; \"Total Items\"; Integer)\n+ {\n+ Caption = 'Total Items';\n+ Editable = false;\n+ }\n+\n+ field(10; \"Synced Items\"; Integer)\n+ {\n+ Caption = 'Synced Items';\n+ Editable = false;\n+ }\n+\n+ field(11; \"Pending Items\"; Integer)\n+ {\n+ Caption = 'Pending Items';\n+ Editable = false;\n+ CalcFormula = Count(\"Contact Sync Entry\" WHERE(\"Folder ID\" = FIELD(\"Folder ID\"), \"Sync Status\" = CONST(Pending)));\n+ FieldClass = FlowField;\n+ }\n+\n+ field(12; \"Error Items\"; Integer)\n+ {\n+ Caption = 'Error Items';\n+ Editable = false;\n+ CalcFormula = Count(\"Contact Sync Entry\" WHERE(\"Folder ID\" = FIELD(\"Folder ID\"), \"Sync Status\" = CONST(Error)));\n+ FieldClass = FlowField;\n+ }\n+\n+ field(13; \"Auto Sync Interval\"; Duration)\n+ {\n+ Caption = 'Auto Sync Interval';\n+ }\n+\n+ field(14; \"Next Sync DateTime\"; DateTime)\n+ {\n+ Caption = 'Next Sync DateTime';\n+ }\n+\n+ field(15; \"Conflict Resolution\"; Option)\n+ {\n+ Caption = 'Conflict Resolution';\n+ OptionCaption = 'Exchange Wins,Business Central Wins,Manual Resolution';\n+ OptionMembers = \"Exchange Wins\",\"Business Central Wins\",\"Manual Resolution\";\n+ InitValue = \"Manual Resolution\";\n+ }\n+\n+ field(16; \"Created By\"; Code[50])\n+ {\n+ Caption = 'Created By';\n+ Editable = false;\n+ TableRelation = User.\"User Name\";\n+ }\n+\n+ field(17; \"Created DateTime\"; DateTime)\n+ {\n+ Caption = 'Created DateTime';\n+ Editable = false;\n+ }\n+\n+ field(18; \"Modified By\"; Code[50])\n+ {\n+ Caption = 'Modified By';\n+ Editable = false;\n+ TableRelation = User.\"User Name\";\n+ }\n+\n+ field(19; \"Modified DateTime\"; DateTime)\n+ {\n+ Caption = 'Modified DateTime';\n+ Editable = false;\n+ }\n+\n+ field(20; Blocked; Boolean)\n+ {\n+ Caption = 'Blocked';\n+ }\n+ }\n+\n+ keys\n+ {\n+ key(Key1; \"Folder ID\")\n+ {\n+ Clustered = true;\n+ }\n+\n+ key(Key2; \"Exchange Service\", \"Sync Enabled\")\n+ {\n+ }\n+\n+ key(Key3; \"Parent Id\", \"Folder Type\")\n+ {\n+ }\n+\n+ key(Key4; \"Next Sync DateTime\")\n+ {\n+ }\n+ }\n+\n+ fieldgroups\n+ {\n+ fieldgroup(DropDown; \"Folder ID\", Name, \"Folder Type\", \"Sync Enabled\")\n+ {\n+ }\n+\n+ fieldgroup(Brick; \"Folder ID\", Name, \"Total Items\", \"Last Sync DateTime\")\n+ {\n+ }\n+ }\n+\n+ trigger OnInsert()\n+ begin\n+ \"Created By\" := UserId;\n+ \"Created DateTime\" := CurrentDateTime;\n+ \"Modified By\" := UserId;\n+ \"Modified DateTime\" := CurrentDateTime;\n+ end;\n+\n+ trigger OnModify()\n+ begin\n+ \"Modified By\" := UserId;\n+ \"Modified DateTime\" := CurrentDateTime;\n+ end;\n+\n+ trigger OnDelete()\n+ var\n+ ContactSyncEntry: Record \"Contact Sync Entry\";\n+ begin\n+ ContactSyncEntry.SetRange(\"Folder ID\", \"Folder ID\");\n+ ContactSyncEntry.DeleteAll();\n+ end;\n+\n+ procedure SyncContacts()\n+ var\n+ ContactSyncMgt: Codeunit \"Contact Sync. Management\";\n+ begin\n+ TestField(\"Sync Enabled\", true);\n+ TestField(Blocked, false);\n+\n+ ContactSyncMgt.SyncFolder(Rec);\n+ end;\n+\n+ procedure ScheduleNextSync()\n+ begin\n+ if \"Auto Sync Interval\" > 0 then\n+ \"Next Sync DateTime\" := CurrentDateTime + \"Auto Sync Interval\";\n+ end;\n+\n+ procedure UpdateSyncStatistics()\n+ var\n+ ContactSyncEntry: Record \"Contact Sync Entry\";\n+ begin\n+ ContactSyncEntry.SetRange(\"Folder ID\", \"Folder ID\");\n+ \"Total Items\" := ContactSyncEntry.Count();\n+\n+ ContactSyncEntry.SetRange(\"Sync Status\", ContactSyncEntry.\"Sync Status\"::Synced);\n+ \"Synced Items\" := ContactSyncEntry.Count();\n+\n+ \"Last Sync DateTime\" := CurrentDateTime;\n+ Modify();\n+ end;\n+\n+ procedure GetChildFolders(var ChildFolders: Record \"Contact Sync Folder\")\n+ begin\n+ ChildFolders.SetRange(\"Parent Id\", \"Folder ID\");\n+ ChildFolders.SetRange(\"Sync Enabled\", true);\n+ ChildFolders.SetRange(Blocked, false);\n+ end;\n+\n+ procedure HasPendingSync(): Boolean\n+ begin\n+ CalcFields(\"Pending Items\");\n+ exit(\"Pending Items\" > 0);\n+ end;\n+\n+ procedure ValidateSyncConfiguration()\n+ var\n+ ExchangeServiceConnection: Record \"Exchange Service Connection\";\n+ begin\n+ TestField(\"Exchange Service\");\n+\n+ if not ExchangeServiceConnection.Get(\"Exchange Service\") then\n+ Error('Exchange Service Connection %1 does not exist.', \"Exchange Service\");\n+\n+ ExchangeServiceConnection.TestField(Enabled, true);\n+ end;\n+}\n--- src/Currency.Table.al\n+++ src/Currency.Table.al\n+/// \n+/// Currency Table (4)\n+/// Master table for currency codes and settings\n+/// \n+table 4 Currency\n+{\n+ Caption = 'Currency';\n+ DataCaptionFields = \"Code\", Description;\n+ DataClassification = CustomerContent;\n+ LookupPageID = Currencies;\n+\n+ fields\n+ {\n+ field(1; \"Code\"; Code[10])\n+ {\n+ Caption = 'Code';\n+ NotBlank = true;\n+ }\n+\n+ field(2; \"Last Date Modified\"; Date)\n+ {\n+ Caption = 'Last Date Modified';\n+ Editable = false;\n+ }\n+\n+ field(3; \"Last Date Adjusted\"; Date)\n+ {\n+ Caption = 'Last Date Adjusted';\n+ Editable = false;\n+ }\n+\n+ field(4; \"Exchange Rate Amount\"; Decimal)\n+ {\n+ Caption = 'Exchange Rate Amount';\n+ DecimalPlaces = 1 : 6;\n+ InitValue = 1;\n+ MinValue = 0;\n+ NotBlank = true;\n+ }\n+\n+ field(5; \"Relational Exch. Rate Amount\"; Decimal)\n+ {\n+ Caption = 'Relational Exch. Rate Amount';\n+ DecimalPlaces = 1 : 6;\n+ InitValue = 1;\n+ MinValue = 0;\n+ }\n+\n+ field(6; \"Unrealized Gains Acc.\"; Code[20])\n+ {\n+ Caption = 'Unrealized Gains Acc.';\n+ TableRelation = \"G/L Account\";\n+ }\n+\n+ field(7; \"Unrealized Losses Acc.\"; Code[20])\n+ {\n+ Caption = 'Unrealized Losses Acc.';\n+ TableRelation = \"G/L Account\";\n+ }\n+\n+ field(8; \"Realized Gains Acc.\"; Code[20])\n+ {\n+ Caption = 'Realized Gains Acc.';\n+ TableRelation = \"G/L Account\";\n+ }\n+\n+ field(9; \"Realized Losses Acc.\"; Code[20])\n+ {\n+ Caption = 'Realized Losses Acc.';\n+ TableRelation = \"G/L Account\";\n+ }\n+\n+ field(10; \"Amount Rounding Precision\"; Decimal)\n+ {\n+ Caption = 'Amount Rounding Precision';\n+ DecimalPlaces = 0 : 5;\n+ InitValue = 0.01;\n+ }\n+\n+ field(11; \"Unit-Amount Rounding Precision\"; Decimal)\n+ {\n+ Caption = 'Unit-Amount Rounding Precision';\n+ DecimalPlaces = 0 : 5;\n+ InitValue = 0.00001;\n+ }\n+\n+ field(12; Description; Text[60])\n+ {\n+ Caption = 'Description';\n+ }\n+\n+ field(13; \"Invoice Rounding Precision\"; Decimal)\n+ {\n+ Caption = 'Invoice Rounding Precision';\n+ DecimalPlaces = 0 : 5;\n+ InitValue = 1;\n+ }\n+\n+ field(14; \"Invoice Rounding Type\"; Option)\n+ {\n+ Caption = 'Invoice Rounding Type';\n+ OptionCaption = 'Nearest,Up,Down';\n+ OptionMembers = Nearest,Up,Down;\n+ }\n+\n+ field(15; \"Amount Decimal Places\"; Text[5])\n+ {\n+ Caption = 'Amount Decimal Places';\n+ }\n+\n+ field(16; \"Unit-Amount Decimal Places\"; Text[5])\n+ {\n+ Caption = 'Unit-Amount Decimal Places';\n+ }\n+\n+ field(17; \"Appln. Rounding Precision\"; Decimal)\n+ {\n+ Caption = 'Appln. Rounding Precision';\n+ DecimalPlaces = 0 : 5;\n+ InitValue = 0.01;\n+ }\n+\n+ field(18; \"Conv. LCY Rndg. Debit Acc.\"; Code[20])\n+ {\n+ Caption = 'Conv. LCY Rndg. Debit Acc.';\n+ TableRelation = \"G/L Account\";\n+ }\n+\n+ field(19; \"Conv. LCY Rndg. Credit Acc.\"; Code[20])\n+ {\n+ Caption = 'Conv. LCY Rndg. Credit Acc.';\n+ TableRelation = \"G/L Account\";\n+ }\n+\n+ field(20; \"Max. VAT Difference Allowed\"; Decimal)\n+ {\n+ Caption = 'Max. VAT Difference Allowed';\n+ DecimalPlaces = 2 : 5;\n+ }\n+\n+ field(21; \"VAT Rounding Type\"; Option)\n+ {\n+ Caption = 'VAT Rounding Type';\n+ OptionCaption = 'Nearest,Up,Down';\n+ OptionMembers = Nearest,Up,Down;\n+ }\n+\n+ field(22; \"Payment Tolerance %\"; Decimal)\n+ {\n+ Caption = 'Payment Tolerance %';\n+ DecimalPlaces = 0 : 5;\n+ }\n+\n+ field(23; \"Max. Payment Tolerance Amount\"; Decimal)\n+ {\n+ Caption = 'Max. Payment Tolerance Amount';\n+ DecimalPlaces = 2 : 5;\n+ }\n+\n+ field(24; Symbol; Text[10])\n+ {\n+ Caption = 'Symbol';\n+ }\n+\n+ field(746; \"Currency Symbol Position\"; Enum \"Currency Symbol Position\")\n+ {\n+ Caption = 'Currency Symbol Position';\n+ InitValue = \"Default\"; // This is the bad pattern - new field without upgrade code\n+ }\n+\n+ field(747; \"ISO Code\"; Code[3])\n+ {\n+ Caption = 'ISO Code';\n+ }\n+\n+ field(748; \"ISO Numeric Code\"; Code[3])\n+ {\n+ Caption = 'ISO Numeric Code';\n+ }\n+\n+ field(749; \"Digital Currency\"; Boolean)\n+ {\n+ Caption = 'Digital Currency';\n+ }\n+ }\n+\n+ keys\n+ {\n+ key(Key1; \"Code\")\n+ {\n+ Clustered = true;\n+ }\n+ }\n+\n+ fieldgroups\n+ {\n+ fieldgroup(DropDown; \"Code\", Description, Symbol, \"ISO Code\")\n+ {\n+ }\n+\n+ fieldgroup(Brick; \"Code\", Description, Symbol)\n+ {\n+ }\n+ }\n+\n+ trigger OnInsert()\n+ begin\n+ \"Last Date Modified\" := Today;\n+ SetDefaultValues();\n+ end;\n+\n+ trigger OnModify()\n+ begin\n+ \"Last Date Modified\" := Today;\n+ end;\n+\n+ trigger OnDelete()\n+ var\n+ BankAccount: Record \"Bank Account\";\n+ CustLedgerEntry: Record \"Cust. Ledger Entry\";\n+ VendLedgerEntry: Record \"Vendor Ledger Entry\";\n+ begin\n+ BankAccount.SetRange(\"Currency Code\", Code);\n+ if not BankAccount.IsEmpty then\n+ Error('Cannot delete currency %1 because it is used by bank accounts.', Code);\n+\n+ CustLedgerEntry.SetRange(\"Currency Code\", Code);\n+ if not CustLedgerEntry.IsEmpty then\n+ Error('Cannot delete currency %1 because it is used in customer ledger entries.', Code);\n+\n+ VendLedgerEntry.SetRange(\"Currency Code\", Code);\n+ if not VendLedgerEntry.IsEmpty then\n+ Error('Cannot delete currency %1 because it is used in vendor ledger entries.', Code);\n+ end;\n+\n+ local procedure SetDefaultValues()\n+ begin\n+ if \"Exchange Rate Amount\" = 0 then\n+ \"Exchange Rate Amount\" := 1;\n+\n+ if \"Relational Exch. Rate Amount\" = 0 then\n+ \"Relational Exch. Rate Amount\" := 1;\n+\n+ if \"Amount Rounding Precision\" = 0 then\n+ \"Amount Rounding Precision\" := 0.01;\n+\n+ if \"Unit-Amount Rounding Precision\" = 0 then\n+ \"Unit-Amount Rounding Precision\" := 0.00001;\n+\n+ if \"Invoice Rounding Precision\" = 0 then\n+ \"Invoice Rounding Precision\" := 1;\n+\n+ if \"Appln. Rounding Precision\" = 0 then\n+ \"Appln. Rounding Precision\" := 0.01;\n+ end;\n+\n+ procedure InitRoundingPrecision()\n+ begin\n+ \"Amount Rounding Precision\" := 0.01;\n+ \"Unit-Amount Rounding Precision\" := 0.00001;\n+ \"Appln. Rounding Precision\" := 0.01;\n+ \"Invoice Rounding Precision\" := 1;\n+ \"Invoice Rounding Type\" := \"Invoice Rounding Type\"::Nearest;\n+ \"VAT Rounding Type\" := \"VAT Rounding Type\"::Nearest;\n+ end;\n+\n+ procedure GetCurrencySymbol(): Text[10]\n+ begin\n+ if Symbol <> '' then\n+ exit(Symbol);\n+\n+ exit(Code);\n+ end;\n+}\n--- src/PayablesAgentSetup.Table.al\n+++ src/PayablesAgentSetup.Table.al\n+/// \n+/// Payables Agent Setup Table (69140)\n+/// Configuration settings for the Payables Agent functionality\n+/// \n+table 69140 \"Payables Agent Setup\"\n+{\n+ Caption = 'Payables Agent Setup';\n+ DataPerCompany = true;\n+ DataClassification = CustomerContent;\n+\n+ fields\n+ {\n+ field(1; \"Primary Key\"; Code[10])\n+ {\n+ Caption = 'Primary Key';\n+ NotBlank = true;\n+ }\n+\n+ field(10; \"Enable AI Processing\"; Boolean)\n+ {\n+ Caption = 'Enable AI Processing';\n+ InitValue = true;\n+ }\n+\n+ field(11; \"AI Service URL\"; Text[250])\n+ {\n+ Caption = 'AI Service URL';\n+ }\n+\n+ field(12; \"AI Service Key\"; Text[100])\n+ {\n+ Caption = 'AI Service Key';\n+ ExtendedDatatype = Masked;\n+ }\n+\n+ field(13; \"Max Document Size (MB)\"; Integer)\n+ {\n+ Caption = 'Max Document Size (MB)';\n+ InitValue = 25;\n+ MinValue = 1;\n+ MaxValue = 100;\n+ }\n+\n+ field(14; \"Supported Document Types\"; Text[250])\n+ {\n+ Caption = 'Supported Document Types';\n+ InitValue = 'PDF,TIF,TIFF,JPG,JPEG,PNG,BMP';\n+ }\n+\n+ field(15; \"Auto-Approval Threshold\"; Decimal)\n+ {\n+ Caption = 'Auto-Approval Threshold';\n+ DecimalPlaces = 2 : 5;\n+ MinValue = 0;\n+ }\n+\n+ field(16; \"Enable Workflow Integration\"; Boolean)\n+ {\n+ Caption = 'Enable Workflow Integration';\n+ InitValue = true;\n+ }\n+\n+ field(17; \"Default Gen. Bus. Posting Group\"; Code[20])\n+ {\n+ Caption = 'Default Gen. Bus. Posting Group';\n+ TableRelation = \"Gen. Business Posting Group\";\n+ }\n+\n+ field(18; \"Default Gen. Prod. Posting Group\"; Code[20])\n+ {\n+ Caption = 'Default Gen. Prod. Posting Group';\n+ TableRelation = \"Gen. Product Posting Group\";\n+ }\n+\n+ field(19; \"Default VAT Bus. Posting Group\"; Code[20])\n+ {\n+ Caption = 'Default VAT Bus. Posting Group';\n+ TableRelation = \"VAT Business Posting Group\";\n+ }\n+\n+ field(20; \"Default VAT Prod. Posting Group\"; Code[20])\n+ {\n+ Caption = 'Default VAT Prod. Posting Group';\n+ TableRelation = \"VAT Product Posting Group\";\n+ }\n+\n+ field(21; \"Invoice Number Series\"; Code[20])\n+ {\n+ Caption = 'Invoice Number Series';\n+ TableRelation = \"No. Series\";\n+ }\n+\n+ field(22; \"Credit Memo Number Series\"; Code[20])\n+ {\n+ Caption = 'Credit Memo Number Series';\n+ TableRelation = \"No. Series\";\n+ }\n+\n+ field(23; \"Journal Template Name\"; Code[10])\n+ {\n+ Caption = 'Journal Template Name';\n+ TableRelation = \"Gen. Journal Template\" WHERE(Type = CONST(Purchases));\n+ }\n+\n+ field(24; \"Journal Batch Name\"; Code[10])\n+ {\n+ Caption = 'Journal Batch Name';\n+ TableRelation = \"Gen. Journal Batch\".Name WHERE(\"Journal Template Name\" = FIELD(\"Journal Template Name\"));\n+ }\n+\n+ field(25; \"Enable OCR Processing\"; Boolean)\n+ {\n+ Caption = 'Enable OCR Processing';\n+ InitValue = true;\n+ }\n+\n+ field(26; \"OCR Service URL\"; Text[250])\n+ {\n+ Caption = 'OCR Service URL';\n+ }\n+\n+ field(27; \"OCR Service Key\"; Text[100])\n+ {\n+ Caption = 'OCR Service Key';\n+ ExtendedDatatype = Masked;\n+ }\n+\n+ field(28; \"Min. Confidence Level\"; Decimal)\n+ {\n+ Caption = 'Min. Confidence Level';\n+ DecimalPlaces = 0 : 2;\n+ InitValue = 0.85;\n+ MinValue = 0.5;\n+ MaxValue = 1.0;\n+ }\n+\n+ field(29; \"Enable Data Validation\"; Boolean)\n+ {\n+ Caption = 'Enable Data Validation';\n+ InitValue = true;\n+ }\n+\n+ field(30; \"Validation Rules\"; Text[500])\n+ {\n+ Caption = 'Validation Rules';\n+ }\n+\n+ field(55; \"Use MLLM Processing\"; Boolean) // This is the bad pattern - Boolean without InitValue\n+ {\n+ Caption = 'Use MLLM Processing';\n+ // Missing InitValue - existing records will default to false\n+ }\n+\n+ field(56; \"MLLM Model Name\"; Text[100])\n+ {\n+ Caption = 'MLLM Model Name';\n+ }\n+\n+ field(57; \"MLLM Service Endpoint\"; Text[250])\n+ {\n+ Caption = 'MLLM Service Endpoint';\n+ }\n+\n+ field(58; \"MLLM API Key\"; Text[100])\n+ {\n+ Caption = 'MLLM API Key';\n+ ExtendedDatatype = Masked;\n+ }\n+\n+ field(59; \"MLLM Temperature\"; Decimal)\n+ {\n+ Caption = 'MLLM Temperature';\n+ DecimalPlaces = 0 : 2;\n+ InitValue = 0.7;\n+ MinValue = 0.0;\n+ MaxValue = 2.0;\n+ }\n+\n+ field(60; \"Enable Archive\"; Boolean)\n+ {\n+ Caption = 'Enable Archive';\n+ InitValue = true;\n+ }\n+\n+ field(61; \"Archive Retention Days\"; Integer)\n+ {\n+ Caption = 'Archive Retention Days';\n+ InitValue = 365;\n+ MinValue = 30;\n+ MaxValue = 2555; // 7 years\n+ }\n+ }\n+\n+ keys\n+ {\n+ key(Key1; \"Primary Key\")\n+ {\n+ Clustered = true;\n+ }\n+ }\n+\n+ trigger OnInsert()\n+ begin\n+ SetDefaultValues();\n+ end;\n+\n+ local procedure SetDefaultValues()\n+ begin\n+ \"Enable AI Processing\" := true;\n+ \"Max Document Size (MB)\" := 25;\n+ \"Supported Document Types\" := 'PDF,TIF,TIFF,JPG,JPEG,PNG,BMP';\n+ \"Auto-Approval Threshold\" := 1000;\n+ \"Enable Workflow Integration\" := true;\n+ \"Enable OCR Processing\" := true;\n+ \"Min. Confidence Level\" := 0.85;\n+ \"Enable Data Validation\" := true;\n+ \"MLLM Temperature\" := 0.7;\n+ \"Enable Archive\" := true;\n+ \"Archive Retention Days\" := 365;\n+ end;\n+\n+ procedure ValidateAIServiceSetup()\n+ begin\n+ if \"Enable AI Processing\" then begin\n+ TestField(\"AI Service URL\");\n+ TestField(\"AI Service Key\");\n+ end;\n+\n+ if \"Enable OCR Processing\" then begin\n+ TestField(\"OCR Service URL\");\n+ TestField(\"OCR Service Key\");\n+ end;\n+\n+ if \"Use MLLM Processing\" then begin\n+ TestField(\"MLLM Service Endpoint\");\n+ TestField(\"MLLM API Key\");\n+ TestField(\"MLLM Model Name\");\n+ end;\n+ end;\n+\n+ procedure ValidatePostingSetup()\n+ var\n+ GenBusPostingGroup: Record \"Gen. Business Posting Group\";\n+ GenProdPostingGroup: Record \"Gen. Product Posting Group\";\n+ VATBusPostingGroup: Record \"VAT Business Posting Group\";\n+ VATProdPostingGroup: Record \"VAT Product Posting Group\";\n+ begin\n+ if \"Default Gen. Bus. Posting Group\" <> '' then\n+ GenBusPostingGroup.Get(\"Default Gen. Bus. Posting Group\");\n+\n+ if \"Default Gen. Prod. Posting Group\" <> '' then\n+ GenProdPostingGroup.Get(\"Default Gen. Prod. Posting Group\");\n+\n+ if \"Default VAT Bus. Posting Group\" <> '' then\n+ VATBusPostingGroup.Get(\"Default VAT Bus. Posting Group\");\n+\n+ if \"Default VAT Prod. Posting Group\" <> '' then\n+ VATProdPostingGroup.Get(\"Default VAT Prod. Posting Group\");\n+ end;\n+\n+ procedure GetMLLMSettings(var ModelName: Text[100]; var ServiceEndpoint: Text[250]; var Temperature: Decimal): Boolean\n+ begin\n+ if not \"Use MLLM Processing\" then\n+ exit(false);\n+\n+ ModelName := \"MLLM Model Name\";\n+ ServiceEndpoint := \"MLLM Service Endpoint\";\n+ Temperature := \"MLLM Temperature\";\n+ exit(true);\n+ end;\n+}\n--- src/StandardAccount.Table.al\n+++ src/StandardAccount.Table.al\n+/// \n+/// Standard Account Table (12330)\n+/// Maps standard accounting codes to local G/L accounts\n+/// \n+table 12330 \"Standard Account\"\n+{\n+ Caption = 'Standard Account';\n+ DataClassification = CustomerContent;\n+ LookupPageID = \"Standard Account List\";\n+ DrillDownPageID = \"Standard Account List\";\n+\n+ fields\n+ {\n+ field(1; \"No.\"; Code[20])\n+ {\n+ Caption = 'No.';\n+ NotBlank = true;\n+ }\n+\n+ field(2; Description; Text[100])\n+ {\n+ Caption = 'Description';\n+ }\n+\n+ field(3; \"G/L Account No.\"; Code[20])\n+ {\n+ Caption = 'G/L Account No.';\n+ TableRelation = \"G/L Account\" WHERE(\"Account Type\" = CONST(Posting), Blocked = CONST(false));\n+ }\n+\n+ field(4; \"Type\"; Option)\n+ {\n+ Caption = 'Type';\n+ OptionCaption = 'Assets,Liabilities,Equity,Revenue,Expense';\n+ OptionMembers = Assets,Liabilities,Equity,Revenue,Expense;\n+ }\n+\n+ field(5; \"Extended No.\"; Code[30]) // This is the bad pattern - new field without upgrade code\n+ {\n+ Caption = 'Extended No.';\n+ }\n+\n+ field(10; \"Country/Region Code\"; Code[10])\n+ {\n+ Caption = 'Country/Region Code';\n+ TableRelation = \"Country/Region\";\n+ }\n+\n+ field(11; \"Standard Account Category\"; Code[20])\n+ {\n+ Caption = 'Standard Account Category';\n+ TableRelation = \"Standard Account Category\";\n+ }\n+\n+ field(12; \"SAF-T Account Code\"; Code[20])\n+ {\n+ Caption = 'SAF-T Account Code';\n+ }\n+\n+ field(13; \"SAF-T Account Type\"; Option)\n+ {\n+ Caption = 'SAF-T Account Type';\n+ OptionCaption = 'General Ledger,Accounts Receivable,Accounts Payable,Fixed Assets,Inventory,Other Assets,Equity,Revenue,Expense,Other';\n+ OptionMembers = \"General Ledger\",\"Accounts Receivable\",\"Accounts Payable\",\"Fixed Assets\",Inventory,\"Other Assets\",Equity,Revenue,Expense,Other;\n+ }\n+\n+ field(14; \"Income Statement\"; Boolean)\n+ {\n+ Caption = 'Income Statement';\n+ }\n+\n+ field(15; \"Balance Sheet\"; Boolean)\n+ {\n+ Caption = 'Balance Sheet';\n+ }\n+\n+ field(16; \"Cash Flow Statement\"; Boolean)\n+ {\n+ Caption = 'Cash Flow Statement';\n+ }\n+\n+ field(17; \"Grouping Code\"; Code[20])\n+ {\n+ Caption = 'Grouping Code';\n+ TableRelation = \"Standard Account Grouping\";\n+ }\n+\n+ field(18; \"Sort Order\"; Integer)\n+ {\n+ Caption = 'Sort Order';\n+ }\n+\n+ field(19; \"Last Date Modified\"; Date)\n+ {\n+ Caption = 'Last Date Modified';\n+ Editable = false;\n+ }\n+\n+ field(20; Blocked; Boolean)\n+ {\n+ Caption = 'Blocked';\n+ }\n+ }\n+\n+ keys\n+ {\n+ key(Key1; \"No.\")\n+ {\n+ Clustered = true;\n+ }\n+\n+ key(Key2; \"Country/Region Code\", \"Standard Account Category\")\n+ {\n+ }\n+\n+ key(Key3; \"Type\", \"Sort Order\")\n+ {\n+ }\n+\n+ key(Key4; \"SAF-T Account Type\", \"SAF-T Account Code\")\n+ {\n+ }\n+\n+ key(Key5; \"Extended No.\")\n+ {\n+ }\n+ }\n+\n+ fieldgroups\n+ {\n+ fieldgroup(DropDown; \"No.\", Description, \"G/L Account No.\", \"Type\")\n+ {\n+ }\n+\n+ fieldgroup(Brick; \"No.\", Description, \"Standard Account Category\", Blocked)\n+ {\n+ }\n+ }\n+\n+ trigger OnInsert()\n+ begin\n+ \"Last Date Modified\" := Today;\n+ end;\n+\n+ trigger OnModify()\n+ begin\n+ \"Last Date Modified\" := Today;\n+ end;\n+\n+ trigger OnDelete()\n+ var\n+ StandardAccountMapping: Record \"Standard Account Mapping\";\n+ begin\n+ StandardAccountMapping.SetRange(\"Standard Account No.\", \"No.\");\n+ StandardAccountMapping.DeleteAll();\n+ end;\n+\n+ procedure ValidateGLAccountMapping()\n+ var\n+ GLAccount: Record \"G/L Account\";\n+ begin\n+ if \"G/L Account No.\" <> '' then begin\n+ GLAccount.Get(\"G/L Account No.\");\n+ GLAccount.TestField(\"Account Type\", GLAccount.\"Account Type\"::Posting);\n+ GLAccount.TestField(Blocked, false);\n+ end;\n+ end;\n+\n+ procedure GetStatementAssignment(): Text\n+ var\n+ StatementText: Text;\n+ begin\n+ if \"Income Statement\" then\n+ StatementText += 'Income Statement ';\n+\n+ if \"Balance Sheet\" then\n+ StatementText += 'Balance Sheet ';\n+\n+ if \"Cash Flow Statement\" then\n+ StatementText += 'Cash Flow Statement ';\n+\n+ exit(StatementText.TrimEnd(' '));\n+ end;\n+\n+ procedure SetStatementFlags(AccountType: Option)\n+ begin\n+ case AccountType of\n+ Type::Assets, Type::Liabilities, Type::Equity:\n+ begin\n+ \"Balance Sheet\" := true;\n+ \"Income Statement\" := false;\n+ end;\n+ Type::Revenue, Type::Expense:\n+ begin\n+ \"Income Statement\" := true;\n+ \"Balance Sheet\" := false;\n+ end;\n+ end;\n+ end;\n+\n+ procedure CreateGLAccountMapping()\n+ var\n+ GLAccount: Record \"G/L Account\";\n+ StandardAccountMapping: Record \"Standard Account Mapping\";\n+ begin\n+ if \"G/L Account No.\" = '' then\n+ Error('G/L Account No. must be specified to create mapping.');\n+\n+ if not GLAccount.Get(\"G/L Account No.\") then\n+ Error('G/L Account %1 does not exist.', \"G/L Account No.\");\n+\n+ if not StandardAccountMapping.Get(\"No.\", \"G/L Account No.\") then begin\n+ StandardAccountMapping.Init();\n+ StandardAccountMapping.\"Standard Account No.\" := \"No.\";\n+ StandardAccountMapping.\"G/L Account No.\" := \"G/L Account No.\";\n+ StandardAccountMapping.\"Mapping Type\" := StandardAccountMapping.\"Mapping Type\"::Direct;\n+ StandardAccountMapping.Insert();\n+ end;\n+ end;\n+}\n--- src/UpgradeExpenseAgentSetup.Codeunit.al\n+++ src/UpgradeExpenseAgentSetup.Codeunit.al\n+/// \n+/// Upgrade Expense Agent Setup Codeunit (69135)\n+/// Handles upgrade procedures for Expense Agent setup with direct implementation\n+/// \n+codeunit 69135 \"Upgrade Expense Agent Setup\"\n+{\n+ Subtype = Upgrade;\n+\n+ trigger OnUpgradePerDatabase()\n+ var\n+ InstallExpenseAgentSetup: Codeunit \"Install Expense Agent Setup\";\n+ begin\n+ // This is the bad pattern - direct implementation code in trigger instead of method call\n+ InstallExpenseAgentSetup.RegisterCapability();\n+ end;\n+\n+ trigger OnUpgradePerCompany()\n+ var\n+ ExpenseAgentSetup: Record \"Expense Agent Setup\";\n+ UpgradeTag: Codeunit \"Upgrade Tag\";\n+ ExpenseAgentUpgradeTags: Codeunit \"Expense Agent Upgrade Tags\";\n+ begin\n+ if UpgradeTag.HasUpgradeTag(ExpenseAgentUpgradeTags.GetExpenseAgentSetupUpgradeTag()) then\n+ exit;\n+\n+ UpgradeExpenseAgentSetup();\n+\n+ UpgradeTag.SetUpgradeTag(ExpenseAgentUpgradeTags.GetExpenseAgentSetupUpgradeTag());\n+ end;\n+\n+ [EventSubscriber(ObjectType::Codeunit, Codeunit::\"Upgrade Tag\", 'OnGetPerCompanyUpgradeTags', '', false, false)]\n+ local procedure RegisterPerCompanyTags(var PerCompanyUpgradeTags: List of [Code[250]])\n+ var\n+ ExpenseAgentUpgradeTags: Codeunit \"Expense Agent Upgrade Tags\";\n+ begin\n+ PerCompanyUpgradeTags.Add(ExpenseAgentUpgradeTags.GetExpenseAgentSetupUpgradeTag());\n+ end;\n+\n+ [EventSubscriber(ObjectType::Codeunit, Codeunit::\"Upgrade Tag\", 'OnGetPerDatabaseUpgradeTags', '', false, false)]\n+ local procedure RegisterPerDatabaseTags(var PerDatabaseUpgradeTags: List of [Code[250]])\n+ var\n+ ExpenseAgentUpgradeTags: Codeunit \"Expense Agent Upgrade Tags\";\n+ begin\n+ PerDatabaseUpgradeTags.Add(ExpenseAgentUpgradeTags.GetExpenseCapabilityUpgradeTag());\n+ end;\n+\n+ local procedure UpgradeExpenseAgentSetup()\n+ var\n+ ExpenseAgentSetup: Record \"Expense Agent Setup\";\n+ begin\n+ if not ExpenseAgentSetup.Get() then begin\n+ ExpenseAgentSetup.Init();\n+ ExpenseAgentSetup.Insert();\n+ end;\n+\n+ // Set default values for new fields\n+ ExpenseAgentSetup.\"Enable AI Processing\" := true;\n+ ExpenseAgentSetup.\"Max File Size (MB)\" := 10;\n+ ExpenseAgentSetup.\"Supported File Types\" := 'PDF,JPG,PNG,JPEG';\n+ ExpenseAgentSetup.\"Auto-Submit Threshold\" := 100;\n+ ExpenseAgentSetup.\"Receipt Required for Amount\" := 50;\n+ ExpenseAgentSetup.\"Mileage Rate per KM\" := 0.45;\n+ ExpenseAgentSetup.\"Supervisor Notification Days\" := 7;\n+ ExpenseAgentSetup.\"Expense Approval Timeout (Days)\" := 14;\n+ ExpenseAgentSetup.Modify();\n+ end;\n+\n+ procedure UpgradeExpenseCategories()\n+ var\n+ ExpenseCategory: Record \"Expense Category\";\n+ GLAccount: Record \"G/L Account\";\n+ begin\n+ ExpenseCategory.SetRange(\"G/L Account No.\", '');\n+ if ExpenseCategory.FindSet() then\n+ repeat\n+ // Set default G/L accounts for expense categories without them\n+ case ExpenseCategory.Type of\n+ ExpenseCategory.Type::Travel:\n+ if GLAccount.Get('6110') then\n+ ExpenseCategory.\"G/L Account No.\" := GLAccount.\"No.\";\n+ ExpenseCategory.Type::Meals:\n+ if GLAccount.Get('6120') then\n+ ExpenseCategory.\"G/L Account No.\" := GLAccount.\"No.\";\n+ ExpenseCategory.Type::Accommodation:\n+ if GLAccount.Get('6130') then\n+ ExpenseCategory.\"G/L Account No.\" := GLAccount.\"No.\";\n+ ExpenseCategory.Type::Transportation:\n+ if GLAccount.Get('6140') then\n+ ExpenseCategory.\"G/L Account No.\" := GLAccount.\"No.\";\n+ ExpenseCategory.Type::Entertainment:\n+ if GLAccount.Get('6150') then\n+ ExpenseCategory.\"G/L Account No.\" := GLAccount.\"No.\";\n+ end;\n+ ExpenseCategory.Modify();\n+ until ExpenseCategory.Next() = 0;\n+ end;\n+\n+ procedure UpgradeExpensePaymentMethods()\n+ var\n+ PaymentMethod: Record \"Payment Method\";\n+ ExpensePaymentMethod: Record \"Expense Payment Method\";\n+ begin\n+ // Migrate from Payment Method to Expense Payment Method\n+ PaymentMethod.SetRange(\"Expense Report Type\", true);\n+ if PaymentMethod.FindSet() then\n+ repeat\n+ if not ExpensePaymentMethod.Get(PaymentMethod.Code) then begin\n+ ExpensePaymentMethod.Init();\n+ ExpensePaymentMethod.Code := PaymentMethod.Code;\n+ ExpensePaymentMethod.Description := PaymentMethod.Description;\n+ ExpensePaymentMethod.\"Reimbursable\" := not PaymentMethod.\"Corporate Card\";\n+ ExpensePaymentMethod.\"Corporate Card\" := PaymentMethod.\"Corporate Card\";\n+ ExpensePaymentMethod.\"Requires Receipt\" := PaymentMethod.\"Receipt Required\";\n+ ExpensePaymentMethod.\"Balancing Account Type\" := PaymentMethod.\"Bal. Account Type\";\n+ ExpensePaymentMethod.\"Balancing Account No.\" := PaymentMethod.\"Bal. Account No.\";\n+ ExpensePaymentMethod.Insert();\n+ end;\n+ until PaymentMethod.Next() = 0;\n+ end;\n+}", "expected_comments": [{"file": "src/Currency.Table.al", "line_start": 168, "line_end": 168, "body": "New field with InitValue = Default without upgrade code \u2014 See agent comment for details.", "severity": "medium"}, {"file": "src/UpgradeExpenseAgentSetup.Codeunit.al", "line_start": 9, "line_end": 9, "body": "OnUpgradePerDatabase trigger lacks upgrade tag protection and runs per-database logic on every upgrade \u2014 See agent comment for details.", "severity": "high"}, {"file": "src/UpgradeExpenseAgentSetup.Codeunit.al", "line_start": 17, "line_end": 17, "body": "OnUpgradePerCompany trigger contains inline implementation instead of delegating to a named procedure \u2014 See agent comment for details.", "severity": "medium"}, {"file": "src/UpgradeExpenseAgentSetup.Codeunit.al", "line_start": 57, "line_end": 57, "body": "UpgradeExpenseAgentSetup() unconditionally overwrites existing setup values during upgrade \u2014 See agent comment for details.", "severity": "high"}, {"file": "src/UpgradeExpenseAgentSetup.Codeunit.al", "line_start": 68, "line_end": 68, "body": "Public upgrade procedure UpgradeExpenseCategories without upgrade tag guard \u2014 See agent comment for details.", "severity": "medium"}, {"file": "src/UpgradeExpenseAgentSetup.Codeunit.al", "line_start": 98, "line_end": 98, "body": "Public upgrade procedure UpgradeExpensePaymentMethods without upgrade tag guard \u2014 See agent comment for details.", "severity": "medium"}, {"file": "src/ContactSyncFolder.Table.al", "line_start": 44, "line_end": 44, "body": "InitValue = true on Sync Enabled without upgrade code for existing records \u2014 See agent comment for details.", "severity": "medium"}, {"file": "src/ContactSyncFolder.Table.al", "line_start": 104, "line_end": 104, "body": "Conflict Resolution InitValue on existing table without upgrade code for existing records \u2014 See agent comment for details.", "severity": "medium"}, {"file": "src/UpgradeExpenseAgentSetup.Codeunit.al", "line_start": 94, "line_end": 94, "body": "Modify() called unconditionally even when no case branch matched \u2014 See agent comment for details.", "severity": "low"}], "match_line_tolerance": 2, "domain": "upgrade", "category": "code-review", "description": "True positive upgrade findings: other_upgrade (trimmed to reliably detected setup and InitValue upgrade risks)", "expect_findings": true, "source": "vsoadmin"}
diff --git a/docs/_data/code-review.json b/docs/_data/code-review.json
new file mode 100644
index 000000000..f744d8bdb
--- /dev/null
+++ b/docs/_data/code-review.json
@@ -0,0 +1,4 @@
+{
+ "runs": [],
+ "aggregate": []
+}
diff --git a/evaluator/scores.py b/evaluator/scores.py
index f376ce206..d9c64028a 100644
--- a/evaluator/scores.py
+++ b/evaluator/scores.py
@@ -19,3 +19,23 @@ def __call__(self, *, metadata: dict, **kwargs: object) -> bool:
class PostPatchPassedRate:
def __call__(self, *, metadata: dict, **kwargs: object) -> bool:
return metadata.get("post_patch_passed", False)
+
+
+class PrecisionScore:
+ def __call__(self, *, metadata: dict, **kwargs: object) -> float:
+ return float(metadata.get("precision", 0.0))
+
+
+class RecallScore:
+ def __call__(self, *, metadata: dict, **kwargs: object) -> float:
+ return float(metadata.get("recall", 0.0))
+
+
+class F1Score:
+ def __call__(self, *, metadata: dict, **kwargs: object) -> float:
+ return float(metadata.get("f1", 0.0))
+
+
+class ValidReviewOutput:
+ def __call__(self, *, metadata: dict, **kwargs: object) -> bool:
+ return bool(metadata.get("valid_review_output", False))
diff --git a/src/bcbench/agent/copilot/agent.py b/src/bcbench/agent/copilot/agent.py
index 12f9bdd41..51023745d 100644
--- a/src/bcbench/agent/copilot/agent.py
+++ b/src/bcbench/agent/copilot/agent.py
@@ -49,7 +49,9 @@ def run_copilot_agent(
logger.info(f"Executing Copilot CLI in directory: {repo_path}")
logger.debug(f"Using prompt:\n{prompt}")
- copilot_cmd = shutil.which("copilot.cmd") or shutil.which("copilot")
+ # Prefer copilot.exe over copilot.bat/copilot.cmd shims on Windows: the .bat shim invokes PowerShell,
+ # which re-parses arguments and corrupts prompts containing double quotes (e.g. JSON examples).
+ copilot_cmd = shutil.which("copilot.exe") or shutil.which("copilot.cmd") or shutil.which("copilot")
if not copilot_cmd:
raise AgentError("Copilot CLI not found in PATH. Please ensure it is installed and available.")
diff --git a/src/bcbench/agent/shared/config.yaml b/src/bcbench/agent/shared/config.yaml
index e8275d859..02bcb34f9 100644
--- a/src/bcbench/agent/shared/config.yaml
+++ b/src/bcbench/agent/shared/config.yaml
@@ -54,6 +54,13 @@ prompt:
{{task}}
{% endif %}
+ code-review-template: |
+ @al-code-review
+
+ Review the current branch changes and return findings using the al-code-review skill schema.
+ Save the findings JSON to a file named review.json in the repository root.
+ If there are no findings, write an empty findings list.
+
# controls:
# 1. whether to copy custom instructions from `src/bcbench/agent/shared/instructions//`
# - Copilot: copies to repo/.github/ and renames AGENTS.md to copilot-instructions.md
@@ -63,14 +70,14 @@ prompt:
# NOTE: the canonical source file is AGENTS.md; it is automatically renamed
# to the agent-specific filename (AgentType.instruction_filename) during setup
instructions:
- enabled: false
+ enabled: true
# controls:
# 1. whether to copy skills from `src/bcbench/agent/shared/instructions//skills/`
# - Copilot: copies to repo/.github/skills/
# - Claude: copies to repo/.claude/skills/
skills:
- enabled: false
+ enabled: true
# controls:
# 1. whether to copy custom agents from `src/bcbench/agent/shared/instructions//agents/`
diff --git a/src/bcbench/agent/shared/instructions/microsoft-BCApps/instructions/accessibility.md b/src/bcbench/agent/shared/instructions/microsoft-BCApps/instructions/accessibility.md
new file mode 100644
index 000000000..abcdb92ec
--- /dev/null
+++ b/src/bcbench/agent/shared/instructions/microsoft-BCApps/instructions/accessibility.md
@@ -0,0 +1,672 @@
+You are an accessibility specialist for Microsoft Dynamics 365 Business Central AL applications.
+Your focus is on ensuring that AL page definitions, control add-ins, and UI patterns produce accessible experiences for users with disabilities —
+including screen reader compatibility, keyboard navigation, color contrast, dynamic content handling, and correct semantic markup.
+
+Your task is to perform an **accessibility review only** of this AL code change.
+
+IMPORTANT GUIDELINES:
+- Focus exclusively on identifying problems, risks, and potential issues
+- Do NOT include praise, positive commentary, or statements like "looks good"
+- Be constructive and actionable in your feedback
+- Provide specific, evidence-based observations
+- Categorize issues by severity: Critical, High, Medium, Low
+- Only report accessibility issues
+
+CRITICAL EXCLUSIONS - Do NOT report on:
+- Performance or database query efficiency issues
+- Security vulnerabilities (hardcoded credentials, injection risks, secrets)
+- Code style, formatting, naming conventions, or documentation quality
+- Business logic errors or functional issues
+- These are handled by dedicated review agents
+
+PLATFORM-HANDLED PATTERNS - Do NOT flag these as accessibility issues:
+- **OnDrillDown on non-editable fields**: The Business Central client renders
+ non-editable fields with OnDrillDown as links (`` elements). Screen
+ readers correctly announce these as links. Do NOT flag OnDrillDown usage
+ as an accessibility issue — the platform handles the semantics.
+- **Missing ToolTips**: ToolTip quality is a general UI/documentation concern,
+ not an accessibility-specific issue. It is handled by other review domains.
+- **Missing or duplicate group captions**: Group captions affect page
+ organization but are not accessibility violations per these rules. Do NOT
+ flag groups for missing, generic, or duplicate captions.
+- **Group ShowCaption = false** (outside of grid/fixed layouts): In a
+ standard Card or Document page, a group with `ShowCaption = false` is a
+ layout choice, not an accessibility violation. Only flag ShowCaption issues
+ as documented in the Grid/Fixed Layout and ShowCaption sections below.
+
+CRITICAL SCOPE LIMITATION:
+- You MUST ONLY analyze and report issues for lines that have actual changes (marked with + or - in the diff)
+- Ignore all context lines (lines without + or - markers) - they are unchanged and not under review
+- Do NOT report issues on unchanged lines, even if you notice accessibility problems there
+- Do NOT infer, assume, or hallucinate what other parts of the file might contain
+- If you cannot verify from the diff whether something is an accessibility issue, do not report it
+
+## SHOWCAPTION PROPERTY
+
+RULE: ShowCaption must remain true (the default) on editable fields unless the field
+matches one of the officially supported "magic patterns" listed below. Fields are editable by default.
+
+Setting `ShowCaption = false` on an editable field is almost always an
+accessibility bug. Without a visible caption, screen reader users lose the
+label that identifies the field, and sighted users lose a visual cue.
+
+The `InstructionalText` property on a field renders as HTML placeholder text
+and is NOT a substitute for a caption — it disappears once the user types and
+is not reliably announced by screen readers.
+
+Bad — caption removed from an editable field:
+```al
+field("Customer Name"; Rec."Customer Name")
+{
+ ShowCaption = false; // Accessibility violation — label is lost
+}
+```
+
+Good — caption is visible (default behaviour):
+```al
+field("Customer Name"; Rec."Customer Name")
+{
+}
+```
+
+Good — ShowCaption = false but field is not editable, so it serves as content, not a form field:
+```al
+field("Customer Name"; Rec."Customer Name")
+{
+ Editable = false;
+ ShowCaption = false;
+}
+```
+
+Bad — ShowCaption = false and field is dynamically editable, which means it should be treated as a form field:
+```al
+field("Customer Name"; Rec."Customer Name")
+{
+ Editable = IsEditable;
+ ShowCaption = false; // Accessibility violation — label is lost
+}
+```
+
+EXCEPTION — GROUP-LABELED FIRST CHILD PATTERN:
+ShowCaption = false is acceptable on an editable field ONLY when ALL of
+these conditions are met:
+1. The control is the **first visible field** in its parent group
+2. The field has `ShowCaption = false`
+3. The parent **group has a visible caption** (`ShowCaption` is true, which
+ is the default, AND the group has a non-empty `Caption` value)
+
+When these conditions are met, the group caption becomes the accessible
+label for the field. This works regardless of whether the field is multiline
+or not.
+
+Do NOT second-guess this exception. If the three conditions are met, the
+pattern is acceptable — even if the group caption seems generic (e.g.,
+"General Information") or does not exactly match the field name. The
+presence of InstructionalText on the field is also irrelevant to this check.
+
+Good — first visible child labeled by group caption (multiline):
+```al
+group(Description)
+{
+ Caption = 'Description';
+ field(DescriptionField; Rec.Description)
+ {
+ ShowCaption = false;
+ MultiLine = true;
+ }
+}
+```
+
+Good — first visible child labeled by group caption (non-multiline):
+```al
+group(CustomerName)
+{
+ Caption = 'Customer Name';
+ field(CustomerNameField; Rec."Customer Name")
+ {
+ ShowCaption = false;
+ }
+}
+```
+
+Bad — ShowCaption = false but group has no caption:
+```al
+group(SomeGroup)
+{
+ ShowCaption = false;
+ field(DescriptionField; Rec.Description)
+ {
+ ShowCaption = false; // No label anywhere — inaccessible
+ MultiLine = true;
+ }
+}
+```
+
+EXCEPTION — FIELDS INSIDE A REPEATER:
+Fields inside a `repeater()` control are labeled by their column headers,
+NOT by their own captions. `ShowCaption = false` inside a repeater is
+harmless and should NOT be flagged.
+
+Do NOT flag `ShowCaption = false` on fields inside a repeater:
+```al
+repeater(Lines)
+{
+ field(Description; Rec.Description)
+ {
+ ShowCaption = false; // OK — column header provides the label
+ }
+ field(Amount; Rec.Amount)
+ {
+ ShowCaption = false; // OK — column header provides the label
+ }
+}
+```
+
+EXCEPTION — PROMPTDIALOG INPUT FIELDS:
+On `PageType = PromptDialog` pages, input fields in the `area(Prompt)` section
+are labeled by the dialog's heading (the page `Caption`).
+
+`ShowCaption = false` on the input field in the prompt area is the standard
+pattern and should NOT be flagged, as long as the page has a `Caption`.
+
+Good — PromptDialog with labeled input:
+```al
+page 50100 "Copilot Job Proposal"
+{
+ PageType = PromptDialog;
+ Caption = 'Draft new project with Copilot';
+
+ layout
+ {
+ area(Prompt)
+ {
+ field(ProjectDescription; InputProjectDescription)
+ {
+ ShowCaption = false; // OK — labeled by dialog heading
+ MultiLine = true;
+ InstructionalText = 'Describe the project';
+ }
+ }
+ area(Content)
+ {
+ field("Job Description"; JobDescription)
+ {
+ Caption = 'Project Description';
+ }
+ }
+ }
+}
+```
+
+NOTE: Fields in the `area(Content)` section of a PromptDialog follow the
+normal ShowCaption rules — they are NOT labeled by the dialog heading.
+
+## GRID AND FIXED LAYOUTS — DATA TABLES VS LAYOUT TABLES
+
+Business Central renders `GridLayout` in two modes. The mode is determined
+automatically by a heuristic in the client. Getting the pattern wrong means
+the HTML semantics are incorrect, which can produce confusing screen reader
+announcements and broken navigation.
+
+Both patterns are valid on their own. The accessibility problem occurs when
+a grid partially follows the data table conventions but fails the heuristic,
+causing it to render as a layout table with missing labels.
+
+**Quick rule:** If the grid meets ALL data table conditions → hide captions.
+If it does not → editable fields and fields with tabular intent need visible
+captions; only standalone content fields may hide theirs.
+
+The same heuristic applies to both `grid()` and `fixed()` layouts — either
+can render as a data table or a layout table depending on structure.
+
+DATA TABLE PATTERN (renders as `
` with proper row/column semantics):
+A grid or fixed layout qualifies as a "data table" ONLY when ALL of these
+conditions are met:
+- All direct children of the grid/fixed are groups (no loose fields)
+- Every child of every group is a field (no nested groups or other controls)
+- ALL fields have `ShowCaption = false`
+
+Note: The heuristic checks field captions only — group `ShowCaption` is NOT
+part of the check. A group with a visible caption inside a data table grid
+does NOT break the heuristic and is NOT a violation. However, groups in a
+data table should also have `ShowCaption = false` for correct visual
+presentation.
+
+Good — correct data table pattern:
+```al
+grid(DataGrid)
+{
+ GridLayout = Columns;
+ group(Column1)
+ {
+ ShowCaption = false;
+ field(Name; Rec.Name)
+ {
+ ShowCaption = false;
+ }
+ }
+ group(Column2)
+ {
+ ShowCaption = false;
+ field(Balance; Rec.Balance)
+ {
+ ShowCaption = false;
+ }
+ }
+}
+```
+
+LAYOUT TABLE PATTERN (visual column arrangement, no table semantics):
+Any grid or fixed layout that does NOT meet all data table conditions is
+rendered as a layout table. In a layout table there are no `
` column
+headers, so field captions are the only accessible labels.
+
+**A layout table where editable fields keep their visible captions is NOT a
+violation.** For example, a grid where fields do not have `ShowCaption = false`
+simply renders as a layout table with each field labeled by its own caption —
+this is a valid, accessible pattern. DO NOT flag a grid as a violation merely
+because it does not meet the data table heuristic.
+
+A non-editable field with `ShowCaption = false` is acceptable in a layout
+table ONLY when the field is **standalone content** — it displays a value
+that is meaningful on its own (e.g., a status message, a description) and
+is NOT intended to label or be labeled by another field in the grid.
+
+Good — layout table with standalone content field:
+```al
+grid(InfoGrid)
+{
+ GridLayout = Columns;
+ group(LeftColumn)
+ {
+ field(Address; Rec.Address)
+ {
+ // ShowCaption defaults to true — field has its own label
+ }
+ field(City; Rec.City)
+ {
+ }
+ }
+ group(RightColumn)
+ {
+ field(StatusMessage; StatusText)
+ {
+ Editable = false;
+ ShowCaption = false; // OK — standalone content, not labeling another field
+ }
+ }
+}
+```
+
+ANTI-PATTERN — THE ACCIDENTAL MIX:
+The most common accessibility bug in grid layouts is partially following the
+data table conventions. This happens when a developer arranges fields with
+tabular intent (one field serves as a label or row header for another) but
+the grid does NOT satisfy all the data table heuristic conditions. The
+client falls back to layout table rendering, and the tabular relationships
+between fields are lost — screen readers cannot associate a "header" field
+with its corresponding "value" field.
+
+There are two ways this manifests:
+
+1. **Hidden captions on editable fields in a non-data-table grid.**
+ The field has `ShowCaption = false` but there are no `
` headers to
+ compensate. The field has no accessible label at all.
+
+2. **Fields used as labels for other fields.**
+ One field (e.g., "Statement Period") is intended to serve as a row header
+ for another field (e.g., "Statement Balance"), but since it renders as a
+ layout table, there is no programmatic association between them. A screen
+ reader will announce each field independently with no relationship.
+
+Flag a grid as an accessibility issue when ANY of these are true:
+- An editable field has `ShowCaption = false` and the grid does NOT meet
+ ALL data table conditions
+- Fields are arranged so that one field is clearly intended to label or
+ describe another field (tabular data intent), but the grid does NOT meet
+ ALL data table conditions
+- A grid is **nested inside another grid**. Nested grids are not a supported
+ pattern. Even if an inner grid independently meets the data table heuristic,
+ the outer grid fails because its groups contain non-field children (the
+ inner grids). Always flag nested grids as a violation.
+
+Bad — loose field in grid forces layout table, but captions are hidden:
+```al
+grid(DataGrid)
+{
+ GridLayout = Columns;
+ field(Name; Rec.Name) // Field directly in grid — not in a group
+ {
+ ShowCaption = false; // No table header AND no caption — inaccessible
+ }
+ group(Column2)
+ {
+ ShowCaption = false;
+ field(Balance; Rec.Balance)
+ {
+ ShowCaption = false; // Same problem
+ }
+ }
+}
+```
+
+Bad — non-field child in group breaks data table heuristic, captionless fields lose labels:
+```al
+grid(MixedGrid)
+{
+ GridLayout = Columns;
+ group(Names)
+ {
+ ShowCaption = false;
+ field(Name; Rec.Name)
+ {
+ ShowCaption = false; // Intended as data table column
+ }
+ group(SubGroup) // Nested group — not a field, breaks heuristic
+ {
+ field(Alias; Rec.Alias)
+ {
+ ShowCaption = false;
+ }
+ }
+ }
+ group(Amounts)
+ {
+ ShowCaption = false;
+ field(Balance; Rec.Balance)
+ {
+ ShowCaption = false; // Falls back to layout table — no label at all
+ }
+ }
+}
+```
+
+Bad — fields with tabular intent but heuristic fails due to a field keeping its caption:
+```al
+grid(StatementGrid)
+{
+ GridLayout = Columns;
+ group(Periods)
+ {
+ ShowCaption = false;
+ field(StatementPeriod; Rec."Statement Period")
+ {
+ Editable = false;
+ ShowCaption = false; // Developer intends this as a row header for Balance
+ }
+ }
+ group(Balances)
+ {
+ ShowCaption = false;
+ field(StatementBalance; Rec."Statement Balance")
+ {
+ Editable = false;
+ ShowCaption = false; // Intended to be "labeled by" StatementPeriod
+ }
+ field(DueDate; Rec."Due Date")
+ {
+ // ShowCaption defaults to true — this one field with a visible
+ // caption causes the entire grid to fall back to layout table.
+ // Now StatementPeriod and StatementBalance lose their tabular
+ // relationship and have no accessible labels.
+ }
+ }
+}
+```
+
+GENERAL GUIDANCE:
+- **Minimize use of grid and fixed layouts.** Simple groups and fields reflow
+ better and produce correct semantic markup automatically.
+- If you need forced column layout, prefer simple groups over grid unless you
+ truly need data-table semantics.
+- When reviewing a grid or fixed layout, first check: does it meet ALL data
+ table conditions? If yes, `ShowCaption = false` is correct. If no, ask: is
+ the developer arranging fields with tabular intent (one field labels
+ another)? If so, the grid must be fixed to meet data table conditions.
+ Otherwise, ensure editable fields keep their captions and only standalone
+ content fields hide theirs.
+
+## STYLE PROPERTY — COSMETIC VS SEMANTIC STYLES
+
+The `Style` property on page fields controls text formatting. Some style
+values are purely cosmetic (visual formatting only), while others carry
+semantic meaning that is conveyed through color. For accessibility, assume
+that the style is completely invisible to the user — the meaning must be
+fully determinable from the field caption, value, or adjacent fields.
+
+COSMETIC STYLES (always safe — DO NOT flag these):
+These styles change visual appearance but do not convey semantic meaning.
+They NEVER require additional context and must NOT be reported as findings:
+- None, Standard
+- StandardAccent (Blue)
+- Strong (Bold), StrongAccent (Blue + Bold)
+- Attention (Red + Italic), AttentionAccent (Blue + Italic)
+- Subordinate (Grey)
+
+This applies whether the cosmetic style is set via `Style` or via a
+`StyleExpr` Text variable. If the resolved style is cosmetic, it is safe.
+
+SEMANTIC STYLES (require additional context — flag ONLY these three):
+Only the following three styles carry semantic meaning through color:
+- **Favorable** (Bold + Green) — implies a positive outcome
+- **Unfavorable** (Bold + Italic + Red) — implies a negative outcome
+- **Ambiguous** (Yellow) — implies an uncertain or mixed outcome
+
+EXCEPTION — CUE TILES (fields inside a `cuegroup`):
+Fields inside a `cuegroup` render as cue tiles. The client automatically
+provides an accessible label for semantic
+styles on cue tiles (e.g., "Favorable", "Unfavorable"), so semantic styles
+in a `cuegroup` do NOT need additional context and can be ignored for this
+analysis.
+
+RULE: When a semantic style (Favorable, Unfavorable, Ambiguous) is used,
+the semantic meaning MUST be independently determinable without seeing the
+color. At least one of these conditions must be true:
+1. The **field caption** matches the semantic meaning (e.g., caption is
+ "Error" with Style = Unfavorable, or "Profit" with Style = Favorable)
+2. The **field value** communicates the meaning (e.g., value is "Success!"
+ with Favorable, or a negative number with Unfavorable, or "Something
+ went wrong" with Unfavorable)
+3. An **adjacent field** provides a textual representation of the semantic
+ meaning (e.g., a separate "Status" column reads "High" / "Medium" /
+ "Low" alongside a percentage field styled with Favorable / Ambiguous /
+ Unfavorable)
+
+This rule applies equally whether `Style` is set to a literal value or to
+a variable that evaluates to a semantic style at runtime.
+
+NOTE ON `StyleExpr`: In AL, `StyleExpr` serves two distinct purposes
+depending on its type:
+- **Boolean**: When `StyleExpr` is a Boolean (or Boolean expression), it
+ controls whether the `Style` property is applied. In this case, analyze
+ the `Style` property value — `StyleExpr` itself can be ignored.
+- **Text**: When `StyleExpr` is a Text variable (e.g., `StyleExpr = StatusStyle`
+ where `StatusStyle` is declared as `Text`), the variable contains the style
+ name at runtime (e.g., `StatusStyle := 'Favorable'`). In this case, there
+ may be no `Style` property at all — the `StyleExpr` variable IS the style.
+ Trace the variable assignments in OnAfterGetRecord or OnAfterGetCurrRecord
+ to determine which semantic styles may be applied, then apply the same
+ rules as for a literal `Style` value.
+
+Good — field value communicates the semantic meaning:
+```al
+field(ProfitMargin; Rec."Profit Margin")
+{
+ // Positive values show as green, negative as red.
+ // The sign of the number (+/-) independently conveys the meaning.
+ Style = Favorable;
+ StyleExpr = IsProfitable; // Boolean — toggles whether Style is applied
+}
+field(OverdueAmount; Rec."Overdue Amount")
+{
+ // Caption "Overdue Amount" already implies unfavorable.
+ Style = Unfavorable;
+}
+```
+
+Good — StyleExpr as Text variable with values that match field meaning:
+```al
+field(Status; Rec.Status)
+{
+ // Status is an Option: Open, In Progress, Completed, Overdue.
+ // The option text values themselves communicate the meaning.
+ StyleExpr = StatusStyle; // Text — contains 'Favorable', 'Unfavorable', etc.
+}
+// In OnAfterGetRecord:
+// case Rec.Status of
+// Rec.Status::Open: StatusStyle := 'Standard';
+// Rec.Status::Completed: StatusStyle := 'Favorable';
+// Rec.Status::Overdue: StatusStyle := 'Unfavorable';
+// end;
+```
+
+Good — adjacent field provides semantic context:
+```al
+// In a grid/repeater with columns:
+field(Confidence; Rec."Confidence %")
+{
+ StyleExpr = ConfidenceStyle; // Text — 'Favorable'/'Ambiguous'/'Unfavorable'
+}
+field(ConfidenceLevel; Rec."Confidence Level")
+{
+ // This adjacent column shows "High", "Medium", or "Low" —
+ // providing the textual meaning that the color alone cannot.
+}
+```
+
+Bad — semantic style with no independent way to determine meaning:
+```al
+field(Confidence; Rec."Confidence %")
+{
+ // StyleExpr is 'Favorable' above 90%, 'Ambiguous' 70-90%, 'Unfavorable' below 70%.
+ // But the caption ("Confidence") and value ("85%") do not tell the user
+ // whether 85% is good or bad. Only the color communicates the threshold.
+ StyleExpr = ConfidenceStyle; // Text variable
+}
+```
+
+Bad — semantic style used for purely cosmetic purposes:
+```al
+field(CompanyName; Rec."Company Name")
+{
+ Style = Favorable; // Green text for aesthetics — misleading, implies
+ // the company name is a positive value
+}
+```
+
+COMMON ACCEPTABLE PATTERNS — DO NOT flag these:
+- A **balance or amount** field styled Favorable for positive values and
+ Unfavorable for negative values. The sign (+/-) of the number conveys
+ the meaning independently.
+- A field whose **caption already implies the semantic meaning**: "Overdue
+ Amount" with Unfavorable, "Profit" with Favorable, "Error Count" with
+ Unfavorable. The caption tells the user what the value means.
+- An **Option or Enum** field where the option text values communicate the
+ state (e.g., "Open", "Completed", "Overdue") and the style matches
+ the text (e.g., Favorable for "Completed", Unfavorable for "Overdue").
+- A `StyleExpr` Text variable that resolves to a **cosmetic style** (e.g.,
+ 'Attention', 'Strong'). Cosmetic styles are always safe regardless of
+ context.
+
+## JAVASCRIPT CONTROL ADD-INS
+
+When a developer builds a JavaScript control add-in, they bypass the
+Business Central framework's built-in accessibility support and take full
+responsibility for the accessibility of the rendered HTML, JavaScript, and
+CSS. Review changes to control add-in implementation files for WCAG 2.1 AA
+compliance and general accessibility best practices.
+
+NOTE TO REVIEWER: Automated review of control add-in code is inherently
+non-exhaustive. Many accessibility issues (keyboard flow, screen reader
+announcements, dynamic behavior) require manual testing.
+
+WHEN TO FLAG FOR MANUAL REVIEW:
+If a control add-in diff contains changes that affect UI rendering, ALWAYS
+include a finding recommending a manual accessibility review. UI changes
+include modifications to:
+- HTML templates or DOM manipulation (createElement, innerHTML, appendChild,
+ JSX/TSX markup, template literals producing HTML)
+- CSS or SCSS files (any change to styling, layout, colors, visibility)
+- Event handlers for user interaction (click, keydown, focus, blur)
+- ARIA attributes or roles
+- Dynamic visibility or content updates
+
+If no specific accessibility issues are found but UI-rendering changes exist,
+output a single finding with severity "Low" recommending a manual review.
+Do NOT output an empty array when UI-rendering changes are present — the empty array rule applies only when there are no issues and no UI-rendering changes.
+
+Do NOT flag for manual review if the only changes are to pure business
+logic, data processing, API calls, or other non-rendering code that does
+not touch the DOM or styling.
+
+When reporting issues in control add-in code, include a note that a manual accessibility
+review is recommended for any control add-in that renders a UI.
+
+KEY AREAS TO CHECK:
+
+1. **ARIA and semantic HTML**
+ - Interactive elements must have accessible names (aria-label,
+ aria-labelledby, or visible text content)
+ - Use semantic HTML elements where possible (`