Skip to main content

Sales price agreement expired

Fires when a sales-price trade agreement lapses past its To date and no other sales price of the same agreement - account, item, currency, unit, quantity break and inventory dimensions - is in effect on the next day. Nothing is written to the database at that moment, so no table or posting hook can observe it; the event is raised by the Lattice effectivity batch, which on each run reports the sales prices whose last valid day has passed since the batch's last position. Subscribers can use it to remove the price from storefronts or external pricing engines, or to alert pricing teams that a customer or price group has been left without a price.

Event classMGCLatticeTradeAgreementPriceExpiredBusinessEvent
Contract classMGCLatticeTradeAgreementPriceBusinessEventContract
CategorySales orders
ModuleModuleAxapta::SalesOrder
Label IDsTradeAgrPriceExpiredName / TradeAgrPriceExpiredDesc
TriggerMGCLatticeTradeAgreementPriceEffectivityBatch.run() (the daily Lattice sales price effectivity events background process, in every legal entity; a run from the menu item, in the legal entity it runs in) -> sales-price ToDate after the sweep's position and before the run date, with no successor on the next day

Trigger / Hook​

Which path raises which event​

The six sales price agreement events share one payload contract and are told apart by changeType / changeTypeName. All six are limited to sales price agreements - PriceDiscTable rows whose relation is Price (sales) (PriceType::PriceSales); purchase prices and discount agreements never raise them, and journal lines with another relation are ignored. Below, agreement means a row's account code and account relation, item code and item relation, currency, unit, quantity break (from quantity) and inventory dimensions; the dates and the amount are not part of it.

EventRaised byFires when
Sales price agreement createdPosting a trade-agreement journalA posted journal line inserted a sales price for an agreement that had no sales price in effect on the new price's From date
Sales price agreement modifiedPosting a trade-agreement journalA posted journal line that references an existing sales price changed at least one of its values
Sales price agreement supersededPosting a trade-agreement journalA posted journal line inserted a sales price for an agreement that already had a sales price in effect on the new price's From date
Sales price agreement deletedPriceDiscTable.delete(), and posting a journal line marked for deletionA sales price is deleted
Sales price agreement effectiveEffectivity batch (background process, every legal entity)A sales price's From date is after the batch's position and on or before the run date
Sales price agreement expired (this event)Effectivity batch (background process, every legal entity)A sales price's To date is after the batch's position and before the run date, and no other sales price of the same agreement is in effect on the day after that To date

Sales-price rows that are inserted or updated outside a journal post - for example through a data entity or by code that calls PriceDiscTable.insert() or update() - are not classified by these events; the record-based Sales price trade agreement changed event (catalog category Accounts receivable) reports them. The journal post itself writes PriceDiscTable with set-based statements; whether those writes also raise the record-based event depends on the Accounts receivable parameters Skip price agreement insert alerts (rows the post inserts) and Skip price agreement update alerts (rows it updates). Set to Yes, a journal post raises only the events in the table above; set to No, the record-based event is also raised for each such row.

Effectivity batch​

  • Batch class: MGCLatticeTradeAgreementPriceEffectivityBatch (extends RunBaseBatch), run from the action menu item MGCLatticeTradeAgreementPriceEffectivityBatch ("Sales price agreement effectivity (effective/expired) events"). The event is raised only when this batch runs; the Lattice sales price effectivity events background process runs it daily (see Activation). The background process sweeps every legal entity that is not a virtual company, each inside that legal entity, so the activation check, the position and the event are that legal entity's. Legal entities where neither the Effective nor the Expired event is active are skipped, and a failure in one legal entity is reported ("The sales price agreement effectivity run failed in legal entity x. The other legal entities were still processed; this one is retried on the next run.") without stopping the others. A run from the menu item sweeps only the legal entity it runs in. In each legal entity, a run does the Effective sweep first, then the Expired sweep.
  • Position: each legal entity keeps one MGCLatticeTradeAgreementPriceEffectivityCursor row per sweep, holding the To date and record ID of the last sales price the Expired sweep processed (CursorDate, CursorRecId; the highest possible record ID means the whole date is done).
  • First run: the first run in which this event is active records the starting position - the day before the run date - and sends nothing. Prices whose last valid day is before the run date are never reported.
  • Firing condition: every sales price that has a To date, where the To date is after the position and before the run date (today in the company's time zone) - that is, its last valid day has passed - in (To date, record ID) order. A run covers every day since the position, so a gap between runs is caught up in one run. When the run has processed them all, the position moves to the day before the run date. Rows without a To date (open-ended prices) never expire.
  • Successor check: the event is not sent when another sales price of the same agreement - same account code and relation, item code and relation, currency, unit, quantity break (from quantity) and inventory dimensions - is in effect on the day after the To date (From date on or before that day, and To date empty or on or after it). The price was then replaced rather than left to lapse. Prices of another quantity break or other inventory dimensions do not count, and neither does a price that starts later than the next day. The check is made when the batch processes the price, so a successor added or removed later does not change what was reported.
  • A price whose To date is changed to a later date after it was reported is reported again when that date has passed; a price saved with a To date the position has already passed (for example a back-dated end date) is not reported.
  • One price at a time: for each price, the event is sent (unless a successor exists) and the position moves past the price in the same transaction. So an interrupted run resumes after the last price it committed and sends nothing twice; a send that fails is rolled back together with its position move, the sweep stops at that price (the run shows "The Sales price agreement expired event for sales price record n could not be sent. The sweep stopped at this price and will retry it on its next run.") and the next run retries it; and two runs at the same time in the same company never both report a price, because each price's transaction locks the position row and a price another run has already processed is skipped.
  • Inactive event: while this event is not activated in the company, the Expired sweep does nothing and keeps its position (the run shows "Sales price agreement expired is not active in this company. Its sweep kept its position and will catch up once the event is activated."). Once the event is activated again, the next run reports the prices that expired in the meantime. If the event is deactivated during a run, the sweep stops and keeps its position.
  • Throttling: the batch pauses 250 ms after every 500 events it sends (Effective and Expired together), outside any transaction.
  • Enablement gate: the sweep checks BusinessEventsConfigurationReader::isBusinessEventEnabled(classStr(MGCLatticeTradeAgreementPriceExpiredBusinessEvent)) at the start and before each price. Effective and Expired are activated, and keep their positions, independently.
  • Each run ends with the message "Sales price agreement effectivity run completed. Effective events sent: n. Expired events sent: n."

The Expired sweep in sweep():

while select row
order by ToDate, RecId
where row.Relation == PriceType::PriceSales
&& row.ToDate != dateNull()
&& row.ToDate <= lastDate
&& (row.ToDate > cursorDate || (row.ToDate == cursorDate && row.RecId > cursorRecId))
{
if (!this.processRow(_changeType, row, row.ToDate))
{
break;
}
}

The successor check, from hasSuccessor():

PriceDiscTable successor;
TransDate dayAfter = _expiring.ToDate + 1;

select firstonly RecId from successor
where successor.Relation == PriceType::PriceSales
&& successor.ItemCode == _expiring.ItemCode
&& successor.ItemRelation == _expiring.ItemRelation
&& successor.AccountCode == _expiring.AccountCode
&& successor.AccountRelation == _expiring.AccountRelation
&& successor.Currency == _expiring.Currency
&& successor.InventDimId == _expiring.InventDimId
&& successor.UnitId == _expiring.UnitId
&& successor.QuantityAmountFrom == _expiring.QuantityAmountFrom
&& successor.RecId != _expiring.RecId
&& successor.FromDate <= dayAfter
&& (successor.ToDate == dateNull() || successor.ToDate >= dayAfter);

Payload Contract​

One row per [DataMember], in declaration order. All six sales price agreement events use this contract; the sources are fields on the expired PriceDiscTable row as it is when the batch runs, mapped in initialize().

JSON fieldDisplay labelType / EDTSourceNotes
legalEntityLegal EntityDataAreaIdPriceDiscTable.DataAreaIdCompany of the trade agreement row
changeTypeChange Typeintenum2int(MGCLatticeTradeAgreementPriceChangeType)Integer change type: Created 0, Modified 1, Superseded 2, Deleted 3, Effective 4, Expired 5. Always 5 for this event
changeTypeNameChange Type Namestrenum2Symbol(enumNum(MGCLatticeTradeAgreementPriceChangeType), …)Change type symbol; always "Expired" for this event
priceTypePrice Typeintenum2int(PriceDiscTable.Relation)Integer value of the PriceType enum; always 4 (PriceSales)
priceTypeNamePrice Type Namestrenum2Symbol(enumNum(PriceType), enum2int(PriceDiscTable.Relation))Always "PriceSales"
accountCodeAccount Codeintenum2int(PriceDiscTable.AccountCode)PriceDiscPartyCodeType: 0 = Table (one customer), 1 = GroupId (customer price group), 2 = All (all customers)
accountCodeNameAccount Code Namestrenum2Symbol(enumNum(PriceDiscPartyCodeType), …)Symbol of accountCode: Table, GroupId or All
accountRelationAccount RelationPriceDiscAccountRelationPriceDiscTable.AccountRelationCustomer account or price group; empty when accountCode is 2
itemCodeItem Codeintenum2int(PriceDiscTable.ItemCode)PriceDiscProductCodeType: 0 = Table (one item), 1 = GroupId, 2 = All
itemCodeNameItem Code Namestrenum2Symbol(enumNum(PriceDiscProductCodeType), …)Symbol of itemCode: Table, GroupId or All
itemRelationItem RelationPriceDiscItemRelationPriceDiscTable.ItemRelationItem number (or item group)
currencyCodeCurrency CodeCurrencyCodePriceDiscTable.CurrencyCurrency of the price
unitIdUnitUnitOfMeasureSymbolPriceDiscTable.UnitIdUnit of measure the price applies to
quantityAmountFromQuantity FromAmountQtyPriceDiscTable.QuantityAmountFromFrom quantity of the row's quantity break
quantityAmountToQuantity ToAmountQtyPriceDiscTable.QuantityAmountToTo quantity of the row's quantity break; 0 when the break has no upper limit
inventDimIdInventory DimensionsInventDimIdPriceDiscTable.InventDimIdInventory dimension combination the price applies to; AllBlank when the price is not dimension-specific
priceUnitPrice UnitPriceUnitPriceDiscTable.PriceUnitNumber of units the amount is quoted for
fromDateFrom DateFromDatePriceDiscTable.FromDateStart of the validity period, in the \/Date(milliseconds)\/ form. A row without a start date carries 1900-01-01 (\/Date(-2208988800000)\/)
toDateTo DateToDatePriceDiscTable.ToDateThe price's last valid day, in the same form
amountAmountPriceDiscAmountPriceDiscTable.AmountThe price that expired
oldAmountOld AmountPriceDiscAmountNot used by this eventAlways 0
discountPercentDiscount PercentDiscPctPriceDiscTable.Percent1Discount percentage stored on the row (Percent1); normally 0 on a sales price row
priceDiscTableRecIdPrice Disc Table RecIdRecIdPriceDiscTable.RecIdRecord ID of the expired row
previousPriceDiscTableRecIdPrevious Price Disc Table RecIdRecIdNot used by this eventAlways 0
changedFieldsChanged Fieldslist of strNot used by this eventAlways an empty array

Every Lattice event also carries the Lattice ordering and correlation envelope, inherited from MGCLatticeBusinessEventContractBase: eventSequence (Int64 - increasing across the whole catalog and all companies, allocated when the event is sent inside the business transaction; it orders the events of one record, but two transactions can commit in the opposite order of their values, and gaps are normal and permanent - see the events overview), correlationId (String - always Scope:Key: Event: followed by a GUID unique to this emission, unless the event joins a run) and schemaVersion (Int - the payload shape version; 1 for this contract). D365 F&O also adds its standard envelope fields to every business event payload: BusinessEventId, BusinessEventLegalEntity, ContextRecordSubject, ControlNumber, EventId, EventTime, EventTimeIso8601, InitiatingUserAADObjectId, MajorVersion, MinorVersion, and ParentContextRecordSubjects. Do not rely on the order of members in the delivered JSON.

Example Payload​

{
"BusinessEventId": "MGCLatticeTradeAgreementPriceExpiredBusinessEvent",
"ControlNumber": 5637149321,
"EventId": "B267C0E2-054E-4EAD-BB4C-C5301EA39596",
"EventTime": "\/Date(1790816409000)\/",
"EventTimeIso8601": "2026-10-01T01:00:09.6264163Z",
"MajorVersion": 0,
"MinorVersion": 0,
"BusinessEventLegalEntity": "USMF",
"ContextRecordSubject": "",
"InitiatingUserAADObjectId": "{048AC514-DCC1-B54A-85A2-7DFA10A6ADEE}",
"ParentContextRecordSubjects": [],
"eventSequence": 48518,
"correlationId": "Event:8653c1c1-56f9-40c2-8011-8db6c3657fea",
"schemaVersion": 1,
"legalEntity": "usmf",
"changeType": 5,
"changeTypeName": "Expired",
"priceType": 4,
"priceTypeName": "PriceSales",
"accountCode": 0,
"accountCodeName": "Table",
"accountRelation": "US-001",
"itemCode": 0,
"itemCodeName": "Table",
"itemRelation": "D0111",
"currencyCode": "USD",
"unitId": "ea",
"quantityAmountFrom": 0,
"quantityAmountTo": 0,
"inventDimId": "AllBlank",
"priceUnit": 1,
"fromDate": "\/Date(1780272000000)\/",
"toDate": "\/Date(1790726400000)\/",
"amount": 1299.00,
"oldAmount": 0,
"discountPercent": 0,
"priceDiscTableRecId": 22565424069,
"previousPriceDiscTableRecId": 0,
"changedFields": []
}

Activation​

  1. In the D365FO client, go to System administration > Setup > Business events > Business events catalog. Find this event by name ("Sales price agreement expired") or by filtering the Category column to Sales orders. Select it and use Activate to bind it to a configured endpoint (for example, an Azure Service Bus queue, Event Grid topic, or HTTPS webhook).
  2. The Lattice sales price effectivity events background process (System administration > Setup > Process automations, Background processes tab) sends this event once a day in every legal entity where it is active; there is nothing to schedule per legal entity. To send a legal entity's events at once, open System administration > Periodic tasks > Lattice business events > Sales price agreement effectivity (effective/expired) events in that legal entity (available to the Lattice Integration administrator role) and select OK. Such a run covers only that legal entity, and because each legal entity keeps its own position, nothing is sent twice. The batch has no parameters.
  3. The first run after the event is activated only records its starting position and sends nothing; events start with the next run.