Change Document: v2.2.0.0 — Customer Creation Bug Fixes & Query Optimizations
| Field | Value |
|---|---|
| Version | 2.2.0.0 |
| Date | 2026-03-04 |
| Extension | BC Dialing Application (Cambay Solutions) |
| Severity | Critical — blocking end-user workflow |
| Status | Implemented (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:
-
Insert()without triggers — Both API pages usedcust.Insert()instead ofcust.Insert(true), skipping the standard BC OnInsert trigger. This bypassed No. Series validation, default field population, and any event subscribers from other extensions. -
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 setsName := '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
| # | Severity | Category | Description |
|---|---|---|---|
| 1 | Critical | Fixed | Remove LockTable() from Pages 80000 and 80003 |
| 2 | High | Fixed | Use Insert(true) instead of Insert() for proper customer setup |
| 3 | Medium | Fixed | Set Name := 'New Customer' placeholder on API-created customers |
| 4 | Medium | Fixed | Separate read-only Customer lookup from creation path to prevent SetLoadFields bleed-through |
| 5 | Medium | Fixed | Customer No. field not populated on phone/email log records when an existing customer is found (Pages 80000 and 80003) |
| 6 | Medium | Fixed | Customer No. field not populated on EndCall log records (Page 80001) — FindFirst() return value was discarded |
| 7 | Medium | Fixed | Switch Job Queue queries to use ProcessingStatus secondary keys instead of overriding with primary key |
| 8 | Low | Fixed | Eliminate duplicate FindFirst() call in AddCustomerBlobLink |
| 9 | Low | Fixed | Add missing filter fields to SetLoadFields calls in LogError procedures |
| 10 | Low | Added | Secondary key ProcessingStatus on CustomerPhoneLog and CustomerEmailLog tables |
| 11 | Low | Added | Secondary indexes on Customer table for Phone No. and E-Mail lookups via table extension |
| 12 | Low | Added | SetLoadFields optimizations on read-only Customer lookups |
| 13 | Low | Changed | Re-apply phone/email after ApplyCustomerTemplate as defensive measure |
| 14 | Low | Changed | Remove unused variable declarations and dead code |
Detailed Changes
1. Remove LockTable() from Pages 80000 and 80003 (Critical)
Files:
src/Page/Pag-80000.ReceivePhoneNumber.alsrc/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.alsrc/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.alsrc/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.alsrc/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.alsrc/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.alsrc/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.alsrc/Page/Pag-80001.EndCall.alsrc/Page/Pag-80003.ReceiveEmail.alsrc/Codeunit/CU60003.NextivaIntegration.alsrc/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.alsrc/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:
PhoneNokey on"Phone No."— supports the phone number lookups used across Pages 80000, 80001, and Codeunits 60003, 60004EMailkey 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.alsrc/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 ID | Object Type | Name | Status |
|---|---|---|---|
| 60000 | Table Extension | CustomerDialerKeys | New — secondary indexes on Customer table |
| 60003 | Codeunit | Nextiva Integration | Modified |
| 60004 | Codeunit | Nextiva Integration Manual | Modified |
| 80000 | Page | ReceivePhoneNumber | Modified |
| 80001 | Page | EndCall | Modified — SetLoadFields, populate Customer No. on log record |
| 80003 | Page | ReceiveEmail | Modified |
| 80000 | Table | CustomerPhoneLog | Modified (new secondary key) |
| 80002 | Table | CustomerEmailLog | Modified (new secondary key) |
| — | app.json | Extension manifest | Modified (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:
- Two calls from the same phone number to arrive at the Azure Function within the same processing window
- Both to reach the BC OData endpoint before either completes the
Insert(true)call - 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. <> ''ORE-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
- Build the
.apppackage via AL: Package in VS Code - Deploy to the target environment via Extension Management or the admin center
- The extension upgrade is transparent — no upgrade codeunit is needed
Post-Deployment Verification
- 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. - While calls are being processed, edit and save a Customer Card — verify no "out of date" error
- Verify new API-created customers appear with the
New Customerplaceholder name - Check the
Nextiva Error Logstable for any new errors after deployment - 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:
| # | Scenario | Expected Result |
|---|---|---|
| 1 | Incoming call with unknown phone number | New Customer created with Name = 'New Customer', correct Phone No., and ECOMMERCE template defaults applied. URL returned points to the new Customer Card. |
| 2 | Incoming call with known phone number | Existing Customer found. URL returned points to the existing Customer Card. No new Customer created. |
| 3 | Incoming email with unknown email address | New Customer created with Name = 'New Customer', correct E-Mail, and ECOMMERCE template defaults applied. |
| 4 | Edit Customer Card during incoming call | Save completes without "out of date" error. |
| 5 | Create new Customer manually during incoming call | Insert completes without lock conflict or error. |
| 6 | Incoming call while Job Queue (CU-60003) is processing | No lock contention between the API page and the background job. |
| 7 | Job Queue processes pending phone and email logs | All pending records are processed. SummaryUpdated, TranscriptUpdated, RecordingUpdated flags set correctly. Records with 3+ attempts are skipped. |
| 8 | Verify Customer template defaults on API-created customer | New Customer has ECOMMERCE template defaults (posting groups, payment terms, etc.) AND the correct Phone No./E-Mail from the incoming call. |
| 9 | Call-end event for known phone number | EndCall log record has Customer No. populated with the matching Customer's number. |
| 10 | Call-end event for unknown phone number | EndCall log record is created with blank Customer No.. Job Queue processes the record using the PhoneNumber field. |