Solution design draft for discussion, 20 October 2026

Telecare price change, synced to Business Central without the three-week watch

Today a daily batch re-prices MSD subscriptions for two weeks, resends each order to FIS, and someone watches it until go-live. The proposal: record the price change once, apply it to each weekly, fortnightly and monthly subscription just before its first new-price bill, and send Business Central one acknowledged message per subscription.

Today vs proposed

Today Pricebook changed by hand Daily batch finds MSD by product name runs every day for about two weeks Clone schedule and BT cancel old, update order line Resend whole order Order V2, one at a time FIS billing no acknowledgement Success is checked by watching the sync for about three weeks. Proposed Price Change one approved record Preview, once one line per subscription sign-off Apply, line by line just before each new bill Outbox, one per line AIS price-update topics Business Central acknowledgement and BC reference Then automatic checks: expected = applied = acknowledged, and nothing left at the old price.
The daily loop and the whole-order resend go away. Affected subscriptions are found once from their billing schedules, the price is applied once, and every line waits for Business Central to confirm it.
Why today needs watching
  • Each schedule holds only one Upcoming bill, and those appear as invoice dates roll on, so affected orders surface a few at a time and the batch has to run daily.
  • The FIS sync resends the whole order, one at a time, and nothing confirms it arrived.
  • The product is found by name (LIKE '%MSD%'), the date sits in a Custom Label, the order totals aren't updated, and nothing records what changed.
What the proposal changes
  • A price change is a business record covering every product it re-prices, with an approval. It updates the pricebook itself, so nobody edits prices one by one.
  • Every affected subscription is listed two weeks ahead, so Finance signs off on a number.
  • Each subscription switches at the start of its own billing period, whatever its frequency.
  • Business Central gets small, repeat-safe messages and confirms each one.

Timeline

19/0621/0623/06 25/0627/0629/06 01/0703/0705/07 07/0709/07 Price change Go-live Today Pricebook changed by hand Daily batch: re-price, resend to FIS Watch that FIS got every order Business and Finance checks Proposed Approve, update pricebook Preview and Finance sign-off Apply and send, as each comes due Reconcile (automatic) then first-bill checks per frequency
Dates follow the MSD example (price change 19/06, go-live 03/07). Apply works through the lines from go-live, each just before its first new-price bill (up to about a month for monthly customers). Alongside, automatic checks run when each frequency's first new-price bill is due (about one, two and four weeks later). Nobody has to watch.
Today: job or manual step Today: monitoring Proposed: system step

How it works, step by step

Six steps, each shown as the screen Finance or the system would see. Product names and today's prices are real; counts, new prices and order numbers are samples. Nobody edits the pricebook by hand: the Price Change does it.

1

Create the price change and set the new prices

About three weeks before go-live. Finance.
You
Create a Price Change, set the go-live date and click Add MSD products. Type one uplift (for example 4%) and click Fill new prices, then override any row that needs a different price.
The system
Adds every product funded by MSD, inactive ones included, with today's price from the pricebook. Fill sets each weekly price and works out fortnightly (× 2) and monthly (× 52 ÷ 12) from it, so the three always agree.
You check
Every row shows a tick. A red row means a typed price doesn't agree with the others in its group.

Records

CreatedPrice_Change__c × 1: Status Draft; Description__c, Funder__c (MSD), Effective_Date__c, Price_Rule__c
CreatedPrice_Change_Product__c × 26, one per product: Product__c, Product_Is_Active__c, Billing_Frequency__c, Price_Group__c, Old_Price_Incl/Excl_GST__c (from the Standard Price Book entry)
UpdatedPrice_Change_Product__c × 26 by Fill: New_Price_Incl_GST__c, New_Price_Excl_GST__c, Price_Check__c

Logic

  • Products = Funder MSD (ProductController.getProductsByDefinitionAttributes), active or not. MSD-named products without that funder count in Missing_Products__c.
  • Fill: weekly = old × (1 + uplift), to the cent; fortnightly = 2 × weekly; monthly = weekly × 52 ÷ 12; excl GST = incl ÷ 1.15 (4 dp).
  • Price_Check__c = OK when the row matches its group's weekly price (± 1 cent) and new ≠ old.
  • Submit for approval is blocked until every row is OK and missing products = 0.
New Price ChangeDraft
Products (26)Add MSD productsUplift4.0%Fill new prices
ProductBillsOldNewPer weekCheck
Medical Alarm - MSD Funded - WeeklyWeekly$17.27$17.96$17.96✓
Medical Alarm - MSD Funded - FortnightlyFortnightly$34.54$35.29$17.65✗ typed over; 2 × weekly is $35.92
Medical Alarm - MSD Funded InAdvance - MonthlyMonthly$74.84$77.83$17.96✓
MSD Anywhere - Fortnightly inactive, still billingFortnightly$51.14$53.18$26.59✓
Securely - MSD Home - MonthlyMonthly$74.84$77.83$17.96✓
… 21 more products
1 price needs fixing before this can be sent for approval.
2

Approve and update the pricebook

Same day. Finance manager approves; then one click.
You
Approve, then click Update Pricebook.
The system
Writes every new price to its product's Standard Price Book entry in one go: all 26 or none. Orders placed from now on are sold at the new price. Existing customers don't change until go-live.
You check
26 of 26 entries updated. Until go-live, Restore Old Prices puts them all back if the change is cancelled.

Records

UpdatedPrice_Change__c Status Approved; Approved_By__c, Approved_On__c (approval process)
UpdatedPricebookEntry × 26 (Standard Price Book): UnitPrice_GST_Inclusive__c = new incl; UnitPrice = new excl. Price_Excl_GST__c is a formula and follows.
UpdatedPrice_Change__c Pricebook_Update_Date__c; Status Pricebook Updated

Logic

  • All 26 entries are written in one transaction: all or none.
  • Restore Old Prices writes the Old_Price_* values back and clears Pricebook_Update_Date__c. It's allowed until the first line is applied.
PC-0007, MSD price change July 2026Approved by Finance manager
  1. Draft
  2. Approved
  3. Pricebook Updated
  4. Previewed
  5. Applied
  6. Synced
  7. Closed
✓ 26 of 26 pricebook entries updated. New orders from today use the new prices.
ProductPricebook beforePricebook now
Medical Alarm - MSD Funded - Weekly$17.27$17.96
Medical Alarm - MSD Funded - Fortnightly$34.54$35.92
Medical Alarm - MSD Funded InAdvance - Monthly$74.84$77.83
… 23 more
Writes the price incl GST and the price excl GST (÷ 1.15) on each entry.Restore Old Prices
3

Preview who changes, and sign off

About two weeks before go-live. Can be rerun until sign-off.
You
Click Run Preview, review, then Sign off.
The system
Creates one Price Change Line for every live subscription to these products. Separately it counts the live billing schedules for each product. The two counts must match in every cell.
You check
The grid is all ticks, and every exception has an answer.

Records

UpsertPrice_Change_Line__c one per live billing schedule (End Date blank): Price_Change_Product__c, Account__c, Order__c, Order_Product__c (the schedule's own line), Old_Billing_Schedule__c, Old_/New_Amount__c, Item_Prices__c, Discount_Amount__c, Billing_Frequency__c, Billing_Type__c, Next_Billing_Date__c, Cutover_Date__c, Apply_Date__c, Idempotency_Key__c. Apply Status Pending (or Skipped), Sync Status Not Sent.
UpdatedPrice_Change_Product__c Live_Order_Lines__c, Order_Lines_Covered__c
UpdatedPrice_Change__c Missing_Products__c; Status Previewed; at sign-off Preview_Signed_Off_By__c / _On__c

Logic

  • Schedules: on an Activated order with an in-scope product, Charge Type Recurring, started, End Date blank. An order with several schedules contributes only the one with a blank End Date.
  • New amount = the sum of new price × quantity for every in-scope order line the schedule bills: its own line, plus in-scope lines on the same order that have no schedule of their own (MSD peripherals).
  • Cutover = the first billing-period start under the price rule, counted from Billing_Start_Date__c by frequency.
  • Apply date = the cutover date; for in-advance billing, the day before the first new-price invoice; go-live day if not billed yet.
  • Old amount = new amount → Skipped ("already at the new price"). Old amount ≠ the sum of its lines' old prices → flagged for Finance. The upsert key is Price Change + Billing Schedule, so a rerun never duplicates lines.
Preview, PC-0007Run 19 Jun, 10:02
4,315Live schedules
4,315Lines created
26Products
0Missing products
Product groupWeeklyFortnightlyMonthly
Medical Alarm172 ✓2,751 ✓7 ✓
Medical Alarm Two Pendant4 ✓111 ✓none
MSD Anywhere (incl. old product codes)103 ✓945 ✓none
Securely Home19 ✓186 ✓1 ✓
Add-ons (mobile, extra pendant)1 ✓15 ✓none
Total2994,0088
  • !3 subscriptions are on a custom amount, not the list price. Finance confirms each one.
  • ✓12 are already at the new price (orders placed since the pricebook update). Their lines are marked Skipped.
  • ✓2 customers on a billing holiday are included; they pay the new price when billing resumes.
  • !1 schedule has a future End Date. It isn't picked (only blank End Dates are), so it's listed for Finance.
  • !2 schedules don't match their order lines (amount ≠ base + peripherals). They're listed for Finance to confirm.
Lines are linked to PC-0007 and can be opened in a report.Sign off preview
4

Apply, each subscription just before its first new-price bill

Daily from go-live, early each morning before the billing run, automatically.
The system
Takes the lines that are due today. For each, it re-checks the subscription (a cancelled order is skipped with a reason).
  • Already billing: ends the old billing schedule the day before the cutover and starts a new one at the new price.
  • Not billed yet: re-prices the existing schedule in place.
Either way it also updates the order line, the order totals and the first new-price bill. It runs until the last line is applied, up to about a month for monthly customers.
You check
Errors = 0. Each subscription keeps its own billing day, and only ever has one current schedule.

Records

UpdatedBilling_Schedule__c (old, split): Billing_End_Date__c = cutover − 1
CreatedBilling_Schedule__c (new, split): copy of the old one; Billing_Start_Date__c = cutover, First_Payment_Date__c, Amount_to_be_billed__c = new price
CancelledBilling_Transaction__c (old schedule): Upcoming bill whose period starts on or after the cutover, if one exists
CreatedBilling_Transaction__c (new schedule): first period from the cutover; Rate__c, Total/GST/List/Discount _Value__c; Upcoming
UpdatedBilling_Schedule__c + Billing_Transaction__c (in place, not billed yet): Amount_to_be_billed__c; the Upcoming bill's Rate__c and _Value__c fields
UpdatedOrderItem UnitPrice, LineGSTAmount__c, LineTotalAmount__c
UpdatedOrder OrderSubTotal / TotalGST / Total / Discount amounts, recalculated from all its lines
UpdatedPrice_Change_Line__c New_Billing_Schedule__c, Apply_Method__c, Apply_Status__c, Applied_On__c (or Skip_Reason__c / Last_Error__c)

Logic

  • Work list: Pending lines with Apply_Date__c ≤ today, ordered by order. Runs before the billing run.
  • Re-check: the order is Activated, the schedule is live, the product is unchanged, the amount isn't already the new price, and the pricebook equals the product row.
  • Split if the schedule has a Submitted or Invoiced bill; otherwise re-price in place.
  • Sweep: any live in-scope schedule without a line gets a late line.
  • All or nothing per line: on failure that line rolls back and shows Error.
Apply, PC-0007Run of Wed 8 Jul, 05:30
1,950Due so far
1,935Applied
15Skipped, with reason
0Errors
2,365Still to come (last 20 Jul)
Sample subscriptionApply dateWhat changesFirst bill at new price
Weekly, in arrears, cycle starts TuesdayTue 7 JulOld schedule ends Mon 6 Jul; new from Tue 7 JulMon 13 Jul, $17.96
Fortnightly, in arrears, cycle started Wed 1 JulWed 15 JulOld schedule ends Tue 14 Jul; new from Wed 15 JulTue 28 Jul, $35.92
Monthly, in advance, on the 20thSun 19 JulOld schedule ends Sun 19 Jul; new from Mon 20 JulMon 20 Jul, $77.83
New order at the old price, first payment Fri 10 JulFri 3 JulNot billed yet: same schedule, re-priced in placeFri 10 Jul, $35.92
Skipped so far: 12 already at the new price, 2 orders cancelled since the preview, 1 moved to a non-MSD product.
5

Send each change to Business Central

Straight after each line is applied, automatically.
The system
As each line is applied, sends it to Business Central through AIS: which subscription, the new price and the date it starts. Business Central replies with its reference and the line ticks to Confirmed. No reply means 3 automatic retries, then Failed.
Think of it as
Tracked parcels: Salesforce posts one, AIS delivers it, Business Central signs for it. A line isn't done until it's signed for.
You check
Failed = 0, or click Resend Failed.

Records

UpdatedPrice_Change_Line__c Sync_Status__c (Sent → Acknowledged or Failed), Sync_Attempts__c, Last_Sent_On__c, Acknowledged_On__c, BC_Reference__c, Last_Error__c
NoneAIS message no Salesforce record: the price-update topics cc.bsline / cc.bsschedule / cc.bsorder

Logic

  • Picks Applied lines that are Not Sent, or Failed with attempts under the limit.
  • Sends three existing messages per line: billing schedule (extended with dates, order line, frequency and key), order line and order. Payloads are in the panel below.
  • AIS acknowledges in groups: an upsert on Idempotency_Key__c, up to 200 per call, so the header roll-ups don't lock.
  • After 3 attempts the line is Failed; Resend Failed resets the count.
Business Central sync, PC-0007Updated 21 Jul, 06:40
4,300Sent
4,295Confirmed by BC
0Waiting
5Failed
LineMessageResult
PCL-001201Weekly, $17.96 from 7 JulConfirmed, BC ref SUB-48213
PCL-001202Fortnightly, $35.92 from 15 JulConfirmed, BC ref SUB-48214
PCL-001203Monthly, $77.83 from 20 JulNo reply after 3 tries
Failed lines keep their error until resent.Resend Failed
5

Step 5 in detail: what Customer Central sends to Business Central

For each applied line, three existing price-update messages go to AIS. No bill is sent at this step.

The example is a fortnightly subscription, split at its cutover on 15 Jul, $34.54 to $35.92. A subscription re-priced in place sends the same three messages, but the schedule message has one row: the same schedule id with the new amount.

1. Billing schedule: cc.bsschedule.priceupdateExtended
{ "billingSchedule": [
  { "id": "a2F…old", "order": "801…", "orderNo": "00081302", "amountToBeBilled": 34.54, "status": "Active",
    "orderItem": "802…", "billingStartDate": "2025-03-05", "billingEndDate": "2026-07-14",
    "billingFrequency": "2W", "billingType": "AR", "firstPaymentDate": "2025-03-18",
    "priceChangeId": "a3X…", "cutoverDate": "2026-07-15", "idempotencyKey": "a3X…:a2F…old" },
  { "id": "a2F…new", "order": "801…", "orderNo": "00081302", "amountToBeBilled": 35.92, "status": "Active",
    "orderItem": "802…", "billingStartDate": "2026-07-15", "billingEndDate": null,
    "billingFrequency": "2W", "billingType": "AR", "firstPaymentDate": "2026-07-28",
    "priceChangeId": "a3X…", "cutoverDate": "2026-07-15", "idempotencyKey": "a3X…:a2F…old" }
]}
Highlighted fields are new. Today this message carries only id, order, orderNo, amountToBeBilled and status. With just those, Business Central can't tell when the old price stops or when the new one starts.
2. Order line: cc.bsline.priceupdateNo change
{ "orderItem": [{ "id": "802…", "order": "801…", "orderNo": "00081302", "unitPrice": 31.2348, "quantity": 1,
  "lineTotalAmount": 35.92, "lineGSTAmount": 4.6852, "listPrice": 30.03, "lineDiscountAmount": 0,
  "costPrice": 0, "productCode": "01t…", "productFamily": "Telecare Products" }]}
There's one row per re-priced order line: the base and any peripherals the schedule bills. listPrice stays at the old price, because Salesforce doesn't allow OrderItem.ListPrice to be updated. Business Central should price from unitPrice and lineTotalAmount.
3. Order: cc.bsorder.priceupdateNo change
{ "order": [{ "order": "801…", "orderNo": "00081302", "actionRequired": null,
  "orderTotalGSTAmount": 4.6852, "orderTotalAmount": 35.92, "orderSubTotalAmount": 31.23 }]}
Later, when the first new-price bill is due: cc.billingtransaction.updateNo change
{ "jobId": "707…", "jobDateTime": "2026-07-28 06:00:00Z", "jobCurrentBatchCount": "1", "jobTotalBatchCount": "3",
  "recordCount": "500", "totalRecordCount": "1340",
  "transactionDetails": [{ "billingTransactionID": "a2G…", "orderNo": "00081302", "billingScheduleID": "a2F…new",
    "paymentDueDate": "2026-07-28", "billStartDate": "2026-07-15", "billEndDate": "2026-07-28", "billingAmount": 35.92 }]}
This is today's bill message. It's sent when a bill is Upcoming and its invoice date has arrived (BillingTransactionAISBatch), and the bill then becomes Submitted. A new-price bill simply carries the new amount and the new schedule's id.
QuestionAnswer
New API?No. The same AIS endpoint (named credential AIS, subject header), and the same three topics Business Central is already being built to receive.
New wrapper?No. These payloads are built by Plinqx mapping records and flows, not Apex DTOs. One mapping is extended: Outbound_BillingScheduleAPI_PatchMap, plus the fields its flow SF_To_AIS_BillingSchedule_Sync_Flow reads.
New buildA small Apex sync job that calls the existing three flows for each applied line and updates the line's sync status (Sent, Acknowledged, Failed).
Acknowledgement backNew, inbound. Either a Plinqx inbound event with a handler flow (the same pattern as Inbound_BillingTransactions_API), or AIS calls Salesforce's standard bulk upsert on Price_Change_Line__c keyed on Idempotency_Key__c.
Sent twice?The hourly patch batches select records changed in the last hour, so they send the same changes too. That's harmless if Business Central upserts on the Salesforce id; otherwise they can skip records changed by a price change.
Alternative, if the Business Central team prefers it: one new topic (for example cc.pricechange.apply) with one combined message per subscription: order line, order, old and new schedule, cutover date and key. It makes acknowledging per subscription cleaner, but needs a new mapping, a new flow, a new AIS route and a new Business Central handler.
6

Reconcile and close

The day after go-live, then automatically when each frequency's first new bill is due.
The system
Runs the checks and shows them as ticks. It also looks for anything still at the old price; that number must be 0.
You
Act only on what isn't ticked. Close the price change when everything is.

Records

AutoPrice_Change__c roll-ups recalculate: Lines_Expected / Applied / Skipped / Apply_Errors / Acknowledged / Sync_Failed
UpdatedPrice_Change__c Left_At_Old_Price__c, First_Bill_Checks__c; Status Synced, then Closed by Finance
CreatedPrice_Change_Line__c late lines from the daily sweep, if any

Logic

  • Left at old price = live schedules or upcoming bills on in-scope products whose amount ≠ the product row's new price (confirmed custom amounts aside).
  • First-bill checks: after each frequency's first new-price bill date, bills dated after the cutover at the old price must be 0.
  • Reconciled__c (formula) is true only when every check passes. Close is allowed only then, and the sweep stops at Close.
Reconciliation, PC-0007Status: Synced
  • ✓Every live schedule has a line (4,315 of 4,315), and every in-scope order line is covered
  • ✓Every line applied, or skipped with a reason (4,300 + 15)
  • ✓Business Central confirmed every applied line (4,300 of 4,300, after resend)
  • ✓Nothing left at the old price (0 schedules, 0 upcoming bills)
  • ✓First weekly bills at the new price (checked Tue 14 Jul)
  • ✓First monthly bills at the new price (checked Tue 21 Jul)
  • …First fortnightly bills: check runs Wed 29 Jul
Close becomes available when every check is ticked.Close price change
Technical view: the same flow as a system sequence
Finance Salesforce (CC) AIS Business Central 1 Create, add products, approve 2 Update pricebook (new orders) 3 Preview: create the lines 4 Preview report for sign-off 5 Sign off From go-live: each line on its apply date 6 Apply due lines (daily) Loop: each applied line 7 Price update, start date, key 8 Upsert subscription price 9 Acknowledge with BC reference 10 Line marked Acknowledged no ack: retry, then Failed 11 Reconciliation checks

Step 7 reuses the AIS topics already being built (cc.bsline.priceupdate, cc.bsschedule.priceupdate, cc.bsorder.priceupdate). The key (Price Change + Billing Schedule) makes a resend safe, because Business Central updates the same record instead of creating a second one.

How the records are created

Price Change and its product rows (step 1)

  • When: as soon as the new prices are known, at least three weeks before go-live. Created by Finance from the Price Changes tab with New.
  • Header (Price_Change__c): description, funder (MSD), go-live date and the price rule. Status starts at Draft.
  • Product rows (Price_Change_Product__c, one per product):
    • Add MSD products finds every product with the Funder = MSD definition attribute, active or not. That's the lookup the June draft in QA already uses (ProductController.getProductsByDefinitionAttributes).
    • As a safety net, any product with live schedules whose name contains MSD but whose funder isn't MSD is reported as missing.
    • Each row copies the product's frequency, and today's price from its Standard Price Book entry.
  • New prices, without typing 26 of them:
    • Fill new prices applies one uplift to each weekly price (rounded to the cent). It works out fortnightly as 2 × weekly and monthly as weekly × 52 ÷ 12 for the same service.
    • Finance can then type over any row, or paste a column of prices.
    • The price excluding GST is incl ÷ 1.15, kept to 4 decimals like today.
  • Before approval the record won't submit while a product is missing, a new price equals the old one, or the weekly, fortnightly and monthly prices disagree.

Updating the pricebook from the Price Change (step 2)

  • Update Pricebook is available once the Price Change is Approved. For each product row it updates that product's active Standard Price Book entry:
    • UnitPrice_GST_Inclusive__c = the new price incl GST;
    • UnitPrice = the new price excl GST;
    • Price_Excl_GST__c is a formula and follows.
  • All or nothing: all 26 entries are updated in one transaction. It then sets Pricebook_Update_Date__c and Status = Pricebook Updated.
  • Restore Old Prices (until Apply): writes the old prices back from the product rows, if the change is cancelled or postponed.
  • Edits outside the price change: if someone changes an in-scope pricebook price by hand afterwards, Apply sees that the pricebook no longer matches the product row and stops that product's lines.

Price Change Lines (step 3, Preview)

A batch reads the billing schedules, not the bills. That's why every subscription is found at once, whatever its frequency and wherever it is in its cycle. There's one line per live billing schedule: an order may have several schedules, and only the one with a blank End Date is picked.

SELECT Id, Order_Product__c, Order_Product__r.OrderId, Order_Product__r.Product2Id, Account__c,
       Amount_to_be_billed__c, Billing_Start_Date__c, Billing_Frequency__c, Billing_Type__c,
       Quantity__c, Next_Billing_Date_Calculated__c, Order_Product__r.LineDiscountAmount__c
FROM   Billing_Schedule__c
WHERE  Order_Product__r.OrderId IN :activatedOrdersWithAnInScopeProduct
AND    Charge_Type__c = 'Recurring'
AND    Billing_Start_Date__c != null
AND    Billing_End_Date__c = null
  • Filled on each line: its product row (the schedule's own product), Order, the schedule's own OrderItem, Account and the schedule.
    • Old_Amount__c is the schedule's amount. New_Amount__c is the sum of the new prices of every in-scope order line the schedule bills: its own line, plus peripherals on the same order with no schedule of their own. Each order line's old and new price is kept in Item_Prices__c.
    • Also: discount, frequency, in advance or in arrears, next billing date and the forecast cutover date.
    • Apply Status = Pending, Sync Status = Not Sent.
  • Already at the new price (orders placed since the pricebook update): the line is marked Skipped, so the counts still add up.
  • Rerun-safe: lines are upserted on Idempotency_Key__c (Price Change + Billing Schedule), so a second preview updates lines instead of duplicating them.
  • Counted twice: every in-scope order line on an Activated order is counted straight from OrderItem into Live_Order_Lines__c and compared with Order_Lines_Covered__c. A peripheral that no schedule bills therefore shows up as uncovered.
  • Exceptions for Finance: a schedule with a future End Date (not picked, so not re-priced), and a schedule whose amount doesn't equal the sum of its order lines' old prices.
  • Billing holidays: a holiday is its own schedule record (Charge Type = Billing Holiday). The customer's recurring schedule stays live, so it's included.
  • Apply date: each line stores the day it will be applied (see the next panel).

How Apply finds the orders (daily from go-live, until the price change is closed)

Apply doesn't search for orders. The lines created at preview are its work list, and each morning it takes the lines that are due:

SELECT Id, Order__c, Order_Product__c, Old_Billing_Schedule__c, Price_Change_Product__c,
       New_Amount__c, Cutover_Date__c, Apply_Date__c
FROM   Price_Change_Line__c
WHERE  Price_Change__c = :priceChangeId
AND    Apply_Status__c = 'Pending'
AND    Apply_Date__c <= TODAY
ORDER BY Order__c
  • Apply date (worked out at preview, stored on the line):
    • normally the subscription's cutover date;
    • for in-advance billing, the day before its first new-price bill is invoiced, if that's earlier;
    • go-live day for a subscription that hasn't billed yet.
    The job runs early each morning, before the billing run.
  • One current schedule per subscription. Because each line is applied on its own date, a subscription never has a future-dated schedule waiting while the old one is still billing. So the existing processes that find "the schedule with a blank end date" keep working unchanged: service cancellation (BillingScheduleController), ChangeProductProcessing and ChangeAccountProcessing. In-advance subscriptions have a short overlap of a few days, because their bill is invoiced before the cutover.
  • One schedule, several order lines: the line covers the schedule's own order line and any peripherals it bills. Apply re-prices all of those order lines, then the schedule, then the order totals. An order with more than one live schedule has a line for each, and they're processed together (hence ORDER BY Order__c).
Re-check before touching a lineIf it fails
Order is still ActivatedSkipped: order cancelled
The schedule still has a blank End DateSkipped: schedule ended
The order lines the schedule bills are unchanged (none added, removed or swapped)Skipped: order lines changed, so re-preview
The schedule's amount isn't already the new priceSkipped: already at the new price
The pricebook price still equals the product row's new priceError: pricebook changed since approval
  • The sweep, every run until the price change is closed: Apply also reruns the preview query. Any live schedule on these products that has no line yet gets a late line. That covers an order activated after the preview or after go-live, or one that came in from a channel at the old price. A late line already at the new price is Skipped; otherwise it's applied the same way. After close, the pricebook is the only safeguard, so any old-price order goes to Finance on an exceptions report.

What Apply does to the records

Example: a fortnightly, in-arrears subscription whose cycle started Wednesday 1 July. Go-live is Friday 3 July, the price goes from $34.54 to $35.92, and the rule is "billing periods starting on or after go-live". Its cutover is therefore Wednesday 15 July, and Apply handles it early that morning.

Order Billing schedules Bills (billing transactions) Order custom totals Updated: recalculated OrderItem (the line) $34.54 to $35.92 Updated Old Billing Schedule $34.54 a fortnight Updated: ends Tue 14 Jul New Billing Schedule $35.92 a fortnight Created: starts Wed 15 Jul Bill 1 to 14 Jul $34.54, Upcoming Left as is (old price) Bill 15 to 28 Jul only if already made here Cancelled Bill 15 to 28 Jul $35.92, Upcoming Created by Apply Bills from 29 Jul $35.92 each Created by the existing flow Price Change Line old + new schedule, cutover Updated: Applied The old schedule makes no more bills: its next period would start after its end date. Amber = existing record updated; teal = new record; dashed = only if present, or made later.
RecordActionFields (example values)
Old Billing ScheduleUpdatedBilling_End_Date__c: blank to 14 Jul (cutover − 1). Nothing else changes, so its history stays intact.
New Billing ScheduleCreatedA copy of the old one (account, bank and payment details, quantity, frequency, in arrears or in advance), with:
Billing_Start_Date__c = 15 Jul (cutover, so the customer keeps their Wednesday fortnight)
First_Payment_Date__c = 28 Jul (in arrears: end of the first period; in advance: the cutover date)
Amount_to_be_billed__c = $35.92 (the sum of the new prices of the order lines it bills); Billing_End_Date__c = blank
Upcoming bill on the old scheduleLeft or CancelledPeriod starts before cutover (1 to 14 Jul): left at the old price.
Period starts on or after cutover (possible for weekly customers): Status__c = Cancelled.
First bill on the new scheduleCreatedBilling_Period_Start_Date__c / End = 15 to 28 Jul; Status__c = Upcoming
Rate__c and Total_Amount_Value__c = $35.92; GST_Amount_Value__c = $4.69; List_Price_Value__c = $31.23
Discount_Amount_Value__c = the line discount; quantity, payment method, and payment date (in arrears: period end)
OrderItem(s)UpdatedEvery in-scope order line the schedule bills: here one; with peripherals, the base and each peripheral. UnitPrice $30.03 to $31.23; LineGSTAmount__c $4.51 to $4.69; LineTotalAmount__c $34.54 to $35.92. LineDiscountAmount__c unchanged.
OrderUpdatedOrderSubTotalAmount__c, OrderTotalGSTAmount__c, OrderTotalAmount__c and OrderDiscountAmount__c recalculated from all the order's lines (base plus add-ons). These are plain number fields that today's batch leaves stale. The standard TotalAmount follows UnitPrice automatically.
Price Change LineUpdatedNew_Billing_Schedule__c, Cutover_Date__c = 15 Jul, Apply_Method__c = Split at cutover, Apply_Status__c = Applied, Applied_On__c. Then it joins the Business Central outbox.
All or nothing per line. Each line's changes are made together. If any one fails, that line's changes roll back, the line shows Error with the message, and the rest carry on.
Later bills need no new code. When a bill is submitted, the existing flow Create recurring Billing Transactions (BillingTransactionController) makes the next Upcoming bill on the same schedule. It prices it from the schedule's Amount_to_be_billed__c and the OrderItem, both of which are now at the new price.
Watch old bills in reports. Total_Amount__c, GST_Amount__c and Unit_Price__c on bills are formulas that read the OrderItem. After Apply, even paid bills show the new price in those fields. Reports and the AIS bill payload must use the stored …_Value__c fields.

Not billed yet: re-priced in place

An order created before go-live whose first payment falls after go-live has a schedule that hasn't produced a submitted bill yet. If it came in at the old price (created before the pricebook update, or from a channel still sending the old price), splitting it would end the old schedule before it even starts. So Apply re-prices the existing records instead. The test: does the schedule have a Submitted or Invoiced bill? If not, re-price in place; if so, split at the cutover as above.

RecordActionFields (example: fortnightly, first payment Fri 10 Jul)
Billing ScheduleUpdatedAmount_to_be_billed__c $34.54 to $35.92. Same schedule, same start date, so no new schedule is created.
Upcoming billUpdatedRate__c and Total_Amount_Value__c = $35.92; GST_Amount_Value__c = $4.69; List_Price_Value__c = $31.23
OrderItem and OrderUpdatedPrices and totals, as for every other line
Price Change LineUpdatedApply_Method__c = Re-priced in place; Apply_Status__c = Applied

Weekly, fortnightly and monthly: making sure every subscription is covered

MSD products come in weekly, fortnightly and monthly versions. Each version is its own product with its own price, for example Medical Alarm at $17.27 a week, $34.54 a fortnight and $74.84 a month. So the design works by product, never by name or frequency. Each subscription bills on a cycle counted from its own start date: the same weekday for weekly and fortnightly, the same date for monthly. The switch has to land on that cycle.

Mon 29 JunMon 6 Jul Mon 13 JulMon 20 Jul Go-live Fri 3 Jul Weekly in arrears, from Tuesday 30 Jun to 6 Jul 7 to 13 Jul new price from Tue 7 Jul Fortnightly in arrears, from Wednesday 1 to 14 Jul: started before go-live, old price 15 to 28 Jul new price from Wed 15 Jul Monthly in advance, on the 20th 20 Jun to 19 Jul, old price from 20 Jul new price from Mon 20 Jul old price period new price period bill date (in arrears: end of period; in advance: start)
Each subscription's new schedule starts on its own next cycle boundary, never on the go-live date itself. The billing-date formula counts cycles from Billing_Start_Date__c, so starting a weekly customer's new schedule on Friday 3 July would move their billing day. Today's batch gets this right by starting the new schedule at the upcoming bill's period start. The new Apply keeps that rule.

The one decision Finance needs to make

Most MSD subscriptions are billed in arrears, at the end of each period. That leaves one question: what price does the period that straddles go-live pay?

Recommended

Billing periods starting on or after go-live

Customers pay the new price only for service after go-live. It's the same rule for every frequency, in advance or in arrears. The first new-price bill comes up to one cycle after go-live (above: 7, 15 and 20 July).

What today's batch does

Bills dated on or after go-live

For in-arrears customers, the period that straddles go-live is billed in full at the new price. A fortnightly customer would pay the new price for up to 13 days of service before go-live (above: the whole 1 to 14 July period).

Five checks that prove nothing is missed

CheckWhenPasses when
Every product with live MSD subscriptions is on the price change, inactive (grandfathered) products includedAdding products, and again at previewMissing products = 0
Weekly, fortnightly and monthly prices agree: fortnightly = 2 × weekly, monthly = weekly × 52 ÷ 12. Today's prices follow this ($17.27, $34.54, $74.84).Before approvalEvery product row ticked
Every live billing schedule (End Date blank) on an order with an in-scope product has exactly one line, and every in-scope order line is covered by onePreview, and again at ApplyLive schedules = lines; live order lines = covered
Each line switches at the start of its own billing period, within one cycle of go-live (7, 14 or about 31 days)Apply0 exceptions
Nothing left at an old price: no live schedule or upcoming bill on these products at the old amountDay after go-live, then when each frequency's first new bill is due (about 1, 2 and 4 weeks)0 found

The three new objects

Price Change is the record Finance approves. Price Change Product holds one row per product with its old and new price, and is what Update Pricebook writes from, so nobody edits 26 pricebook entries by hand. MSD alone has 26 products across weekly, fortnightly and monthly. Price Change Line is one live billing schedule, the one with a blank End Date, covering its base product and any peripherals it bills: the audit row, the work list for Apply and the outbox for Business Central in one.

Price_Change__c Name PC-{0000} + 21 custom fields Effective_Date__cDate Price_Rule__cPicklist Status__cPicklist Approved_By__cLookup(User) Lines_Expected__cRoll-up count Lines_Acknowledged__cRoll-up count Left_At_Old_Price__cNumber + 14 more fields master-detail one row per product Price_Change_Product__c Name PCP-{000} + 14 custom fields Product__cLookup(Product2) Billing_Frequency__cText Old_Price_Incl_GST__cCurrency New_Price_Incl_GST__cCurrency Weekly_Equivalent__cFormula Live_Order_Lines__cNumber Order_Lines_Covered__cNumber + 7 more fields UserApproved_By__c Product2Product__c master-detail one per subscription lookup Price_Change_Line__c Name PCL-{000000} + 27 custom fields Price_Change__cMaster-detail Price_Change_Product__cLookup Order_Product__cLookup(OrderItem) Old_Billing_Schedule__cLookup New_Billing_Schedule__cLookup Billing_Frequency__cText Cutover_Date__cDate New_Amount__cCurrency Apply_Status__cPicklist Sync_Status__cPicklist Idempotency_Key__cText, unique + 16 more fields AccountAccount__c OrderOrder__c OrderItemOrder_Product__c Billing Scheduleold and new Billing_Schedule__c Arrows point from a record to the record it looks up. Amber type = roll-up summary on the header. Billing Schedule (amber box) is the existing object Apply ends and creates. Full field lists are below.
Three new objects, 62 custom fields in all (21 + 14 + 27), each with an auto-number Name. No new fields on existing objects.

Line status lifecycle

Apply_Status__c Sync_Status__c Skipped Lines_Skipped__c Pending Error Lines_Apply_Errors__c Applied Lines_Applied__c Sent Acknowledged Lines_Acknowledged__c Failed Lines_Sync_Failed__c order changed fails fixed, retry apply OK send BC confirms no ack after retries resend
The preview creates every line as Pending (counted by Lines_Expected__c). Amber names are the header roll-ups that count each state.

Field lists

Price_Change__c, Price Change

Name (auto-number PC-{0000}) + 21 custom fields: 13 entered or set by the process, 7 roll-ups, 1 formula
#FieldTypePurpose
What and when
1Description__cText(255)For example "MSD price change July 2026"
2Funder__cPicklist, requiredScope for Add MSD products: every product with this funder attribute, active or not
3Effective_Date__cDate, requiredGo-live date
4Price_Rule__cPicklist"Billing periods starting on or after go-live" (default) or "Bills dated on or after go-live"
5Pricebook_Update_Date__cDate/TimeSet by Update Pricebook; cleared by Restore Old Prices
Approval and status
6Status__cPicklistDraft, Approved, Pricebook Updated, Previewed, Applied, Synced, Closed, Cancelled
7Approved_By__cLookup(User)Finance approver of the prices
8Approved_On__cDate/Time
9Preview_Signed_Off_By__cLookup(User)Who signed off the preview
10Preview_Signed_Off_On__cDate/TimeSign-off locks the preview
Counts (roll-up summaries)
11Products_In_Scope__cRoll-up countProduct rows
12Lines_Expected__cRoll-up countAll lines
13Lines_Applied__cRoll-up countApply Status = Applied
14Lines_Skipped__cRoll-up countApply Status = Skipped
15Lines_Apply_Errors__cRoll-up countApply Status = Error
16Lines_Acknowledged__cRoll-up countSync Status = Acknowledged
17Lines_Sync_Failed__cRoll-up countSync Status = Failed
Checks (set by the check jobs)
18Missing_Products__cNumberMSD-named products with live subscriptions whose funder isn't MSD
19Left_At_Old_Price__cNumberLive schedules or upcoming bills on these products still at an old amount
20First_Bill_Checks__cText(255)For example "Weekly OK 14 Jul; Monthly OK 21 Jul; Fortnightly due 29 Jul"
21Reconciled__cFormula checkboxMissing = 0, expected = applied + skipped, applied = acknowledged, no errors or failures, nothing left at the old price

Price_Change_Product__c, Price Change Product

Name (auto-number PCP-{000}) + 14 custom fields: one row per product; the source for Update Pricebook
#FieldTypePurpose
1Price_Change__cMaster-detail(Price_Change__c)The parent price change
2Product__cLookup(Product2), requiredOne row per product (duplicates blocked)
3Product_Is_Active__cCheckboxCopied, so inactive products that still bill stand out
4Billing_Frequency__cText(3)Copied from the product: 1W, 2W or 1M
5Price_Group__cText(80)For example "Medical Alarm": links the weekly, fortnightly and monthly versions for Fill and the checks
6Old_Price_Excl_GST__cCurrency(16,4)From the pricebook when added; used by Restore Old Prices
7Old_Price_Incl_GST__cCurrency(16,2)From the pricebook when added
8New_Price_Excl_GST__cCurrency(16,4)Incl ÷ 1.15; written to the pricebook's UnitPrice
9New_Price_Incl_GST__cCurrency(16,2)Filled from the uplift or typed; written to UnitPrice_GST_Inclusive__c
10Weekly_Equivalent__cFormula currencyNew price ÷ 1, 2 or (52 ÷ 12)
11Price_Check__cText(255)"OK", or what's wrong, for example "2 × weekly is $35.92"
12Live_Order_Lines__cNumberOrder lines of this product on Activated orders, counted straight from OrderItem (includes peripherals, which have no schedule of their own)
13Order_Lines_Covered__cNumberOf those, the order lines covered by a Price Change Line
14Coverage_OK__cFormula checkboxLive order lines = order lines covered

Price_Change_Line__c, Price Change Line

Name (auto-number PCL-{000000}) + 27 custom fields: 7 relationships, 9 amount and schedule, 4 apply, 7 sync
#FieldTypePurpose
Relationships
1Price_Change__cMaster-detail(Price_Change__c)The parent price change
2Price_Change_Product__cLookup(Price_Change_Product__c)The product row of the schedule's own product
3Account__cLookup(Account)Customer, for reports and support
4Order__cLookup(Order)Order whose totals are recalculated
5Order_Product__cLookup(OrderItem)The schedule's own order line (the base product)
6Old_Billing_Schedule__cLookup(Billing_Schedule__c)The live schedule (End Date blank) this line re-prices; ended the day before cutover, or re-priced in place
7New_Billing_Schedule__cLookup(Billing_Schedule__c)Created from cutover at the new price (split only)
Amount and schedule
8Old_Amount__cCurrency(16,2)The schedule's amount, per period incl GST
9New_Amount__cCurrency(16,2)Sum of the new prices × quantity of every in-scope order line the schedule bills (base + peripherals)
10Item_Prices__cLong Text(32,768)Every order line this schedule bills: OrderItem, product, quantity, old and new price (JSON); the audit trail for peripherals
11Discount_Amount__cCurrency(16,2)Line discounts carried across
12Billing_Frequency__cText(3)1W, 2W or 1M
13Billing_Type__cText(2)AR (in arrears) or AD (in advance)
14Next_Billing_Date__cDateAt preview, for the Finance report
15Cutover_Date__cDateFirst day at the new price: the start of this subscription's first billing period under the price rule
16Apply_Date__cDateThe day Apply handles this line: the cutover date, or earlier for in-advance billing; go-live day if not billed yet
Apply result
17Apply_Status__cPicklistPending, Applied, Skipped, Error
18Apply_Method__cPicklistSplit at cutover, or Re-priced in place (not billed yet)
19Skip_Reason__cText(255)Already at the new price, cancelled, schedule ended, order lines changed
20Applied_On__cDate/Time
Business Central sync (the outbox)
21Sync_Status__cPicklistNot Sent, Sent, Acknowledged, Failed
22Sync_Attempts__cNumber(3,0)Automatic retry up to a set limit
23Last_Sent_On__cDate/Time
24Acknowledged_On__cDate/Time
25BC_Reference__cText(80)Business Central's id for the update
26Last_Error__cLong Text(32,768)Last apply or sync error
27Idempotency_Key__cText(80), unique, external IDPrice Change id + Billing Schedule id: one line per schedule, a resend is safe, and AIS can return acknowledgements by upsert

Also needed: a Finance permission set (create, approve, update pricebook, view) and an integration permission for AIS to update the sync fields. Seven roll-ups is well under the per-object limit (25 by default).

Options for the Business Central side

Recommended now

A. Salesforce stays the billing engine

Salesforce re-prices schedules and bills as above. Business Central receives each subscription's new price with its cutover date, and invoices from Salesforce's bills. Least change, and it reuses the AIS topics being built.

Later option

B. Business Central owns subscription billing

Salesforce sends one price-list change per product (new price, go-live, price rule) and Business Central re-prices its own contracts. Fewest messages, but it needs Business Central subscription billing in scope.

Changes to what exists

  • Manual pricebook edits for price changes stop: the Price Change writes the Standard Price Book entries (and can restore them).
  • Retire TelecareMSDPriceChangeUpdateBatch, its schedule class and the MSDPriceDateChange label after the first run on the new design. Keep its one good rule: the new schedule starts at a period start.
  • June draft in QA (TelecarePriceChangeBatch, PriceChangeService; not in UAT or Prod): reuse its product lookup by funder attribute, then retire it. As written, it sets every schedule on an order to the whole order total, and adds bills with no dates or status.
  • AIS patch batches (OrderPatchHourlyBatch, OrderLinePatchHourlyBatch, BillingScheduleHourlyBatch) pick records changed in the last hour, so a failed run can miss changes. For price changes they send from the lines instead; the same outbox pattern would make the general sync safer.
  • No whole-order resend (Order V2) for price changes.
  • No change to service cancellation (BillingScheduleController), ChangeProductProcessing or ChangeAccountProcessing. They find "the schedule with a blank end date", and applying each line on its own date keeps exactly one such schedule per subscription.

Risks and how the design handles them

  • A wrong price reaching new orders: prices are checked across frequencies and approved before Update Pricebook can run. If one is still wrong, Restore Old Prices puts the pricebook back until go-live.
  • Pricebook edited by hand after the update: Apply compares the pricebook with each product row and stops that product's lines until they match.
  • Billing day moves if a new schedule starts on go-live day. Apply starts it on the subscription's own cycle boundary, and check 4 proves it.
  • Volume: a price change touches every live MSD subscription, tens of thousands of lines. Apply and the sync run in batches. AIS returns acknowledgements in groups (an upsert of up to 200 lines per call on Idempotency_Key__c), so header roll-ups don't hit UNABLE_TO_LOCK_ROW.
  • Old bills show new prices in three formula fields. Reports and the AIS payload use the stored …_Value__c fields.
  • Orders that change between preview and their apply date (cancel, downgrade, swap): Apply re-checks each line and skips with a reason.
  • Old-price orders after the pricebook update: order prices come from the order payload, not the pricebook (OrderItemController, BillingScheduleController). The daily sweep catches them until close (re-priced in place if not billed yet); after close they appear on an exceptions report.
  • Daily Apply must run before the billing run, so a due line is re-priced before its bill is invoiced. In-advance subscriptions have a few days' overlap between the new schedule's creation and its start.
  • Custom amounts and discounts: the preview lists subscriptions not on the list price for Finance to confirm. Line discounts carry across as today.
  • GST: excl-GST prices are worked out from incl-GST at 15% and kept to 4 decimals, matching today's pricebook.
  • First use: a full rehearsal in UAT against the Business Central test environment, with the five checks signed off, before the first production price change.

Questions

For Finance
  1. Price rule: billing periods starting on or after go-live (recommended), or bills dated on or after go-live (today's behaviour)?
  2. Who approves the prices, and who signs off the preview?
  3. Is a single uplift for all MSD products the usual case, or are prices set per product?
  4. Where do the website, TIS and Order Quick Create get MSD prices, and how soon do they pick up a pricebook change?
For the Business Central and AIS team
  1. Who generates recurring invoices in the Business Central design: Salesforce bills (A) or Business Central subscription billing (B)?
  2. Can AIS or Business Central return an acknowledgement per message, with Business Central's reference, in groups?
  3. Can Business Central hold a future-dated price per subscription? Each subscription has its own cutover date.
  4. Will Business Central upsert on Salesforce ids (OrderItem, billing schedule), so a resend is safe?
  5. A price change covers every live MSD subscription. What is the AIS limit per hour?