Skip to main content

External Events

Goods Received publishes one event, when a delivery — or part of one — is approved. Nothing is published while a delivery is being created or counted, so an event always means "a store has accepted these quantities".

Read more about how to subscribe in External Events.

The event source​

Event sourcegoods-received
Name in the catalogueGRC: Goods received
Versionv1
Content typeapplication/json
Published whenA delivery, a container, or a single line is approved

Inside Hii Retail the same event feeds Stock services, which is how an approved delivery becomes stock. Subscribing externally does not change or delay that.

This is the event to watch for receival deviations, whatever the goods came from. Each line carries expectedQuantity, what the delivery said, next to approvedQuantity, what the store accepted, and reasonCodes explain any difference: short, over or damaged. A delivery from another store is no exception. Its shipperType is businessUnitId, and each line's orderId and orderLineId are the store transfer's id and line id, so a tin lost between two stores shows up here and nowhere else. Both quantities are levels: take the latest event for a line, as described under Consuming the events.

The payload​

The full schema is on Goods received completed format, and is also published as a JSON Schema you can validate against — the event source entry in the External Events catalogue links it. What the fields mean:

The delivery​

FieldRequiredMeaning
tenantId✅The tenant
businessUnitId✅The receiving store
uniqueId✅Hii Retail's id for the delivery. Stable across every event for that delivery
deliveryNoteId✅The shipper's delivery note number
shipperId✅Who sent the goods
shipperType✅businessPartnerId — an external supplier or warehouse — or businessUnitId — another store
deliveryDate✅The delivery date stated on the delivery note — when the goods were expected, not when they arrived or were approved
submittedAt✅When the store approved. This is the event's own timestamp
submittedBy✅The user who approved
comment➖Free text from the delivery note. May be null
transportationAmount➖Delivery-level transport cost. May be null
customsAmount➖Delivery-level customs cost. May be null
lines✅The lines this approval covered

⚠ shipperType tells you whether this was a purchase or an internal movement. A delivery from another store is goods moving inside the customer's own estate; treating it as a supplier receipt will overstate purchases. Branch on shipperType before booking anything financial.

The lines​

FieldRequiredKindMeaning
lineId✅—The line's id within the delivery
itemId✅—The item
expectedQuantity✅LevelWhat the delivery note said
approvedQuantity✅LevelWhat is accepted, as it now stands
deltaQuantity✅DeltaThe change in approved quantity since this line was last published
unitPrice✅LevelPrice per unit. May be null — meaning unpriced, not free
currencyCode✅—Three-letter currency of unitPrice. May be null
reasonCodes➖LevelThe line's complete current set of reasons, each with a signed quantity
orderId➖—The purchase order
orderLineId➖—The purchase order line
containers➖—The line's container chain, innermost first — its own container, then each enclosing one outwards
batchNumber➖—Batch, if the shipper supplied one
bestBeforeDate➖—Best-before date, if supplied
expirationDate➖—Expiration date, if supplied
serialNumbers➖—Serial numbers as expected — not the accepted subset

🛑 deltaQuantity is a change; approvedQuantity and reasonCodes are totals. Adding up reason code quantities across events double-counts every correction. The rules and a worked example are in Delta versus level — read it before writing a consumer.

Message attributes​

Set on every message, and usable for server-side filtering so a subscriber only receives what it cares about:

AttributeValue
Tenant-IdThe tenant
Business-Unit-IdThe receiving store
Delivery-IdSame as uniqueId in the payload
Shipper-IdWho sent the goods
Shipper-TypebusinessPartnerId or businessUnitId
Correlation-IdTraces back to the request that caused this. Your own value if you supplied one on import

Filtering on Business-Unit-Id is the usual way to feed a per-store integration, and on Shipper-Type to separate supplier receipts from inter-store movements.

Consuming the events​

One delivery, many events​

🛑 An event describes one approval, not one delivery. A five-line delivery approved line by line produces five events, each carrying only the lines that approval covered. A consumer that treats the first event as the delivery's complete content will act on a fifth of it.

Every event carries at least one line. Approving something that was already approved publishes nothing, so an event always means that at least one line was newly accepted or corrected.

Accumulate by uniqueId, and expect more events for a delivery you have already seen — including days later, when a correction is recorded.

Duplicates are possible​

Delivery is at least once. The same event can arrive more than once, and a consumer must be idempotent.

A safe deduplication key is uniqueId + submittedAt for the event, or uniqueId + lineId + submittedAt at line level. A redelivery of the same approval carries identical values; a genuine new approval of the same line has a later submittedAt.

⚠ Do not deduplicate on uniqueId + lineId alone. That is exactly the pair a legitimate correction reuses, so you would discard the correction.

Do not rely on arrival order​

⚠ External subscribers get no ordering guarantee. Events for the same delivery can arrive out of order, and a correction can overtake the approval it corrects.

Order by submittedAt within a uniqueId. Two practical consequences:

  • If you keep a current approved quantity per line, ignore an event whose submittedAt is older than the one you have recorded. Applying it would revert a correction.
  • If you accumulate deltaQuantity, order matters less — the sum is the same — but you must still not apply the same event twice, so deduplication is not optional.

Event time versus expected time​

deliveryDate and submittedAt answer different questions:

UseFor
submittedAtSequencing events, and "when did we learn this?"
deliveryDateAccounting period, supplier performance, "when were the goods due?"

🛑 deliveryDate can fall on either side of submittedAt, by any amount. It is an expectation recorded by whoever sent the delivery note, not an observation:

  • Earlier — goods received on Friday and approved on Monday, or a paper note entered days late.
  • Later — goods that arrived ahead of schedule and were approved before the date the note promised.

Only one bound exists anywhere in the service: a delivery created in the store, header-first, is rejected if its date is more than 14 days in the past. There is no upper bound, and GS1 imports are not bounded at all — an imported note carries whatever date the sending system put on it.

So never order events by deliveryDate, never assume it precedes submittedAt, and never assume the two fall in the same reporting period.

deliveryDate and submittedAt answer different questions. deliveryDate is the date written on the delivery note, an expectation; submittedAt is when the store approved. Goods due Friday and approved Monday put deliveryDate first; goods that arrived early put submittedAt first. Order events by submittedAt, and use deliveryDate only for the accounting period.

New optional fields arrive on v1​

Adding an optional field is not a breaking change, so it is added to v1 rather than published as a new version. The published schema gains the field before any event carries it. Ignore fields you do not recognise, never reject a message because it carries one, and if you validate against the schema, validate against the current published copy rather than one you saved. A new version is published only for a breaking change — a field removed, renamed, retyped or made required — and the catalogue will list it.

⚠ Reason code ids are open, and enums may grow. Reason codes are the customer's own values, so treat them as opaque strings and never switch exhaustively on them. Handle a shipperType you do not recognise by falling through to a safe default rather than rejecting the message.

Nullable is not the same as absent​

unitPrice and currencyCode are always present on a line but may be null — the source system did not supply a price. That is not the same as a price of zero.

🛑 Do not coerce a null price to zero for anything financial. A null price means the delivery carries no cost information for that line and the cost has to come from somewhere else — a purchase order, a price list, or the item master. Booking it as free goods understates cost of goods sold.

The optional fields marked ➖ above may be absent or null. Assume any of them can be missing on any line.