Skip to content

CRM: Billing Rules by Dashboard View

All four views query the same BQ table: DYNAMICS_DATA.Accounts_and_related_data (nested CRM snapshot). This document compares how each view handles the same billing concepts.


ViewFilterNotes
AM Portfolio (list/portfolio CY)o.stateCodeName != 'Canceled'Hard exclude. Cancelled orders never counted.
AM Portfolio (PY queries)o.stateCodeName != 'Canceled' OR ol.dateOfCancellation >= TIMESTAMP('{PY}-01-01')Softer - keeps recently cancelled to see PY churn.
Revenue Bridgeo.stateCodeName != 'Canceled' OR ol.dateOfCancellation >= TIMESTAMP('{PY}-12-01')Softer - includes cancelled if cancellation is recent (needed to track churn month).
Solution Portfolio (CY)o.stateCodeName != 'Canceled'Hard exclude for CY.
Solution Portfolio (PY)Omits filter entirelyMost permissive - captures churned accounts for PY comparison.
Company View (suspension)o.stateCodeName != 'Canceled'Hard exclude.

INCONSISTENCY: AM Portfolio PY uses {PY}-01-01 as cancellation cutoff, Revenue Bridge uses {PY}-12-01. These should probably be the same. Solution PY drops the filter entirely, which is the most permissive.

ViewFilter
AM Portfolio (CY)COALESCE(ol.statusName, '') != 'Canceled'
AM Portfolio (PY)COALESCE(ol.statusName, '') != 'Canceled' OR ol.dateOfCancellation >= TIMESTAMP('{PY}-01-01')
Revenue BridgeCOALESCE(ol.statusName, '') != 'Canceled' OR ol.dateOfCancellation >= TIMESTAMP('{PY}-12-01')
Solution Portfolio (CY)COALESCE(ol.statusName, '') != 'Canceled'
Solution Portfolio (PY)Omits filter entirely

Same pattern: Bridge is softer on line cancellation to track churn timing. Solution PY is most permissive.

ViewFilter
All viewso.signedDate IS NULL OR o.signedDate >= TIMESTAMP('2017-09-04')

CONSISTENT across all views. Excludes legacy pre-migration orders.

ViewFilter
All viewsCOALESCE(ol.product.productGroup.name, '') != 'DIR - PRINT'
All viewsol.product IS NOT NULL
All views (subs)COALESCE(ol.product.subscription, FALSE) = TRUE

CONSISTENT. DIR - PRINT always excluded, null products excluded, subscription flag respected.


ViewFilter
All viewsCOALESCE(ri.isDeposit, FALSE) = FALSE

CONSISTENT. Deposit RIs never count toward revenue.

ViewFilter
All views (except bundled)COALESCE(ri.totalLineItemAmount, 0) > 0 AND COALESCE(ri.invoicePeriod, 0) > 0
AM Portfolio (bundled PMS)COALESCE(ri.totalLineItemAmount, 0) = 0 AND COALESCE(ri.invoicePeriod, 0) > 0

CONSISTENT. Zero-amount RIs excluded from revenue (except bundled PMS tracking in AM Portfolio).

This is where the biggest differences are.

ViewCY Active FilterPY Active Filter
AM Portfolio (best_ri)ri.expireOn IS NULL OR ri.expireOn > CURRENT_TIMESTAMP()ri.firstInvoiceOn <= TIMESTAMP('{sameDatePY}') AND (ri.expireOn IS NULL OR ri.expireOn > TIMESTAMP('{sameDatePY}'))
AM Portfolio (active_lines)ri.firstInvoiceOn <= CURRENT_TIMESTAMP() AND (ri.expireOn IS NULL OR ri.expireOn > CURRENT_TIMESTAMP())Same pattern with PY date
Revenue Bridgeri.firstInvoiceOn < TIMESTAMP('{CY+1}-01-01') AND (ri.expireOn IS NULL OR ri.expireOn > TIMESTAMP('{PY}-12-01'))N/A (wide window, monthly filtering in SQL)
Solution Portfolio (CY)ri.nextInvoiceOn >= NOW() AND (ri.expireOn IS NULL OR ri.expireOn > NOW()) + suspension checkri.firstInvoiceOn <= Dec 31 of year AND (ri.expireOn IS NULL OR ri.expireOn > Jan 1 of year)
Company View (suspension)ri.expireOn IS NULL OR ri.expireOn > CURRENT_TIMESTAMP() + suspendTill > CURRENT_TIMESTAMP()N/A

KEY DIFFERENCES:

  • AM Portfolio best_ri does NOT check firstInvoiceOn for CY (relies on ROW_NUMBER to pick best RI). This means future-dated RIs that have not started billing yet WILL be included.
  • Revenue Bridge uses the widest window (firstInvoiceOn < next year start, expireOn > prior Dec) then does monthly filtering in SQL with CROSS JOIN months.
  • Solution Portfolio CY uses nextInvoiceOn >= NOW() which is stricter than AM Portfolio - it requires the RI to have a future invoice scheduled.
  • AM Portfolio active_lines adds firstInvoiceOn <= NOW() which best_ri does NOT have.

Each billing group can have multiple RIs. All views pick “the best one”:

ViewPARTITION BYORDER BY
AM Portfolio (best_ri)bg.idIF(ri.nextInvoiceOn >= NOW(), 0, 1), ri.createdOn DESC
AM Portfolio (py_best_ri)bg.idri.createdOn DESC
Revenue Bridge (bg_ri)bg.id, ol.salesOrderDetailIdCY: IF(ri.nextInvoiceOn >= NOW(), 0, 1), ri.createdOn DESC; Historical: ri.createdOn DESC
Solution Portfoliobg.idSame pattern as AM Portfolio

KEY DIFFERENCE: Revenue Bridge partitions by (bgId, salesOrderDetailId) - meaning it picks one RI per BG per order line. AM Portfolio and Solution partition by bgId alone - one RI per BG regardless of how many lines share that BG.

Impact: If a BG has 3 order lines, Revenue Bridge gets 3 rows (one per line), AM Portfolio gets 1 row (for the whole BG). Revenue Bridge then distributes the RI amount across lines via proportional split, so the math should converge, but the intermediate structures differ.


ViewMethod
AM Portfolio (list)riAmount / riPeriod - simple BG-level, no line allocation
AM Portfolio (portfolio)riAmount * (ol.baseAmount / totalBase) / riPeriod - proportional split per line
Revenue BridgeriAmount * (baseAmount / totalBase) / riPeriod - proportional split per line, with lineCount fallback
Solution PortfolioSame proportional split

INCONSISTENCY: AM Portfolio LIST mode uses simple riAmount / riPeriod per BG. This is fine for AM-level totals (one BG = one value), but the PORTFOLIO mode uses the proportional split to show per-account product breakdowns. The two modes may not agree at the margins due to rounding.


ViewSuspended =Active =
AM PortfoliosuspendTill IS NOT NULL AND suspendTill > NOW()nextInvoiceOn IS NOT NULL AND nextInvoiceOn >= NOW()
Revenue BridgesuspendTill captured but monthly filtering is by firstInvoiceOn/expireOn/dateOfCancellation - suspension NOT explicitly filtered in SQL
Solution PortfoliosuspendTill > cutoff as separate bridge category
Company ViewsuspendTill > NOW() (dedicated suspension query)

INCONSISTENCY: Revenue Bridge does NOT exclude suspended accounts from “active” MRR per month. If a BG is suspended but its RI window covers the month, it appears as active revenue. The bridge JS may handle this, but the SQL does not.

AM Portfolio separates suspended MRR from active MRR cleanly at the SQL level.


ViewMethod
All viewsol.product.setup = TRUE OR product.name LIKE '%setup%/%set up%/%set-up%' OR product.number IN ('site st', 'sitepro multi')

CONSISTENT across all views.

ViewYear Filter
AM PortfolioComplex 3-way OR: (1) fromDate/toDate overlap with CY + has non-deposit RI, (2) no dates + RI firstInvoiceOn in CY, (3) no RI + signedDate in CY
Revenue Bridgeri.firstInvoiceOn in selected year (simpler)
Solution PortfolioSame 3-way OR as AM Portfolio (prorated by month overlap)

INCONSISTENCY: Revenue Bridge uses a simpler filter (just firstInvoiceOn in year). AM Portfolio and Solution have the full 3-way OR that handles edge cases (lines with service periods but no RI, lines with RI but no dates).

Impact: Revenue Bridge may miss setup/DEA lines that have fromDate/toDate spanning the year but whose RI firstInvoiceOn is in a different year.

ViewMethod
AM PortfolioProrated: baseAmount * (months_in_year / total_months) using SAFE_DIVIDE with DATE_DIFF
Revenue BridgeNo proration - full baseAmount attributed to the month of firstInvoiceOn
Solution PortfolioSame proration as AM Portfolio

INCONSISTENCY: A 24-month setup fee of E2,400 spanning 2025-2026 would show as:

  • AM Portfolio: E1,200 (12/24 months in CY)
  • Revenue Bridge: E2,400 (full amount in the firstInvoiceOn month)
  • Solution Portfolio: E1,200 (prorated)

ViewPY MRR Snapshot DateMethod
AM Portfolio{sameDatePY} (same month/day last year)Point-in-time: RI active on that exact date
Revenue BridgeDec of prior yearOpening balance = Dec PY monthly values
Solution PortfolioCY: same month/day PY; Historical: full year windowDepends on whether selected year is current

INCONSISTENCY: AM Portfolio compares to same-day-last-year, Revenue Bridge compares to Dec 31 prior year. For an AM looking at March 23 2026:

  • AM Portfolio PY MRR = what was active on March 23 2025
  • Revenue Bridge Opening = what was active in Dec 2025

ViewUses bg_replacement_proxy?How?
AM PortfolioYesPY MRR and PY active lines include proxy rows via UNION ALL
Revenue BridgeYesSynthetic Dec PY rows injected in JS for opening balance
Solution PortfolioYesPY comparison includes proxy via UNION ALL
Company ViewNoOnly shows current suspended BGs

CONSISTENT (except Company View which does not need PY comparison).


#IssueImpactPriority
1Cancellation cutoff dates differ: AM PY uses {PY}-01-01, Bridge uses {PY}-12-01, Solution PY drops filterCould count/miss recently cancelled accounts differentlyMedium
2AM best_ri does not check firstInvoiceOn for CYFuture-dated RIs (not yet billing) included in current MRRHigh
3Revenue Bridge does not filter suspension in SQLSuspended accounts appear as “active” revenue in bridge monthsHigh
4Setup/DEA proration: Bridge does not prorate, AM Portfolio and Solution doSetup revenue can be 2x in Bridge vs AM Portfolio for multi-year setup feesMedium
5Setup/DEA year filter: Bridge uses simple firstInvoiceOn, others use 3-way ORBridge may miss setup lines with service periods in CY but RI in PYLow
6PY snapshot date: AM uses same-day-PY, Bridge uses Dec PYYoY comparison means different things in each viewLow (by design)
7ROW_NUMBER partition: Bridge uses (bgId, lineId), AM uses (bgId) onlyShould not affect totals but creates different intermediate structuresLow

10. Shared Rules (Canonical - should be the same everywhere)

Section titled “10. Shared Rules (Canonical - should be the same everywhere)”

These are the “golden rules” that should be identical across all views:

1. Exclude DIR - PRINT product group
2. Exclude null products
3. Exclude orders signed before 2017-09-04
4. Exclude deposit RIs (ri.isDeposit = TRUE)
5. Exclude zero-amount RIs (ri.totalLineItemAmount = 0)
6. Exclude zero-period RIs (ri.invoicePeriod = 0)
7. MRR = riAmount / riPeriod (per BG), allocated by baseAmount proportion across lines
8. Setup = product.setup flag OR name pattern matching
9. DEA = non-subscription, non-setup one-time fees
10. BG Replacement: use bg_replacement_proxy for PY comparison

ViewSQL LocationRuntime
AM Portfolioworker/src/handlers/am-portfolio.js (lines 27-845)Cloudflare Worker
Revenue Bridgeworker/src/handlers/revenue-bridge.jsCloudflare Worker
Solution Portfolion8n-workflows/dashboard-solution-portfolio.json (Build Query Code node)n8n (Worker falls back)
Company Viewsrc/screens/CompanyView.jsx (inline SQL, lines 363-440)Frontend (BQ execute)
Ask the docsRAG over this site
Ask anything about the FCR Dashboard platform — architecture, BigQuery, the worker routes, billing rules, the LRC stack, scoring… Answers are grounded in this documentation, with source links.
How does the deal-brief refresh work? Which routes are Worker vs n8n? How is account health scored?