Year-end collections forecasting
A code app over live finance data, with a campaign engine the business runs itself.
The situation
A 2,000-user law firm needed several hundred partners to enter net-collection estimates for the year-end push, against inventory figures that finance rebuilds nightly, with rollups for executives by department and practice group and a reminder campaign that tracked who had responded. It had been spreadsheets and email.
What was built
A Power Apps code app in React and TypeScript on the Power Apps SDK, reading and appending directly to an on-premises SQL Server through the data gateway. Three SharePoint lists hold campaign settings, a send log and a command queue. Two Power Automate flows do all the sending: a command flow that fires when the app writes a command row, and a daily engine that reads the settings and handles cadence reminders, the final-notice window and a weekday digest to leadership. Access is a firm-managed Entra security group; the mobile story is an Intune web-link icon, because code apps don't run in the mobile player. One source tree is deployed to three environments, with an environment chip in the header built from the folder's own config so nobody mistakes test for production.
Try it
A working replica of Collections Forecast, rebuilt from scratch with made-up people and numbers. Click around; nothing here touches a real system.
Stack
- Power Apps code app (React 19, TypeScript, Vite, Power Apps SDK)
- SQL Server via on-premises data gateway
- SharePoint Online
- Office 365 Users
- Power Automate (two flows)
- Office 365 Outlook
- Entra ID security group
- Intune managed web link
- Dev, Test and Prod environments, pay-as-you-go billing
What was hard, and what was done about it
The SQL connector is not a database driver
In this SDK version it pages silently, in practice caps filter trees at about 100 nodes, has no COUNT, GROUP BY or DISTINCT, works only with single-column keys, and its data-source binding refused one complex view. Every read walks a keyset. Long IN lists are chunked into ten ORed comparisons. Aggregation happens client-side. The estimates table got a surrogate identity key. The flows read that view through the same connector without trouble, so they handle the audience query.
Code apps cannot call a flow
At build time the SDK had no Power Automate connector and the app has no user token for Graph. A custom connector could wrap an HTTP-triggered flow, but that is one more endpoint to secure and one more component to deploy per environment, for one job. The pattern chosen: the app writes a command row to a list and a flow triggers on it. Every send in the system is a row first, which also gives an audit trail for free.
Nothing may leak while testing
Test mode is one setting. When it's on, every partner-facing send is redirected to the developer's inbox with a prefix and a cap of one. The campaign was rehearsed end to end against real data without a partner seeing it.
Workflow expressions have opinions
Numerics from SQL must be cast or comparisons silently fail. The leadership digest's 'new since last run' cut compared a local timestamp to UTC and missed records; the fix was explicit Eastern conversion and a 72-hour window on Mondays. Long expression-bearing HTML is kept in Compose actions, because the designer reflows it.
Phones
iOS Safari zooms on any control under 16px and never zooms back, and the desktop emulator won't show it. Inputs are 16px under pointer: coarse, layouts use 100dvh, and the home-screen icon is rendered from one master for both platforms' safe areas.
Knowing who actually uses it
The platform's analytics do not cover code apps by user or device, so the app logs its own usage to a SharePoint list: launch, landing, screen opened and estimate written, with device class, operating system, browser, viewport, how the person arrived (icon, email, digest, direct), the build stamp and the platform's own time-to-interactive. Labels only, never a figure. Writes are fire-and-forget and a failure is swallowed, so nothing on the partner path waits on the log. A dashboard in the app draws it as hand-built SVG, every chart with a table twin, and the feature is gated per environment by a flag so the same source stays silent in production until it is switched on.
Security and governance
Four tiers see four different slices: administrators, executives, department heads, and the people under them. Row access is derived from identity columns on the data itself, filtered server-side and confirmed on the client. The SQL connection is shared, so this is a visibility rule for low-sensitivity projection data, not a security boundary; anything that had to be enforced against a determined user would sit in Dataverse security roles or a per-role SQL view. An in-code admin allowlist outranks the role table. Recording an estimate on someone's behalf re-derives authority on every write and fails closed; previewing as a partner severs writes entirely. Every important write reads the row back by ID before the UI says it saved.
Outcome
The campaign system is live and administrators run it from an email center inside the app. Pay-as-you-go billing was proven with a controlled test before anyone committed: one unlicensed user, one launch per app, exactly the rows predicted on the next month's report.