Product Type Decorators¶
Product Type Decorators are how Badger Commerce lets a product declare extra inputs that need to be collected at add-to-basket time and then carried through to fulfilment. A "virtual gift card" needs a recipient email; a "donation" might want a donor name; a "gift card top-up" needs to know which card to credit. Each of these is implemented as a decorator — a small server-side bean that validates the inputs, stores them on the line item, and tells the basket / fulfilment subsystems how to render and process them downstream.
This page covers both halves of the contract: how decorators are wired into the platform internally, and how headless clients (the mobile POS app, third-party storefronts) use the REST registry to discover decorator schemas and submit decorator inputs.
The platform contract¶
A decorator is a Spring-registered bean implementing
ProductTypeDecorator<T>. The minimum a decorator declares:
getName()— stable identifier that products refer to viaProduct.productTypeDecorators(aSet<String>on the domain model —Product.typeDecoratorson the REST wire).populateLineItem(siteContext, lineItem, product, quantity, extraParameters)— validates the supplied attribute map, writes the accepted values ontolineItem.attributes, and throwsMissingDecoratorAttributeException/InvalidDecoratorAttributeExceptionon rejection.populateDisplayParameters(...)— provides display strings for the basket/admin views.extractProductTypeData(...)— pulls a typed value object back out of the line item for downstream code (e.g. the fulfilment pipeline).getInputAttributes()— declarative schema of the attributes the decorator collects, used by the REST registry endpoint (GET /v1/decorators). Decorators that don't collect any input (physicalGiftCard,subscriptionProduct) can leave the default empty list in place.
Decorators are discovered by Spring — drop a new @Service extending AbstractProductTypeDecorator
into common/.../products/decorators/impl/ and ProductDecoratorService picks it up at startup.
Built-in decorators¶
| Name | Required inputs | Optional inputs | Notes |
|---|---|---|---|
physicalGiftCard |
none | none | Metadata-only — marks the line as a posted card for fulfilment. |
virtualGiftCard |
recipientEmail |
recipientName, senderMessage |
Recipient is emailed the card on order completion. |
giftCardTopUp |
giftCardId or giftCardNumber |
none | Either id or number is required — both can't be expressed in schema. |
donationProduct |
none | showDonationAmount, showName, giftAidAcceptance, donorName (≤50), donorMessage (≤200) |
All defaults to anonymous, no Gift Aid. |
subscriptionProduct |
none | none | Variant carries the Stripe price key — not a decorator input. |
REST surface¶
Reading: GET /v1/decorators¶
Returns the full registry of decorators with their input schemas. Headless clients should fetch this once on startup and cache the response — it only changes on platform deploy.
[
{
"name": "virtualGiftCard",
"label": "Virtual gift card",
"description": "Delivered by email to the recipient on order completion.",
"attributes": [
{
"name": "recipientEmail",
"type": "EMAIL",
"required": true,
"label": "Recipient email",
"description": "Where to send the gift card. Required."
},
{
"name": "recipientName",
"type": "TEXT",
"required": false,
"label": "Recipient name",
"description": "Optional. Appears on the gift card."
},
{
"name": "senderMessage",
"type": "MULTILINE_TEXT",
"required": false,
"label": "Personal message",
"description": "Optional. Shown on the gift card."
}
]
}
]
label and description are resolved server-side against the request locale and the tenant's
message bundle, so the response is ready to display without further i18n work on the client.
The type enum maps onto HTML input types (TEXT, MULTILINE_TEXT, EMAIL, NUMBER, DATE,
BOOLEAN). The wire format of values in AddToBasketRequest.attributes is always a string
regardless of type.
Discovering which decorators apply to a product¶
The Product schema exposes the decorator names a product carries:
The client looks each name up in the cached registry to know which fields to render.
Submitting decorator inputs on add-to-basket¶
PUT /v1/basket HTTP/1.1
Content-Type: application/json
{
"skuId": "gift-card-50",
"quantity": 1,
"attributes": {
"recipientEmail": "[email protected]",
"senderMessage": "Happy birthday!"
}
}
The same attributes map is accepted on the batch CreateBasketRequest.basketItems[] entries.
Error responses¶
When a decorator rejects the inputs, the server returns 400 with a typed body:
{
"reason": "MISSING",
"decoratorName": "virtualGiftCard",
"attributeName": "recipientEmail",
"missingAttributes": ["recipientEmail"],
"message": "Decorator virtualGiftCard requires attributes [recipientEmail] which were not found in the request."
}
{
"reason": "INVALID",
"decoratorName": "giftCardTopUp",
"attributeName": "giftCardNumber",
"message": "Decorator giftCardTopUp rejected attribute [giftCardNumber] because of [Invalid card number.]."
}
The same 400 path also returns the existing InvalidVariantSelectionError body for unrelated
variant-selection problems; the two schemas have disjoint reason codes so a client can switch
on reason without inspecting a discriminator.
Adding a new decorator¶
- Create a class under
common/.../products/decorators/impl/extendingAbstractProductTypeDecorator<T>. - Implement
getName(),populateLineItem(...),populateDisplayParameters(...),extractProductTypeData(...). - Override
getInputAttributes()to declare the schema (skip if the decorator collects no input). - Add
decorator.<name>.label,decorator.<name>.description, and per-attributedecorator.<name>.<attribute>.label/decorator.<name>.<attribute>.descriptionentries tocommerce-core/src/main/resources/messages.properties. - Add the existing admin label key —
admin.edit.productTypeDecorator.<name>— so the admin product editor can label the decorator checkbox.
The REST registry, the admin product editor, and the storefront basket UI all pick up the new decorator automatically — no spec or controller changes are needed because the schemas are introspected at runtime.