Discount Approval Evaluation and Event Publication: Workbook
Business context and architecture | Download package | Implementation
Show sellers which proposals need manager or executive review, and notify the review team. Connect Eventstream to make the same review requirements available for further processing. This exercise requests review; it does not approve a discount.
Resources and coverage
| Component | Default name | Package coverage |
|---|---|---|
| Event schema and delegated Publish/Consume permissions | DiscountSchema |
Public preview; exact nine-field Sales.Deals.DiscountApprovalRequired contract |
| User Data Function publisher | DiscountPublisher |
Deployed with the native Business Event connection and seven typed proposal parameters |
| Activator (Teams) | DiscountActions |
Event subscription and active Notify discount review rule; four dynamic notification fields |
| Eventstream | DiscountRouting |
Create and connect manually before testing |
| Independent managed event history | Not configured by this package | Creation-time option; see Optional event history |
| Companion browser application | Not configured by this package | Optional source experience from the application sample; the package test invokes the same native UDF |
The package prepares three Fabric items. Connect Eventstream in Step 3; this connection is a manual step because the deployment API does not expose Business Events as a source.
Before you start
Use Python 3.10+, Azure CLI and PowerShell on Windows, an active supported Fabric capacity, and permission to create the scenario's workspace items and execute its source. Prepare any learner-owned connections listed in the coverage table.
1. Configure your environment
Download discount-approval-routing.zip or the repository ZIP, extract it, and open its cicd\discount-approval-routing folder.
Replace the three placeholders below:
Set-Location '<extracted-folder>\cicd\discount-approval-routing'
$tenantId = '<your-tenant-uuid>'
$workspaceId = '<your-workspace-uuid>'
az login --tenant $tenantId
python .\deploy.py configure --tenant-id $tenantId --workspace-id $workspaceId --auth-provider azure_cli --output .\config.local.json
Review the tenant, workspace and unused resource names in config.local.json. accept_preview_apis defaults to true; set it to false if you do not accept preview APIs, which prevents deployment. Keep the schema name between 3 and 20 characters because it is also the native UDF connection alias.
Use Python 3.10 or later, Azure CLI, and a delegated user with access to an active Fabric capacity. teams_recipients: ["@me"] sends the notification to the same account as Azure CLI. Configuration does not authenticate, create resources or overwrite an existing local configuration.
2. Deploy to Fabric
Open your workspace and find DiscountSchema, DiscountPublisher and DiscountActions. The review notification is prepared. Continue with the Eventstream connection before submitting sample proposals.
3. Connect Eventstream before testing
- In Real-Time Hub, open
Sales.Deals.DiscountApprovalRequiredunderDiscountSchemain the configured workspace. - Select Add consumer, then Eventstream.
- Select the same workspace and name the Eventstream
DiscountRouting, or use your configured name. - Review the Business Event source and select Connect. Do not substitute a custom endpoint source.
- Open the Eventstream and confirm the source uses the correct schema set and event, the default stream is running, and Data preview is available.
- Open
DiscountActions. ConfirmNotify discount reviewuses On each value, its Teams action targets your configured recipient, and it includesdeal_id,approval_level,approval_reasonandregion.
No history association or Eventhouse query is required for this package's response. See Optional event history for the creation-time conditions.
4. Evaluate proposals and inspect delivery
Run the sample once to evaluate eight proposals. Two should stay within policy and need no notification. The other six should reach both Teams and Eventstream: two for manager review and four for executive review.
| Proposal | Discount % | Margin % | Expected level | Publication requested |
|---|---|---|---|---|
| Within policy | 5 | 30 | none |
No |
| Seller boundary | 10 | 25 | none |
No |
| Manager lower boundary | 10.01 | 25 | manager |
Yes |
| Manager upper boundary | 15 | 25 | manager |
Yes |
| Executive discount | 15.01 | 25 | executive |
Yes |
| Executive margin | 10 | 24.99 | executive |
Yes |
| Executive precedence | 12 | 24 | executive |
Yes |
| Both executive reasons | 20 | 20 | executive |
Yes |
Evaluation criteria
- Keep the eight deal IDs returned by the sample. Use
source_run_idto find this exercise's proposals and compare their review levels with the table. - Inspect six individual Teams notifications for those IDs: two manager and four executive review requirements. Compare all four dynamic context fields.
- In
DiscountRoutingData preview, find the same six proposals. Check their deal details,approval_level,approval_reasonandevent_timeagainst the sample. - Confirm the two within-policy IDs are absent from notifications and event payloads. Exactly 10% discount with 25% margin stays within policy; exactly 15% requires manager review; a margin below 25% takes executive precedence.
- Match results by deal ID so that a repeated delivery is not mistaken for another proposal. Review is still required; none of these notifications is an approval decision.
The expected outcome is six proposals ready for review in both Teams and Eventstream, with no notification for the two proposals within policy.
Optional application integration
The main exercise uses the prepared sample, not the browser application. If connecting your own sales application, use the already deployed function and the following adapter reference.
Connect the sales application
The companion sample contains the browser app and Python proxy.
The browser sends the demonstrated snake_case request:
{
"deal_id": "DEAL-2048",
"account_name": "Sample Health",
"region": "West",
"deal_value": 450000.0,
"requested_discount_percent": 18.0,
"estimated_margin_percent": 22.0,
"event_time": "2026-09-15T17:15:00Z"
}
The proxy maps it explicitly to the exact UDF parameters:
| Application field | UDF parameter |
|---|---|
deal_id |
dealId |
account_name |
accountName |
region |
region |
deal_value |
dealValue |
requested_discount_percent |
requestedDiscountPercent |
estimated_margin_percent |
estimatedMarginPercent |
event_time |
eventTime |
Publish the UDF, enable its public URL, and grant the developer Execute permission. Then authenticate with Azure CLI and start the sample:
The public UDF URL is supplied only at startup. It is not stored in browser code or committed. The proxy acquires a delegated development token server-side for:
Azure CLI delegated authentication is the demonstrated developer path. It is not a production identity design. Production hosting must use an approved, configurable identity and token lifecycle. Tokens must never be logged or exposed to the browser.
The proxy uses the demonstrated 120-second timeout by default. Use --timeout to configure it. If token acquisition or Fabric invocation fails, the proxy returns a clear error and does not fall back to local simulation.
Understand the evaluation result
The application shows the proposal's approval_level and message. It labels event_published as publication_requested: the review requirement has been submitted, while delivery to the review team is checked separately.
The adapter displays errors instead of a simulated policy result when Fabric cannot complete the evaluation. It accepts a successful output as a JSON object or string, requires the three result fields, and retains an optional invocationId as invocation_id for support. See the Python proxy for response handling.
Local simulation is available in the sample README, but does not publish events or exercise this flow.
Optional event history
The dashed branch in the business architecture is an extension, not another completion requirement. Choose Analyze in Eventhouse and its destination during Business Event creation if you are configuring an event natively with managed history. The package does not configure this association. Skip these queries for its deployed event; this Workbook does not provide a supported post-creation association procedure.
A source Eventhouse, a same-name table or a direct table write is not a managed Business Event subscription. History records received findings, not completed actions. Received-record counts can include repeated publications or deliveries and do not describe the full source population.
The following queries apply only to that creation-time history destination. Check delivered notifications or saved business outcomes separately. Keep currencies separate when comparing amounts.
History queries
See Business Events and Eventhouse.
Implementation reference
Expand the details you need to understand or adapt the solution. This is reference, not a second setup tutorial. The package's default schema-set name is DiscountSchema. Standalone examples may use the original illustrative alias; match the connection alias to your configured schema set before reusing code. Event names and payload fields remain unchanged.
Event contract
The shared event fields are unchanged. The package prepares this contract; inspect it rather than creating a second event.
{
"type": "record",
"name": "Sales.Deals.DiscountApprovalRequired",
"fields": [
{
"name": "deal_id",
"type": "string",
"doc": "Unique identifier of the proposal"
},
{
"name": "account_name",
"type": "string",
"doc": "Customer account associated with the proposal"
},
{
"name": "region",
"type": "string",
"doc": "Sales region responsible for the proposal"
},
{
"name": "deal_value",
"type": "double",
"doc": "Total commercial value of the proposal"
},
{
"name": "requested_discount_percent",
"type": "double",
"doc": "Discount percentage requested by the seller"
},
{
"name": "estimated_margin_percent",
"type": "double",
"doc": "Estimated margin after applying the requested discount"
},
{
"name": "approval_level",
"type": "string",
"doc": "Required approval authority: manager or executive"
},
{
"name": "approval_reason",
"type": "string",
"doc": "Discount rule that triggered the approval request"
},
{
"name": "event_time",
"type": "string",
"doc": "UTC time when the proposal was evaluated, ISO 8601 format"
}
]
}
Source and publisher reference
These snippets explain the source and publication logic. Use the prepared publisher for the main exercise; standalone examples are not an additional source run.
return {
"approval_level": approval_level,
"event_published": True,
"message": (
f"Deal {dealId} requires {approval_level} approval. "
"Business Event published."
),
}
| Path | Condition | Synchronous result | Business Event |
|---|---|---|---|
| Within policy | Discount is at most 10% and margin is at least 25% | approval_level: none |
Not published |
| Manager approval | Discount is greater than 10% and at most 15%, and margin is at least 25% | approval_level: manager |
Publication requested |
| Executive approval | Discount is greater than 15% or margin is below 25% | approval_level: executive |
Publication requested |
Payload and inspection reference
These individual examples explain payloads and inspection. They are not another test dataset or a second set of expected results; use the complete sample in the main steps.
| Proposal | Discount % | Margin % | Expected level | Expected event |
|---|---|---|---|---|
| Within policy | 5 | 30 | none |
None |
| Seller boundary | 10 | 25 | none |
None |
| Manager lower boundary | 10.01 | 25 | manager |
One |
| Manager upper boundary | 15 | 25 | manager |
One |
| Executive discount | 15.01 | 25 | executive |
One |
| Executive margin | 10 | 24.99 | executive |
One |
| Executive precedence | 12 | 24 | executive |
One |
| Both executive reasons | 20 | 20 | executive |
One |
Further reading
- companion application sample
- Real-Time Hub, Business Events, Create
discount-approval-event-schema.avscevaluate_discount_request.py- UDF publisher documentation
- Eventstream overview
- schema versioning guidance
- retry guidance
What are you building?
Ask a question, share your scenario, or help shape the next developer guide.
Join the conversation on Reddit →