Retail assortment published
Fires when Commerce headquarters processes a published retail assortment for the first time in a legal entity, or processes it again after its effective item list there has changed. The payload carries the assortment ID and name, when it was published, its validity dates, and the effective item list: one entry per released item, after the assortment's Exclude lines and validity dates are applied, with a price per item. When the assortment is tied to a customer, the prices are that customer's sales prices in its currency; otherwise they're the base sales prices in the accounting currency. Downstream systems can use it to refresh a channel, marketplace, or B2B catalog from the assortment. A processing run that leaves the item list unchanged sends nothing.
| Event class | MGCLatticeRetailAssortmentPublishedBusinessEvent |
| Contract class | MGCLatticeRetailAssortmentPublishedBusinessEventContract |
| Category | Product information management |
| Module | ModuleAxapta::ProductInformationManagement |
| Label IDs | RetailAssortmentPublishedBusinessEventName / RetailAssortmentPublishedBusinessEventDescription |
| Trigger | RetailAssortmentLookupTask.run() -> after the per-assortment explosion task runs, on first publish or when the effective item list changed |
Trigger / Hook
- Extension class:
MGCLatticeRetailAssortmentLookupTask_Class_Extension, decorated with[ExtensionOf(classStr(RetailAssortmentLookupTask))]. - CoC method signature:
public void run()(no parameters). - When the task runs: the Process assortments job (Retail and Commerce > Retail and Commerce IT > Products and inventory > Process assortments, class
RetailAssortmentLookupJob) creates oneRetailAssortmentLookupTaskfor every assortment whose status is Published, or Draft with a publish date, on every run. The Publish button on an assortment schedules the same job for that one assortment. - Firing condition: after
next run()completes, when the task's assortment record ID and assortment ID are both set (the standard task logs an error and returns without processing when either is missing) and the event is activated,MGCLatticeRetailAssortmentPublishedBusinessEvent::publishIfChanged():- resolves the assortment's effective item list in the legal entity the task runs in (see Item list below);
- computes a fingerprint of that list (a SHA-256 hash of the sorted item numbers);
- compares it with the fingerprint recorded in the
MGCLatticeRetailAssortmentPublishStatetable for the assortment in this legal entity; - sends the event when there's no record yet (first publish) or the fingerprint differs, and records the new fingerprint and the time. A recurring Process assortments job therefore raises the event once per assortment, and again only when the assortment's item list in that legal entity changes. Price changes alone, and changes to the assortment's name, customer or channels, don't raise it; a changed customer shows in the next event that the item list raises.
- Transaction: the fingerprint is recorded in its own transaction and the event is sent after that transaction has committed (after the caller's transaction, when one is open), never inside it. A publish failure therefore happens outside any transaction: it is logged as an Error row with the payload in the Lattice business event log, raises the Business event publish failed event, and the business event reconciliation sweep re-publishes it. The next Process assortments run doesn't send an unchanged list again. A duplicate-key or update conflict with a concurrent run for the same assortment is retried, and the retry sends nothing when the other run already published the same list.
- Expiry: when an assortment expires, the standard job marks it Expired and stops processing it, so no event is sent for the expiry itself.
- Deletion: deleting an assortment deletes its
MGCLatticeRetailAssortmentPublishStaterecords with it (the relation cascades on delete). No event is sent for the deletion. - Item list (built by
MGCLatticeRetailAssortmentPublishedBusinessEventContract::effectiveItemIds()from the assortment's exploded lookup lines,RetailAssortmentLookup):- Only lines valid today (UTC) are used, the same date the standard job uses to expire assortments. The lookup lines carry the assortment's validity dates, so before the assortment's Valid from date and after its Valid to date the list is empty.
- A product included at product level counts unless the same product is excluded at product level. A product master included at product level is also left out when every one of its variants is excluded.
- A variant included on its own counts for its product master unless that variant is excluded, or the master has a product-level line of its own (which then decides).
- Products are mapped to the items released in the legal entity where the task runs (
InventTable). A product master is listed once however many of its variants are assorted, and products that aren't released there are left out.
- Customer: the Customer account field that Lattice adds to the assortment (
RetailAssortmentTable.MGCLatticeCustAccount). Set it on the assortment page (Retail and Commerce > Catalogs and assortments > Assortments, General tab, next to Name; the lookup lists the customers of the current legal entity), or import it through the standard Assortment data entity (RetailAssortmentEntity, ODataRetailAssortments), where Lattice adds theMGCLatticeCustAccountandMGCLatticeCustDataAreaIdfields. Entering a customer on the page records the current legal entity as the customer's legal entity (Customer legal entity, read-only), and an import that leaves it blank gets the legal entity it runs in. Selecting a customer also fills in the assortment name with the customer's name when the name is still empty; a name you entered is never changed or cleared. A failed publish's payload is stored on its Error row in the Lattice business event log, and a replay resends that payload with the originaleventSequenceandcorrelationId; a row with no stored payload is rebuilt from the assortment's current state. Assortments are shared by all legal entities and customers aren't, so the customer's prices are used only when the Process assortments task runs in the customer's legal entity; in every other legal entity the assortment is published with base prices. - Price at publish:
- Without a customer, when the task runs in another legal entity than the customer's, or when the customer doesn't exist there,
salesPriceis the item's base sales price,InventTableModule.Pricefor the Sales module, andcurrencyCodeis the accounting currency. - When the task runs in the customer's legal entity and the customer exists there,
salesPriceis the price a sales order line for that customer would get: it's calculated withPriceDisc_Pricefor the customer's account and its customer price group, so trade agreements for the customer, for its price group and for all customers apply. The search uses a quantity of 1, the item's sales unit, the customer's currency (or the accounting currency when the customer has none), blank inventory dimensions, and today's date in the time zone of the user the task runs as (the session date, the default price date of standard sales order lines, rather than the server's UTC date). When no agreement applies, it's the base sales price converted to the customer's currency and the sales unit.currencyCodeis the customer's currency. - Retail pricing (price adjustments, affiliation price groups) isn't applied.
- Without a customer, when the task runs in another legal entity than the customer's, or when the customer doesn't exist there,
- Audit: this event is recorded in the Lattice business event log (System administration > Inquiries > Lattice business events > Business event log) each time it's sent.
- Enablement gate: guarded by
BusinessEventsConfigurationReader::isBusinessEventEnabled(classStr(MGCLatticeRetailAssortmentPublishedBusinessEvent)), checked before the item list is resolved. While the event isn't activated nothing is recorded, so the first run after activation sends the event for every processed assortment.
public void run()
{
next run();
RetailAssortmentRefRecId assortmentRefRecId = this.parmRetailAssortmentRecId();
// The standard task logs an error and returns without processing when either the assortment record
// ID or the assortment ID is missing; nothing was published in that case.
if (assortmentRefRecId
&& this.parmRetailAssortmentID()
&& BusinessEventsConfigurationReader::isBusinessEventEnabled(classStr(MGCLatticeRetailAssortmentPublishedBusinessEvent)))
{
MGCLatticeRetailAssortmentPublishedBusinessEvent::publishIfChanged(assortmentRefRecId);
}
}
From MGCLatticeRetailAssortmentPublishedBusinessEvent::publishIfChanged():
ttsbegin;
MGCLatticeRetailAssortmentPublishState publishState = MGCLatticeRetailAssortmentPublishState::find(assortment.RecId, true);
if (MGCLatticeRetailAssortmentPublishState::mustPublish(publishState.RecId != 0, publishState.ItemListFingerprint, fingerprint))
{
// ... records the fingerprint and LastPublishedDateTime, inserting or updating the state record ...
mustSend = true;
}
ttscommit;
if (mustSend)
{
MGCLatticeRetailAssortmentPublishedBusinessEvent::newFromRetailAssortment(assortment, effectiveItems).sendAfterCommit();
}
Payload Contract
One row per [DataMember], in declaration order. Header fields come from the processed RetailAssortmentTable record, mapped in initialize().
| JSON field | Display label | Type / EDT | Source | Notes |
|---|---|---|---|---|
legalEntity | Legal entity | DataAreaId | curExt() of the Process assortments task | The legal entity the item list was built and published in, lower case |
customerAccount | Customer account | CustAccount | RetailAssortmentTable.MGCLatticeCustAccount | Blank for a generic assortment. Sent as set on the assortment, also when it is not priced in this legal entity |
customerLegalEntity | Customer legal entity | DataAreaId | RetailAssortmentTable.MGCLatticeCustDataAreaId | The legal entity the customer account belongs to, lower case; blank for a generic assortment. The prices are the customer's only when it equals legalEntity |
currencyCode | Currency | CurrencyCode | CustTable.Currency, or Ledger::accountingCurrency() | The currency of every salesPrice in items; see the price rules above |
assortmentId | Assortment ID | RetailAssortmentID | RetailAssortmentTable.AssortmentID | |
assortmentName | Assortment name | RetailAssortmentName | RetailAssortmentTable.name | |
publishedDateTime | Published date and time | utcdatetime | RetailAssortmentTable.PublishedDateTime | When the assortment was last published, not when it was processed. UTC, "\/Date(milliseconds)\/" form |
validFrom | Valid from | ValidFromDate | RetailAssortmentTable.ValidFrom | A date, sent as midnight UTC in the "\/Date(milliseconds)\/" form |
validTo | Valid to | ValidToDate | RetailAssortmentTable.ValidTo | A date, sent as midnight UTC in the "\/Date(milliseconds)\/" form. When the assortment has no end date, this is the maximum date the page defaults to |
items | Items | List of MGCLatticeRetailAssortmentDetailContract | See the item list rules above | JSON array, sorted by item number; empty when nothing is effective |
Each element of items (MGCLatticeRetailAssortmentDetailContract):
| JSON field | Display label | Type / EDT | Source | Notes |
|---|---|---|---|---|
itemId | Item ID | ItemId | InventTable.ItemId | |
salesPrice | Sales price | SalesPrice | InventTableModule.Price (Sales), or the customer's sales price | In currencyCode, per the item's price unit; see the price rules above |
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 built; gaps are normal), correlationId (String, always Scope:Key - Event:<guid> when the emission belongs to no 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": "MGCLatticeRetailAssortmentPublishedBusinessEvent",
"ControlNumber": 5637146402,
"EventId": "5B8E2C70-4A19-4F3D-86B5-D0C7E9A2F413",
"EventTime": "\/Date(1790302476000)\/",
"EventTimeIso8601": "2026-09-25T02:14:36.0292964Z",
"MajorVersion": 0,
"MinorVersion": 0,
"BusinessEventLegalEntity": "USMF",
"ContextRecordSubject": "",
"InitiatingUserAADObjectId": "{C7391FB5-3BD3-D650-130E-96706E878F3A}",
"ParentContextRecordSubjects": [],
"eventSequence": 48633,
"correlationId": "Event:e7a15c94-2b6f-4d08-9a3e-5f1c8b0d2e76",
"schemaVersion": 1,
"legalEntity": "usmf",
"customerAccount": "",
"customerLegalEntity": "",
"currencyCode": "USD",
"assortmentId": "ASMT-0007",
"assortmentName": "Houston store speakers",
"publishedDateTime": "\/Date(1790289068000)\/",
"validFrom": "\/Date(1767225600000)\/",
"validTo": "\/Date(1798675200000)\/",
"items": [
{
"itemId": "D0001",
"salesPrice": 349.99
},
{
"itemId": "D0002",
"salesPrice": 499.0
}
]
}
Activation
In the D365FO client, go to System administration > Setup > Business events > Business events catalog. Find this event by name ("Retail assortment published") or by filtering the Category column to Product information management. 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).
The event is only raised when assortments are processed, so the Process assortments job must run: schedule it as a recurring batch job, or use Publish on an assortment. Activate the event in the legal entity where that job runs, because the item list is built from the products released in that company, and the list last published is remembered per legal entity.