PL-600 study materials
PL-600 study resources

Litware - Quote Approval Performance and Enforcement

PL-600 · Put your knowledge to work in context.

All case studies

Litware - Quote Approval Performance and Enforcement

Litware's "Quote Approval" model-driven app stores 900,000 quote line rows. A synchronous pre-operation plug-in on Quote recalculates the total by retrieving every child line; saves now take 8–20 seconds and users intermittently get a generic error. A form-scoped business rule clears Discount Reason when Discount % is zero, but data imported through the API keeps both populated. Approvals are created from a canvas app and from the model-driven app, and duplicates appear when a user double-submits. The Open Quotes view, filtered on a calculated column Days Open, fails for large result sets. Finance needs a rolling 12-month report over all lines.

1. Fix the save-time recalculation without losing correctness.

Read the worked answer

The plug-in retrieves every child row on every save, so cost grows with the largest quote, not with the change. Three viable designs:

  1. Delta maintenance (recommended for an authoritative total). Post-operation plug-in on Quote Line create/update/delete that increments or decrements the parent total. It touches one row, so it stays fast. Guard against lost updates with optimistic concurrency (ConcurrencyBehavior) on the parent update, and make the handler retry-safe because the platform may retry.
  2. Rollup column. System-maintained and asynchronous - recalculated on a schedule (hourly by default). Correct for reporting, wrong if approvals need a real-time total. Also note rollup columns are capped per organisation, so don't design dozens.
  3. Asynchronous recalculation with a visible pending state. Acceptable if the business will tolerate a short lag; pair it with a reconciliation job.

Also remove the sync plug-in from the critical path entirely, and confirm the retrieval used aggregates rather than loading rows. Even after the fix, keep a nightly reconciliation to repair drift, since plug-ins can be bypassed by bulk operations or maintenance.

2. Fix the discount rule so it is enforced, and stop the duplicate approvals.

Read the worked answer

Discount rule: a form-scoped business rule runs only in that form. API imports, plug-ins and bulk updates bypass it, which is exactly the symptom observed. Raise the rule's scope to Entity so it executes on the server, or - if the business needs to block invalid saves rather than clean them up - implement a synchronous pre-validation plug-in that throws a clear error, and keep a form-scoped rule purely for UX. Prefer the plug-in for anything that must be true regardless of client.

Add a data-quality check over existing rows, because the bypass has been running for months.

Duplicate approvals: UI-level disabling of the button is necessary but insufficient - two clients race. Add an alternate key on Approval (e.g., quote ID + revision) so the second create fails deterministically, and handle the duplicate-detected response in both the canvas app and the model-driven app with a friendly message and a re-read. Consider optimistic concurrency on Quote so a stale submit is rejected. Note that an alternate key enforces uniqueness only after the index build completes, so run it during a quiet window and validate existing data first.

3. Resolve the failing view and give Finance its 12-month report.

Read the worked answer

The view: filtering on a calculated column is the problem - calculated columns are computed at read time and cannot be indexed, so the query scans and the result set exceeds what a single retrieve can page through efficiently. Replace it with a filter on an indexed, persisted column: maintain a real Days Open column via a daily flow or the delta plug-in, or filter on a relative condition against an indexed date column (which also survives the passage of time without recalculation). Keep the default view returning a bounded set, and push breadth into a chart or report rather than the grid.

The report: do not serve 900k rows through the app or through a paged Dataverse retrieve. Use a reporting layer - Power BI with DirectQuery or import over the analytical endpoint / Fabric link - and define the aggregation there. Confirm column security implications if Margin is in the report, and use long-term retention or archival for closed quotes so the operational tables stay small.

Finally, set expectations with Finance on refresh latency: near real-time reporting on a transactional store is what created the original performance problem, so decide the freshness SLA explicitly rather than inheriting it.