Skip to main content

Change Document: v2.2.0.0 — Customer Creation Bug Fixes & Query Optimizations

FieldValue
Version2.2.0.0
Date2026-03-04
ExtensionBC Dialing Application (Cambay Solutions)
SeverityCritical — blocking end-user workflow
StatusImplemented (pending deployment & verification)

Background

The v2.1.0.0 release addressed Customer table lock contention caused by the Nextiva Integration background job (CU-60003) by breaking its single long-running transaction into per-record micro-transactions. As part of that release, LockTable() was added to API Pages 80000 (ReceivePhoneNumber) and 80003 (ReceiveEmail) as a concurrency guard against duplicate Customer creation from simultaneous API calls.

In practice, the LockTable() cure is worse than the disease. Every incoming phone call or email acquires an exclusive lock on the entire Customer table. While this lock is held — even for milliseconds — no other session in BC can insert or modify any Customer record. CS agents editing or creating customers while calls are coming in receive the error "The information on this page is out of date," forcing them to discard their changes and retry. During periods of moderate call volume, this effectively blocks all concurrent Customer operations.

Additionally, two pre-existing bugs in the customer creation logic (present since v2.0.0.12) were identified during this investigation:

  1. Insert() without triggers — Both API pages used cust.Insert() instead of cust.Insert(true), skipping the standard BC OnInsert trigger. This bypassed No. Series validation, default field population, and any event subscribers from other extensions.

  2. No Name set on new customers — Neither API page set a Name on newly created Customer records. The reference implementation in CU-60000 (HandleIncomingCall) correctly sets Name := 'New Customer' as a placeholder. The API pages omitted this, producing blank customer records that are difficult to identify in lists and lookups. Production data confirms the presence of blank-name customers created by the API.


Summary of Changes

#SeverityCategoryDescription
1CriticalFixedRemove LockTable() from Pages 80000 and 80003
2HighFixedUse Insert(true) instead of Insert() for proper customer setup
3MediumFixedSet Name := 'New Customer' placeholder on API-created customers
4MediumFixedSeparate read-only Customer lookup from creation path to prevent SetLoadFields bleed-through
5MediumFixedCustomer No. field not populated on phone/email log records when an existing customer is found (Pages 80000 and 80003)
6MediumFixedCustomer No. field not populated on EndCall log records (Page 80001) — FindFirst() return value was discarded
7MediumFixedSwitch Job Queue queries to use ProcessingStatus secondary keys instead of overriding with primary key
8LowFixedEliminate duplicate FindFirst() call in AddCustomerBlobLink
9LowFixedAdd missing filter fields to SetLoadFields calls in LogError procedures
10LowAddedSecondary key ProcessingStatus on CustomerPhoneLog and CustomerEmailLog tables
11LowAddedSecondary indexes on Customer table for Phone No. and E-Mail lookups via table extension
12LowAddedSetLoadFields optimizations on read-only Customer lookups
13LowChangedRe-apply phone/email after ApplyCustomerTemplate as defensive measure
14LowChangedRemove unused variable declarations and dead code

Detailed Changes

1. Remove LockTable() from Pages 80000 and 80003 (Critical)

Files:

  • src/Page/Pag-80000.ReceivePhoneNumber.al
  • src/Page/Pag-80003.ReceiveEmail.al

Before:

cust.Reset();
cust.LockTable();
cust.SetRange("Phone No.", Rec.PhoneNumber);

After:

cust.SetRange("Phone No.", Rec.PhoneNumber);

Rationale: The LockTable() call acquires an exclusive lock on the entire Customer table for the duration of the API page's OnInsertRecord trigger. While the lock was intended to prevent duplicate Customer creation from concurrent API calls with the same phone number, the cost — blocking all Customer table operations across all sessions — far outweighs the benefit. The duplicate-customer scenario requires two API calls for the same phone number to arrive within the same millisecond, which is negligible in practice.

The cust.Reset() call was also removed. A freshly declared Record variable has no filters or state to reset, making the call unnecessary.

2. Use Insert(true) Instead of Insert() (High)

Files:

  • src/Page/Pag-80000.ReceivePhoneNumber.al
  • src/Page/Pag-80003.ReceiveEmail.al

Before:

cust.Insert();

After:

cust.Insert(true);

Rationale: Insert() without the true parameter skips the OnInsert trigger on the Customer table. This trigger handles standard BC customer setup: No. Series consumption, default field population, and event subscriber execution. Skipping it can leave the customer record in an incomplete state.

The reference implementation in CU-60000 (HandleIncomingCall) uses Insert(true) and has been running in production without issues. The SetInsertFromTemplate(true) flag suppresses template-related logic during insert, and InitCustomerNo pre-assigns the customer number, so the OnInsert trigger does not re-assign it.

3. Set Placeholder Name on New Customers (Medium)

Files:

  • src/Page/Pag-80000.ReceivePhoneNumber.al
  • src/Page/Pag-80003.ReceiveEmail.al

Before:

cust.Init();
cust."Phone No." := Rec.PhoneNumber;

After:

cust.Init();
cust."Phone No." := Rec.PhoneNumber;
cust.Name := 'New Customer';

Rationale: CU-60000 sets Name := 'New Customer' as a placeholder; the API pages did not. This results in blank-name Customer records that are difficult to identify in the Customer List and lookups. The placeholder name makes API-created customers immediately identifiable while the CS agent fills in the actual customer details.

4. Re-apply Phone/Email After Template Application (Low)

Files:

  • src/Page/Pag-80000.ReceivePhoneNumber.al
  • src/Page/Pag-80003.ReceiveEmail.al

Before:

ConfigTemplateMgt.ApplyCustomerTemplate(cust, TemplateHeader);

Rec."Customer No." := cust."No.";

After (Pag-80000):

ConfigTemplateMgt.ApplyCustomerTemplate(cust, TemplateHeader);
cust."Phone No." := Rec.PhoneNumber;
cust.Modify(true);

Rec."Customer No." := cust."No.";

After (Pag-80003):

ConfigTemplateMgt.ApplyCustomerTemplate(cust, TemplateHeader);
cust."E-Mail" := Rec.EmailAddress1;
cust.Modify(true);

Rec."Customer No." := cust."No.";

Rationale: ApplyCustomerTemplate applies all field values defined in the ECOMMERCE template and calls Modify() internally. If the template is ever updated to define a Phone No. or E-Mail default value, it would overwrite the incoming call's contact information. Re-applying the phone number or email after template application and explicitly calling Modify(true) ensures the contact information from the incoming call is always preserved, regardless of future template changes.

5. Separate Read-Only Customer Lookup from Creation Path (Medium)

Files:

  • src/Page/Pag-80000.ReceivePhoneNumber.al
  • src/Page/Pag-80003.ReceiveEmail.al

Before:

cust.SetLoadFields("No.", "Phone No.");
cust.SetRange("Phone No.", Rec.PhoneNumber);
if not cust.FindFirst() then begin
cust.Init(); // SetLoadFields still active on this variable
cust.Insert(true); // triggers fire with SetLoadFields constraint
ConfigTemplateMgt.ApplyCustomerTemplate(cust, TemplateHeader); // internal Get() would be partial
cust.Modify(true); // xRec population may be affected

After:

CustLookup.SetLoadFields("No.", "Phone No.");
CustLookup.SetRange("Phone No.", Rec.PhoneNumber);
if not CustLookup.FindFirst() then begin
NewCust.Init(); // clean variable, no SetLoadFields constraint
NewCust.Insert(true); // triggers fire normally
ConfigTemplateMgt.ApplyCustomerTemplate(NewCust, TemplateHeader); // full record access
NewCust.Modify(true); // xRec populated correctly

Rationale: SetLoadFields is "sticky" on a record variable — Init() does not clear it. If ApplyCustomerTemplate or any OnInsert/OnModify subscriber internally calls Get() or Find() on the same record variable, only the fields specified in SetLoadFields would be loaded. All other fields — including Name, posting groups, and template defaults — would revert to default values. Using a separate CustLookup variable for the read-only query isolates the SetLoadFields optimization from the creation path.

6. Populate Customer No. on Log Records for Existing Customers (Medium)

Files:

  • src/Page/Pag-80000.ReceivePhoneNumber.al
  • src/Page/Pag-80003.ReceiveEmail.al

Before:

end else begin
sURL := StrSubstNo(BaseURL, CustLookup."No.");
Rec.CustomerURL := sURL;
end;

After:

end else begin
Rec."Customer No." := CustLookup."No.";
sURL := StrSubstNo(BaseURL, CustLookup."No.");
Rec.CustomerURL := sURL;
end;

Rationale: When an existing customer was found by phone number or email, the Customer No. field on the log record was never set — it was only populated in the new-customer creation branch. This meant phone and email log records for existing customers had a blank Customer No., making it impossible to trace log entries back to specific customers without re-running the phone/email lookup. The fix mirrors the assignment already present in the new-customer branch.

6b. Populate Customer No. on EndCall Log Records (Medium)

File: src/Page/Pag-80001.EndCall.al

Before:

cust.SetLoadFields("No.", "Phone No.");
cust.SetRange("Phone No.", Rec.PhoneNumber);
cust.FindFirst(); // return value discarded — no effect

After:

cust.SetLoadFields("No.", "Phone No.");
cust.SetRange("Phone No.", Rec.PhoneNumber);
if cust.FindFirst() then
Rec."Customer No." := cust."No.";

Rationale: The EndCall page's OnInsertRecord trigger performed a Customer lookup but discarded the FindFirst() return value entirely. Rec."Customer No." was never set, so every EndCall log record had a blank Customer No.. This is the same issue as item 6 (Pages 80000 and 80003) but in the EndCall page, which was not part of the original fix. The corrected code uses the lookup result to populate Customer No. when a match is found, consistent with the pattern in ReceivePhoneNumber and ReceiveEmail.

Note: EndCall does not create new customers — its sole purpose is to record the Nextiva session ID for async processing by the Job Queue. If no customer is found by the incoming phone number, Customer No. remains blank and the Job Queue's TryProcessPhoneLog procedure will attempt its own customer lookup from the log record's phone number field.

7. Switch Job Queue Queries to ProcessingStatus Keys (Medium)

Files:

  • src/Codeunit/CU60003.NextivaIntegration.al

Before:

ProcessCalls.SetRange(SummaryUpdated, false);
ProcessCalls.SetRange(TranscriptUpdated, false);
ProcessCalls.SetRange(RecordingUpdated, false);
ProcessCalls.SetFilter(Attempts, '<%1', 3);
ProcessCalls.SetCurrentKey("No."); // forces primary key — new index ignored
ProcessCalls.Ascending(false);

After:

ProcessCalls.SetCurrentKey(SummaryUpdated, TranscriptUpdated, RecordingUpdated, Attempts);
ProcessCalls.SetRange(SummaryUpdated, false);
ProcessCalls.SetRange(TranscriptUpdated, false);
ProcessCalls.SetRange(RecordingUpdated, false);
ProcessCalls.SetFilter(Attempts, '<%1', 3);

Rationale: The new ProcessingStatus secondary keys on CustomerPhoneLog and CustomerEmailLog were added to optimize the Job Queue's filter queries. However, SetCurrentKey("No.") was overriding BC's index selection, forcing the query optimizer to traverse the primary key and evaluate filters row-by-row. Switching SetCurrentKey to the ProcessingStatus key allows the database to use the index for efficient filtering.

The Ascending(false) directive was removed because it only applied to the primary key ordering (newest-first processing). Processing order is not functionally significant for the sync batch — all pending records are processed regardless of order.

8. Eliminate Duplicate FindFirst() Call (Low)

Files:

  • src/Codeunit/CU60003.NextivaIntegration.al (AddCustomerBlobLink)
  • src/Codeunit/CU-60004.NextivaIntegrationManual.al (AddCustomerBlobLink)

Before:

if CustomerRec.FindFirst() then begin
bReturn := CustomerRec.FindFirst(); // redundant second database call

After:

bReturn := CustomerRec.FindFirst();
if bReturn then begin

Rationale: The original code called FindFirst() twice — the first call's result was used for the if condition, then immediately discarded while calling FindFirst() again to populate bReturn. The second call was a wasted database round-trip with identical results.

9. Add SetLoadFields to Customer Lookups in LogError (Low)

Files:

  • src/Codeunit/CU60003.NextivaIntegration.al (LogError)
  • src/Codeunit/CU-60004.NextivaIntegrationManual.al (LogError)

Before:

CustomerRec.SetRange("Phone No.", CstrPhoneLog.PhoneNumber);
if CustomerRec.FindFirst() then
ErrLog."Customer No." := CustomerRec."No.";

After:

CustomerRec.SetLoadFields("No.", "Phone No.");
CustomerRec.SetRange("Phone No.", CstrPhoneLog.PhoneNumber);
if CustomerRec.FindFirst() then
ErrLog."Customer No." := CustomerRec."No.";

Rationale: The LogError procedure queried the Customer table with no SetLoadFields, causing BC to load all 200+ Customer fields for a lookup that only uses "No.". Added SetLoadFields with both the return field ("No.") and the filter field ("Phone No." or "E-Mail") — including the filter field follows best practice for clarity and correctness across platform versions.

10. Add SetLoadFields Optimizations (Low)

Files:

  • src/Page/Pag-80000.ReceivePhoneNumber.al
  • src/Page/Pag-80001.EndCall.al
  • src/Page/Pag-80003.ReceiveEmail.al
  • src/Codeunit/CU60003.NextivaIntegration.al
  • src/Codeunit/CU-60004.NextivaIntegrationManual.al

Added SetLoadFields calls before all read-only Customer table lookups. This tells BC to only load the specified fields from the database, reducing data transfer and memory usage. Based on Application Insights telemetry showing full-record loads where only 1-2 fields were needed.

11. Add ProcessingStatus Secondary Keys (Low)

Files:

  • src/Table/Tab80000.CustomerPhoneLog.al
  • src/Table/Tab80002.CustomerEmailLog.al

Added non-clustered secondary keys matching the filter patterns used by the Job Queue processor (CU-60003):

  • CustomerPhoneLog: key on (SummaryUpdated, TranscriptUpdated, RecordingUpdated, Attempts)
  • CustomerEmailLog: key on (InboundEmailUpdated, OutboundEmailUpdated, Attempts)

12. Add Secondary Indexes on Customer Table (Low)

Files:

  • src/TableExt/TabExt-60000.CustomerDialerKeys.al (new)

Added a table extension on the standard Customer table with two non-clustered secondary keys:

  • PhoneNo key on "Phone No." — supports the phone number lookups used across Pages 80000, 80001, and Codeunits 60003, 60004
  • EMail key on "E-Mail" — supports the email lookups used in Page 80003 and Codeunits 60003, 60004

Without these indexes, every SetRange("Phone No.", ...) or SetRange("E-Mail", ...) query against the Customer table performs a full table scan. As the Customer table grows, these scans degrade linearly — particularly impactful for the Job Queue processor (CU-60003), which runs these lookups for every pending log record in each cycle.

Write overhead tradeoff: Every Insert, Modify, and Delete operation on the Customer table now maintains two additional index entries. For Bestway's Customer table (moderate volume, infrequent inserts), the read performance gain significantly outweighs the marginal write cost. The Customer table is read-heavy in this extension's usage pattern — every incoming call, email, and Job Queue cycle queries it by phone or email.

13. Remove Unused Variable Declarations (Low)

Files:

  • src/Page/Pag-80000.ReceivePhoneNumber.al
  • src/Page/Pag-80003.ReceiveEmail.al

Removed declarations for myInt, Phone, bReturn, and IsHandled — all declared but never referenced. Removed commented-out debug code (test URLs, RecordId assignments).


Object Inventory

Object IDObject TypeNameStatus
60000Table ExtensionCustomerDialerKeysNew — secondary indexes on Customer table
60003CodeunitNextiva IntegrationModified
60004CodeunitNextiva Integration ManualModified
80000PageReceivePhoneNumberModified
80001PageEndCallModified — SetLoadFields, populate Customer No. on log record
80003PageReceiveEmailModified
80000TableCustomerPhoneLogModified (new secondary key)
80002TableCustomerEmailLogModified (new secondary key)
app.jsonExtension manifestModified (version bump)

File Structure After Changes

Cambay Solutions_BC Dialing Application/
├── app.json (modified — v2.2.0.0)
├── docs/
│ ├── CHANGELOG.md (new)
│ ├── CHANGE-v2.1.0.0.md (migrated from root)
│ ├── CHANGE-v2.1.0.0.docx (migrated from root)
│ └── CHANGE-v2.2.0.0.md (new — this document)
├── src/
│ ├── Codeunit/
│ │ ├── CU-60000.PhoneIntegration.al (unchanged)
│ │ ├── CU-60001.AttachMediaCodeunit.al (unchanged)
│ │ ├── CU-60002.AzureBlobStorage.al (unchanged)
│ │ ├── CU-60004.NextivaIntegrationManual.al (modified — SetLoadFields, duplicate FindFirst)
│ │ └── CU60003.NextivaIntegration.al (modified — SetLoadFields, SetCurrentKey, duplicate FindFirst)
│ ├── Page/
│ │ ├── Pag-60000.ReceivingComponent.al (unchanged)
│ │ ├── Pag-60001.CompletionComponent.al (unchanged)
│ │ ├── Pag-80000.ReceivePhoneNumber.al (modified)
│ │ ├── Pag-80001.EndCall.al (modified — SetLoadFields, populate Customer No.)
│ │ ├── Pag-80003.ReceiveEmail.al (modified)
│ │ └── ...
│ ├── Table/
│ │ ├── Tab80000.CustomerPhoneLog.al (modified — new secondary key)
│ │ ├── Tab80002.CustomerEmailLog.al (modified — new secondary key)
│ │ └── ...
│ └── TableExt/
│ └── TabExt-60000.CustomerDialerKeys.al (new — Customer table indexes)
└── BCDialerPermissions.permissionset.al (unchanged)

Known Limitations

Duplicate Customer Risk (Accepted)

Removing LockTable() reintroduces a theoretical race condition: two simultaneous API calls for the same phone number could both pass the FindFirst() check and create duplicate Customer records. This requires:

  1. Two calls from the same phone number to arrive at the Azure Function within the same processing window
  2. Both to reach the BC OData endpoint before either completes the Insert(true) call
  3. The phone number lookup to return "not found" for both before either insert commits

In practice, this is a sub-millisecond window. Even if it occurs, the result is a duplicate Customer record — a minor data quality issue that can be resolved by merging records manually. The alternative — locking the entire Customer table on every incoming call — has been demonstrated to block CS agent workflows in production.

Job Queue Processing Order Changed

The v2.1.0.0 Job Queue processor (CU-60003) processed records newest-first (SetCurrentKey("No."); Ascending(false)). In v2.2.0.0, processing order follows the ProcessingStatus secondary key, which orders by status fields rather than record number. Within the filtered set (all status fields = false, Attempts < 3), record order is non-deterministic. This is acceptable because all pending records are processed in each Job Queue cycle regardless of order — the processing logic is idempotent and order-independent.

Customer Table Index Write Overhead (Accepted)

The new CustomerDialerKeys table extension (TabExt-60000) adds two secondary indexes to the standard Customer table — one on Phone No. and one on E-Mail. Every Insert, Modify, and Delete operation on the Customer table now maintains these additional index entries. For Bestway's usage pattern — moderate Customer table volume, infrequent Customer inserts, and frequent phone/email lookups from incoming calls and the Job Queue — the read performance improvement significantly outweighs the write cost. If the Customer table write volume changes substantially in the future, the indexes should be re-evaluated.

Existing Blank Customers

This fix prevents future blank-name customers but does not remediate existing ones. A follow-up data cleanup task should be performed to identify and update (or merge) Customer records with blank Names that were created by the API pages prior to this fix.

To identify affected records, query for Customers where:

  • Name = '' (blank)
  • Phone No. <> '' OR E-Mail <> ''
  • Created date falls within the period when v2.0.0.12 or v2.1.0.0 was deployed

Deployment Notes

Pre-Deployment

  • No data migration required
  • No manual configuration changes needed
  • No dependencies to update

Deployment

  1. Build the .app package via AL: Package in VS Code
  2. Deploy to the target environment via Extension Management or the admin center
  3. The extension upgrade is transparent — no upgrade codeunit is needed

Post-Deployment Verification

  1. Trigger an incoming call to the API endpoint — verify the returned URL points to a Customer with Name = 'New Customer' and the correct Phone No.
  2. While calls are being processed, edit and save a Customer Card — verify no "out of date" error
  3. Verify new API-created customers appear with the New Customer placeholder name
  4. Check the Nextiva Error Logs table for any new errors after deployment
  5. Monitor Application Insights telemetry for API page call success rates

Post-Deployment Cleanup

Review and update existing blank-name Customer records as described in Known Limitations above.


Testing

Refer to the extension's UAT test plan for full test coverage. The following scenarios are specific to this change:

#ScenarioExpected Result
1Incoming call with unknown phone numberNew Customer created with Name = 'New Customer', correct Phone No., and ECOMMERCE template defaults applied. URL returned points to the new Customer Card.
2Incoming call with known phone numberExisting Customer found. URL returned points to the existing Customer Card. No new Customer created.
3Incoming email with unknown email addressNew Customer created with Name = 'New Customer', correct E-Mail, and ECOMMERCE template defaults applied.
4Edit Customer Card during incoming callSave completes without "out of date" error.
5Create new Customer manually during incoming callInsert completes without lock conflict or error.
6Incoming call while Job Queue (CU-60003) is processingNo lock contention between the API page and the background job.
7Job Queue processes pending phone and email logsAll pending records are processed. SummaryUpdated, TranscriptUpdated, RecordingUpdated flags set correctly. Records with 3+ attempts are skipped.
8Verify Customer template defaults on API-created customerNew Customer has ECOMMERCE template defaults (posting groups, payment terms, etc.) AND the correct Phone No./E-Mail from the incoming call.
9Call-end event for known phone numberEndCall log record has Customer No. populated with the matching Customer's number.
10Call-end event for unknown phone numberEndCall log record is created with blank Customer No.. Job Queue processes the record using the PhoneNumber field.