bc-code-intelligence-mcp
Version:
BC Code Intelligence MCP Server - Complete Specialist Bundle with AI-driven expert consultation, seamless handoffs, and context-preserving workflows
716 lines (605 loc) • 25.7 kB
Markdown
# API Page Validation Rule Execution - AL Code Sample
## Basic Validation Rule Implementation
```al
page 50500 "Validated Customer API"
{
PageType = API;
APIPublisher = 'contoso';
APIGroup = 'customers';
APIVersion = 'v1.0';
EntityName = 'customer';
EntitySetName = 'customers';
SourceTable = Customer;
DelayedInsert = true;
ODataKeyFields = SystemId;
layout
{
area(Content)
{
repeater(GroupName)
{
field(id; Rec.SystemId)
{
Caption = 'Id';
Editable = false;
}
field(number; Rec."No.")
{
Caption = 'Number';
// Field validation automatically triggered by BC
}
field(displayName; Rec.Name)
{
Caption = 'Display Name';
// Required field validation
}
field(email; Rec."E-Mail")
{
Caption = 'Email';
// Email format validation through table field definition
}
field(creditLimit; Rec."Credit Limit (LCY)")
{
Caption = 'Credit Limit';
// Numeric validation and business rules
}
field(customerPostingGroup; Rec."Customer Posting Group")
{
Caption = 'Customer Posting Group';
// TableRelation validation
}
}
}
}
// Business validation rules executed during API operations
trigger OnInsertRecord(BelowxRec: Boolean): Boolean
var
CustomerValidation: Codeunit "Customer Validation Rules";
begin
// Execute comprehensive business validation
if not CustomerValidation.ValidateNewCustomer(Rec) then
exit(false);
// Additional API-specific validations
ValidateAPISpecificRules();
exit(true);
end;
trigger OnModifyRecord(): Boolean
var
CustomerValidation: Codeunit "Customer Validation Rules";
begin
// Execute modification validation rules
if not CustomerValidation.ValidateCustomerModification(Rec, xRec) then
exit(false);
ValidateAPISpecificRules();
exit(true);
end;
trigger OnDeleteRecord(): Boolean
var
CustomerValidation: Codeunit "Customer Validation Rules";
begin
// Validate deletion is allowed
if not CustomerValidation.ValidateCustomerDeletion(Rec) then
exit(false);
exit(true);
end;
local procedure ValidateAPISpecificRules()
begin
// API-specific validation rules
if Rec.Name = '' then
Error('Display name is required for API operations');
if Rec."E-Mail" <> '' then
if not IsValidEmail(Rec."E-Mail") then
Error('Invalid email format: %1', Rec."E-Mail");
// Credit limit validation
if Rec."Credit Limit (LCY)" < 0 then
Error('Credit limit cannot be negative');
end;
local procedure IsValidEmail(Email: Text): Boolean
var
EmailPattern: Text;
begin
EmailPattern := '*@*.*';
exit(Email.Contains('@') and (StrLen(Email) > 5));
end;
}
// Comprehensive validation rules codeunit
codeunit 50500 "Customer Validation Rules"
{
// Validate new customer creation
procedure ValidateNewCustomer(var Customer: Record Customer): Boolean
begin
// Required field validations
if not ValidateRequiredFields(Customer) then
exit(false);
// Business rule validations
if not ValidateBusinessRules(Customer) then
exit(false);
// Data integrity validations
if not ValidateDataIntegrity(Customer) then
exit(false);
exit(true);
end;
// Validate customer modifications
procedure ValidateCustomerModification(var Customer: Record Customer; OldCustomer: Record Customer): Boolean
begin
// Check if critical fields are being modified
if Customer."No." <> OldCustomer."No." then
Error('Customer number cannot be modified');
// Validate changes don't violate business rules
if Customer."Customer Posting Group" <> OldCustomer."Customer Posting Group" then
if not ValidatePostingGroupChange(Customer, OldCustomer) then
exit(false);
// Validate credit limit changes
if Customer."Credit Limit (LCY)" <> OldCustomer."Credit Limit (LCY)" then
if not ValidateCreditLimitChange(Customer, OldCustomer) then
exit(false);
exit(ValidateNewCustomer(Customer));
end;
// Validate customer deletion
procedure ValidateCustomerDeletion(var Customer: Record Customer): Boolean
var
SalesLine: Record "Sales Line";
CustLedgerEntry: Record "Cust. Ledger Entry";
begin
// Check for existing transactions
CustLedgerEntry.SetRange("Customer No.", Customer."No.");
if not CustLedgerEntry.IsEmpty() then
Error('Cannot delete customer %1. Customer has ledger entries.', Customer."No.");
// Check for open sales documents
SalesLine.SetRange("Sell-to Customer No.", Customer."No.");
if not SalesLine.IsEmpty() then
Error('Cannot delete customer %1. Customer has open sales documents.', Customer."No.");
exit(true);
end;
local procedure ValidateRequiredFields(var Customer: Record Customer): Boolean
begin
if Customer."No." = '' then
Error('Customer number is required');
if Customer.Name = '' then
Error('Customer name is required');
if Customer."Customer Posting Group" = '' then
Error('Customer posting group is required');
exit(true);
end;
local procedure ValidateBusinessRules(var Customer: Record Customer): Boolean
var
CustomerPostingGroup: Record "Customer Posting Group";
PaymentTerms: Record "Payment Terms";
begin
// Validate posting group exists
if not CustomerPostingGroup.Get(Customer."Customer Posting Group") then
Error('Customer posting group %1 does not exist', Customer."Customer Posting Group");
// Validate payment terms if specified
if Customer."Payment Terms Code" <> '' then
if not PaymentTerms.Get(Customer."Payment Terms Code") then
Error('Payment terms %1 do not exist', Customer."Payment Terms Code");
// Validate credit limit is reasonable
if Customer."Credit Limit (LCY)" > 1000000 then
Error('Credit limit exceeds maximum allowed amount of 1,000,000');
exit(true);
end;
local procedure ValidateDataIntegrity(var Customer: Record Customer): Boolean
var
ExistingCustomer: Record Customer;
begin
// Check for duplicate customer numbers
ExistingCustomer.SetRange("No.", Customer."No.");
ExistingCustomer.SetFilter(SystemId, '<>%1', Customer.SystemId);
if not ExistingCustomer.IsEmpty() then
Error('Customer number %1 already exists', Customer."No.");
// Validate email uniqueness if configured
if Customer."E-Mail" <> '' then
if not ValidateEmailUniqueness(Customer) then
exit(false);
exit(true);
end;
local procedure ValidatePostingGroupChange(var Customer: Record Customer; OldCustomer: Record Customer): Boolean
var
CustLedgerEntry: Record "Cust. Ledger Entry";
begin
// Prevent posting group change if customer has open entries
CustLedgerEntry.SetRange("Customer No.", Customer."No.");
CustLedgerEntry.SetRange(Open, true);
if not CustLedgerEntry.IsEmpty() then
Error('Cannot change posting group for customer %1. Customer has open ledger entries.', Customer."No.");
exit(true);
end;
local procedure ValidateCreditLimitChange(var Customer: Record Customer; OldCustomer: Record Customer): Boolean
var
SalesHeader: Record "Sales Header";
OutstandingAmount: Decimal;
begin
// If credit limit is being reduced, check outstanding orders
if Customer."Credit Limit (LCY)" < OldCustomer."Credit Limit (LCY)" then begin
SalesHeader.SetRange("Sell-to Customer No.", Customer."No.");
SalesHeader.SetRange("Document Type", SalesHeader."Document Type"::Order);
if SalesHeader.FindSet() then
repeat
SalesHeader.CalcFields("Amount Including VAT");
OutstandingAmount += SalesHeader."Amount Including VAT";
until SalesHeader.Next() = 0;
if OutstandingAmount > Customer."Credit Limit (LCY)" then
Error('Cannot reduce credit limit below outstanding order amount of %1', OutstandingAmount);
end;
exit(true);
end;
local procedure ValidateEmailUniqueness(var Customer: Record Customer): Boolean
var
ExistingCustomer: Record Customer;
begin
ExistingCustomer.SetRange("E-Mail", Customer."E-Mail");
ExistingCustomer.SetFilter(SystemId, '<>%1', Customer.SystemId);
if not ExistingCustomer.IsEmpty() then
Error('Email address %1 is already assigned to another customer', Customer."E-Mail");
exit(true);
end;
}
```
## Advanced Field-Level Validation
```al
page 50501 "Item Advanced Validation API"
{
PageType = API;
APIPublisher = 'contoso';
APIGroup = 'inventory';
APIVersion = 'v1.0';
EntityName = 'item';
EntitySetName = 'items';
SourceTable = Item;
DelayedInsert = true;
ODataKeyFields = SystemId;
layout
{
area(Content)
{
repeater(GroupName)
{
field(id; Rec.SystemId)
{
Caption = 'Id';
Editable = false;
}
field(number; Rec."No.")
{
Caption = 'Number';
trigger OnValidate()
begin
ValidateItemNumber();
end;
}
field(description; Rec.Description)
{
Caption = 'Description';
trigger OnValidate()
begin
ValidateDescription();
end;
}
field(unitPrice; Rec."Unit Price")
{
Caption = 'Unit Price';
trigger OnValidate()
begin
ValidateUnitPrice();
end;
}
field(unitCost; Rec."Unit Cost")
{
Caption = 'Unit Cost';
trigger OnValidate()
begin
ValidateUnitCost();
end;
}
field(inventoryPostingGroup; Rec."Inventory Posting Group")
{
Caption = 'Inventory Posting Group';
trigger OnValidate()
begin
ValidateInventoryPostingGroup();
end;
}
}
}
}
local procedure ValidateItemNumber()
var
ItemNumberPattern: Text;
NoSeriesManagement: Codeunit NoSeriesManagement;
begin
if Rec."No." = '' then
Error('Item number cannot be empty');
// Validate item number format
ItemNumberPattern := GetItemNumberPattern();
if ItemNumberPattern <> '' then
if not MatchesPattern(Rec."No.", ItemNumberPattern) then
Error('Item number %1 does not match required pattern %2', Rec."No.", ItemNumberPattern);
// Check for duplicate item numbers
if ItemNumberExists(Rec."No.", Rec.SystemId) then
Error('Item number %1 already exists', Rec."No.");
end;
local procedure ValidateDescription()
begin
if Rec.Description = '' then
Error('Item description is required');
if StrLen(Rec.Description) < 3 then
Error('Item description must be at least 3 characters long');
// Check for prohibited words
if ContainsProhibitedWords(Rec.Description) then
Error('Item description contains prohibited content');
end;
local procedure ValidateUnitPrice()
begin
if Rec."Unit Price" < 0 then
Error('Unit price cannot be negative');
// Validate against unit cost for margin checking
if (Rec."Unit Cost" > 0) and (Rec."Unit Price" < Rec."Unit Cost") then
if not Confirm('Unit price is below unit cost. Continue?') then
Error('Unit price validation failed');
// Check for unrealistic pricing
if Rec."Unit Price" > 1000000 then
Error('Unit price exceeds maximum allowed amount');
end;
local procedure ValidateUnitCost()
begin
if Rec."Unit Cost" < 0 then
Error('Unit cost cannot be negative');
// Validate cost against existing inventory value
ValidateCostAgainstInventory();
end;
local procedure ValidateInventoryPostingGroup()
var
InventoryPostingGroup: Record "Inventory Posting Group";
begin
if Rec."Inventory Posting Group" = '' then
Error('Inventory posting group is required');
if not InventoryPostingGroup.Get(Rec."Inventory Posting Group") then
Error('Inventory posting group %1 does not exist', Rec."Inventory Posting Group");
end;
local procedure GetItemNumberPattern(): Text
var
InventorySetup: Record "Inventory Setup";
begin
if InventorySetup.Get() then
exit(InventorySetup."Item Nos."); // Use number series as pattern
exit('');
end;
local procedure MatchesPattern(Value: Text; Pattern: Text): Boolean
var
NoSeriesLine: Record "No. Series Line";
begin
// Simplified pattern matching for demo
// In real implementation, use proper number series validation
exit(StrLen(Value) >= 4);
end;
local procedure ItemNumberExists(ItemNo: Code[20]; ExcludeSystemId: Guid): Boolean
var
Item: Record Item;
begin
Item.SetRange("No.", ItemNo);
Item.SetFilter(SystemId, '<>%1', ExcludeSystemId);
exit(not Item.IsEmpty());
end;
local procedure ContainsProhibitedWords(Description: Text): Boolean
var
ProhibitedWords: List of [Text];
Word: Text;
begin
// Define prohibited words for item descriptions
ProhibitedWords.Add('TEMP');
ProhibitedWords.Add('TEST');
ProhibitedWords.Add('DELETE');
foreach Word in ProhibitedWords do
if Description.ToUpper().Contains(Word) then
exit(true);
exit(false);
end;
local procedure ValidateCostAgainstInventory()
var
ItemLedgerEntry: Record "Item Ledger Entry";
AverageCost: Decimal;
Variance: Decimal;
begin
// Calculate current average cost
ItemLedgerEntry.SetRange("Item No.", Rec."No.");
ItemLedgerEntry.CalcSums(Quantity, "Cost Amount (Actual)");
if ItemLedgerEntry.Quantity <> 0 then begin
AverageCost := ItemLedgerEntry."Cost Amount (Actual)" / ItemLedgerEntry.Quantity;
Variance := Abs(Rec."Unit Cost" - AverageCost) / AverageCost * 100;
// Warn if cost variance exceeds threshold
if Variance > 25 then // 25% variance threshold
if not Confirm('Unit cost varies %1% from average inventory cost. Continue?', false, Round(Variance, 1)) then
Error('Cost validation failed');
end;
end;
}
```
## Cross-Field and Business Logic Validation
```al
// Complex validation codeunit for sales orders
codeunit 50501 "Sales Order Validation API"
{
// Comprehensive sales order validation
procedure ValidateSalesOrder(var SalesHeader: Record "Sales Header"): Boolean
begin
// Execute all validation rules in sequence
if not ValidateOrderHeader(SalesHeader) then
exit(false);
if not ValidateCustomerRelationship(SalesHeader) then
exit(false);
if not ValidateOrderTotals(SalesHeader) then
exit(false);
if not ValidateInventoryAvailability(SalesHeader) then
exit(false);
exit(true);
end;
local procedure ValidateOrderHeader(var SalesHeader: Record "Sales Header"): Boolean
begin
// Date validations
if SalesHeader."Order Date" = 0D then
Error('Order date is required');
if SalesHeader."Order Date" < WorkDate() - 30 then
Error('Order date cannot be more than 30 days in the past');
if SalesHeader."Requested Delivery Date" < SalesHeader."Order Date" then
Error('Requested delivery date cannot be before order date');
// Document status validation
if SalesHeader."Document Type" <> SalesHeader."Document Type"::Order then
Error('Only sales orders can be processed through this API');
exit(true);
end;
local procedure ValidateCustomerRelationship(var SalesHeader: Record "Sales Header"): Boolean
var
Customer: Record Customer;
CustLedgerEntry: Record "Cust. Ledger Entry";
OutstandingAmount: Decimal;
begin
// Validate customer exists and is active
if not Customer.Get(SalesHeader."Sell-to Customer No.") then
Error('Customer %1 does not exist', SalesHeader."Sell-to Customer No.");
if Customer.Blocked <> Customer.Blocked::" " then
Error('Customer %1 is blocked for %2', Customer."No.", Customer.Blocked);
// Check credit limit
if Customer."Credit Limit (LCY)" > 0 then begin
CustLedgerEntry.SetRange("Customer No.", Customer."No.");
CustLedgerEntry.SetRange(Open, true);
CustLedgerEntry.CalcSums("Remaining Amt. (LCY)");
OutstandingAmount := CustLedgerEntry."Remaining Amt. (LCY)";
SalesHeader.CalcFields("Amount Including VAT");
if (OutstandingAmount + SalesHeader."Amount Including VAT") > Customer."Credit Limit (LCY)" then
Error('Order would exceed customer credit limit. Outstanding: %1, Order: %2, Limit: %3',
OutstandingAmount, SalesHeader."Amount Including VAT", Customer."Credit Limit (LCY)");
end;
exit(true);
end;
local procedure ValidateOrderTotals(var SalesHeader: Record "Sales Header"): Boolean
var
SalesLine: Record "Sales Line";
TotalAmount: Decimal;
LineCount: Integer;
begin
// Validate order has lines
SalesLine.SetRange("Document Type", SalesHeader."Document Type");
SalesLine.SetRange("Document No.", SalesHeader."No.");
LineCount := SalesLine.Count();
if LineCount = 0 then
Error('Sales order must have at least one line');
if LineCount > 100 then
Error('Sales order cannot have more than 100 lines');
// Validate minimum order amount
SalesHeader.CalcFields("Amount Including VAT");
if SalesHeader."Amount Including VAT" < 10 then
Error('Minimum order amount is 10.00');
exit(true);
end;
local procedure ValidateInventoryAvailability(var SalesHeader: Record "Sales Header"): Boolean
var
SalesLine: Record "Sales Line";
Item: Record Item;
AvailableInventory: Decimal;
begin
SalesLine.SetRange("Document Type", SalesHeader."Document Type");
SalesLine.SetRange("Document No.", SalesHeader."No.");
SalesLine.SetRange(Type, SalesLine.Type::Item);
if SalesLine.FindSet() then
repeat
if Item.Get(SalesLine."No.") then begin
Item.CalcFields(Inventory, "Qty. on Sales Order");
AvailableInventory := Item.Inventory - Item."Qty. on Sales Order";
if SalesLine.Quantity > AvailableInventory then
Error('Insufficient inventory for item %1. Requested: %2, Available: %3',
Item."No.", SalesLine.Quantity, AvailableInventory);
end;
until SalesLine.Next() = 0;
exit(true);
end;
}
// API page with comprehensive validation
page 50502 "Sales Order Validation API"
{
PageType = API;
APIPublisher = 'contoso';
APIGroup = 'sales';
APIVersion = 'v1.0';
EntityName = 'salesOrder';
EntitySetName = 'salesOrders';
SourceTable = "Sales Header";
DelayedInsert = true;
ODataKeyFields = SystemId;
SourceTableView = WHERE("Document Type" = CONST(Order));
layout
{
area(Content)
{
repeater(GroupName)
{
field(id; Rec.SystemId)
{
Caption = 'Id';
Editable = false;
}
field(number; Rec."No.")
{
Caption = 'Number';
}
field(customerNumber; Rec."Sell-to Customer No.")
{
Caption = 'Customer Number';
}
field(orderDate; Rec."Order Date")
{
Caption = 'Order Date';
}
field(requestedDeliveryDate; Rec."Requested Delivery Date")
{
Caption = 'Requested Delivery Date';
}
field(totalAmount; TotalAmount)
{
Caption = 'Total Amount';
Editable = false;
}
}
}
}
var
TotalAmount: Decimal;
SalesOrderValidation: Codeunit "Sales Order Validation API";
trigger OnAfterGetRecord()
begin
Rec.CalcFields("Amount Including VAT");
TotalAmount := Rec."Amount Including VAT";
end;
trigger OnInsertRecord(BelowxRec: Boolean): Boolean
begin
// Execute comprehensive validation before insert
exit(SalesOrderValidation.ValidateSalesOrder(Rec));
end;
trigger OnModifyRecord(): Boolean
begin
// Execute validation before modification
exit(SalesOrderValidation.ValidateSalesOrder(Rec));
end;
}
```
## Implementation Notes
**Validation Rule Execution Best Practices:**
1. **Validation Strategy:**
- Implement field-level validation for immediate feedback
- Use record-level validation for business logic enforcement
- Combine BC standard validation with custom business rules
- Provide clear, actionable error messages
2. **Performance Optimization:**
- Execute lightweight validations first (required fields, formats)
- Defer expensive validations (database lookups) until necessary
- Cache validation results when possible
- Use filtered queries to minimize database access
3. **Error Handling:**
- Return specific error messages indicating the validation failure
- Include relevant data in error messages for troubleshooting
- Use consistent error message formats across API endpoints
- Log validation failures for monitoring and improvement
4. **Business Rule Integration:**
- Leverage existing BC table validation where possible
- Implement custom validation for API-specific requirements
- Ensure validation rules align with UI behavior
- Document validation rules for API consumers