Tutorial

How To use Custom CustomFields on Prices?

You need more fields on the Prices of your Products?

This tutorial explains how to enhance the standard Price in the commercetools platform with an extra field in case your use case requires it. In the example use case for this tutorial we’d like to have a kilo price for our products that come in different weights, like 100g or 500g. To be able to compare the prices of those products better, we’ll assign a kilo price to each product telling how much the product cost if its weight was one kilogram.

In commercetools terms we are going to extend the standard price on the ProductVariant with a Custom Type that will contain the CustomField that we need for the kiloPrice.

Create Custom Type for Price with CustomField

Before we can add the custom field kiloPrice to the PriceDraft of our ProductDraft to be created we need to come up with a Custom Type for our enhanced Price resource that will contain the CustomField kiloPrice.
With the resourceTypeIds we’ll tell the platform that we’d like to extend the Price resource in particular and in the fieldDefinitions we’ll specify that we’d like to have the CustomField named ‘kiloPrice’ of the MoneyType on our customized price that we’ll give the unique key ‘price-withKiloPrice’.

The POST request to the API endpoint /<project-id>/types contains following payload:

#!/bin/sh
curl -sH "Authorization: Bearer ACCESS_TOKEN" https://api.sphere.io/{project-key}/types -d @- << EOF
{
  "key": "price-withKiloPrice",
  "name": {
    "en": "additional custom field kiloPrice"
  },
  "resourceTypeIds": [
    "product-price"
  ],
  "fieldDefinitions": [
    {
      "type": {
        "name": "Money"
      },
      "name": "kiloPrice",
      "label": {
        "en": "kilo price",
        "de": "Kilopreis"
      },
      "required": false,
      "inputHint": "SingleLine"
    }
  ]
}
EOF
{
  "key": "price-withKiloPrice",
  "name": {
    "en": "additional custom field kiloPrice"
  },
  "resourceTypeIds": [
    "product-price"
  ],
  "fieldDefinitions": [
    {
      "type": {
        "name": "Money"
      },
      "name": "kiloPrice",
      "label": {
        "en": "kilo price",
        "de": "Kilopreis"
      },
      "required": false,
      "inputHint": "SingleLine"
    }
  ]
}
 final FieldDefinition fieldDefinition = FieldDefinition.of(MoneyFieldType.of(),
         "kiloPrice",
         LocalizedString.of(Locale.ENGLISH, "kilo price", Locale.GERMAN, "Kilopreis"),
         false,
         TextInputHint.SINGLE_LINE);
 final TypeDraftDsl typeDraft = TypeDraftBuilder.of("price-withKiloPrice", en("additional custom field kiloPrice"), asSet("product-price"))
         .fieldDefinitions(asList(fieldDefinition))
         .build();
 return client.execute(TypeCreateCommand.of(typeDraft));
<?php
 $key = 'price-withKiloPrice';
 $name = LocalizedString::ofLangAndText('en', 'additional custom field kiloPrice');
 $description= LocalizedString::of();
 $resourceTypeIds= ['product-price'];
 $typeDraft = TypeDraft::ofKeyNameDescriptionAndResourceTypes($key, $name, $description, $resourceTypeIds);
 $typeDraft->setFieldDefinitions(
     FieldDefinitionCollection::of()
         ->add(
             FieldDefinition::of()
                 ->setType(
                     FieldType::of()
                         ->setName('Money')
                 )
                 ->setName('kiloPrice')
                 ->setLabel(
                     LocalizedString::of()
                         ->add('en', 'kilo price')
                         ->add('de', 'Kilopreis')
                 )
                 ->setRequired(false)
                 ->setInputHint('SingleLine')
         )
 );
 $request = TypeCreateRequest::ofDraft($typeDraft);
 $response = $client->execute(TypeCreateRequest::ofDraft($typeDraft));
 $customType = $request->mapResponse($response);

After successful creation the HTTP status code should be 201 and the response will contain our new Custom Type with the custom field of name “kiloPrice”:

{
  "id": "<customized-price-type-id>",
  "version": 1,
  "key": "price-withKiloPrice",
  "name": {
    "en": "additional custom field kiloPrice"
  },
  "resourceTypeIds": [
    "product-price"
  ],
  "fieldDefinitions": [
    {
      "name": "kiloPrice",
      "label": {
        "en": "kilo price",
        "de": "Kilopreis"
      },
      "required": false,
      "type": {
        "name": "Money"
      },
      "inputHint": "SingleLine"
    }
  ],
  "createdAt": "2015-10-14T09:17:14.734Z",
  "lastModifiedAt": "2015-10-14T09:17:14.734Z"
}

Create Product with CustomField on Price

Now we are able to create a product with the standard price as well as the kiloPrice we introduced before.
For this tutorial we assume that you have created a ProductType before that we now use for creating our ProductDraft.

Let’s do so by sending a POST request on the API endpoint /<project-id>/products with following payload:

#!/bin/sh
curl -sH "Authorization: Bearer ACCESS_TOKEN" https://api.sphere.io/{project-key}/products -d @- << EOF
{
  "productType": {
      "typeId":"product-type",
      "id":"<product-type-id>"
  },
  "name": {
    "en": "Product with kilo price"
  },
  "slug": {
    "en": "product_slug_kilo_price"
  },
  "masterVariant": {
    "sku": "SKU-kiloPrice",
    "prices": [
      {
        "value": {
          "currencyCode": "EUR",
          "centAmount": 100
        },
        "custom": {
            "typeId": "<customized-price-type-id>",
            "fields": {
                "kiloPrice": {
                    "currencyCode": "EUR",
                    "centAmount": 1000
                }
            }
        }
      }
    ]
  }
}
EOF
{
  "productType": {
    "typeId":"product-type",
    "id":"<product-type-id>"
  },
  "name": {
    "en": "Product with kilo price"
  },
  "slug": {
    "en": "product_slug_kilo_price"
  },
  "masterVariant": {
    "sku": "SKU-kiloPrice",
    "prices": [
      {
        "value": {
          "currencyCode": "EUR",
          "centAmount": 100
        },
        "custom": {
          "typeId": "<customized-price-type-id>",
          "fields": {
            "kiloPrice": {
              "currencyCode": "EUR",
              "centAmount": 1000
            }
          }
        }
      }
    ]
  }
}
 final MonetaryAmount monetaryAmountForKiloPrice = PriceDraft.of(BigDecimal.TEN, DefaultCurrencyUnits.EUR).getValue();
 final CustomFieldsDraft kiloPriceCustomFieldDraft = CustomFieldsDraftBuilder.ofType(customTypeForPrice)
         .addObject("kiloPrice", monetaryAmountForKiloPrice)
         .build();
 final PriceDraftDsl productPriceDraft = PriceDraft.of(BigDecimal.ONE, DefaultCurrencyUnits.EUR)
         .withCustom(kiloPriceCustomFieldDraft);
 final ProductVariantDraft productVariantDraft = ProductVariantDraftBuilder.of()
         .sku("SKU-kiloPrice")
         .prices(asList(productPriceDraft))
         .build();
 final ProductDraft productDraft = ProductDraftBuilder.of(productType, en("Product with kilo price"),
         en("product_slug_kilo_price"), productVariantDraft)
         .build();
 return client.execute(ProductCreateCommand.of(productDraft));
<?php
 $name = LocalizedString::ofLangAndText('en', 'Product with kilo price');
 $slug = LocalizedString::ofLangAndText('en', 'product_slug_kilo_price');
 $productDraft = ProductDraft::ofTypeNameAndSlug(
     $productType->getReference(),
     $name,
     $slug
 );
 $productDraft->setMasterVariant(
     ProductVariantDraft::of()
         ->setSku('SKU-kiloPrice')
         ->setPrices(
             PriceDraftCollection::of()
                 ->add(
                     PriceDraft::of()
                         ->setValue(Money::ofCurrencyAndAmount('EUR', 100))
                         ->setCustom(
                             CustomFieldObject::of()
                                 ->setType(
                                     TypeReference::ofId($customType->getId())
                                 )
                                 ->setFields(
                                     FieldContainer::of()
                                         ->set(
                                             "kiloPrice",
                                             Money::ofCurrencyAndAmount('EUR', 1000)
                                         )
                                 )
                         )
                 )
         )
 );
 $request = ProductCreateRequest::ofDraft($productDraft);
 $response = $client->execute(ProductCreateRequest::ofDraft($productDraft));
 $product = $request->mapResponse($response);

You should have received a response to that request with the HTTP status code 201 containing the Product with the customized Price resource:

{
  "id": "<created-product-id>",
  "version": 1,
  "productType": {
    "typeId": "product-type",
    "id": "<product-type-id>"
  },
  "catalogs": [],
  "masterData": {
    "current": {
      "name": {
        "en": "Product with kilo price"
      },
      "categories": [],
      "categoryOrderHints": {},
      "slug": {
        "en": "product_slug_kilo_price"
      },
      "masterVariant": {
        "id": 1,
        "sku": "SKU-1",
        "prices": [
          {
            "value": {
              "currencyCode": "EUR",
              "centAmount": 100
            },
            "id": "<created-product-price-id>",
            "custom": {
              "type": {
                "typeId": "type",
                "id": "<customized-price-type-id>"
              },
              "fields": {
                "kiloPrice": {
                    "currencyCode": "EUR",
                    "centAmount": 1000
                }
              }
            }
          }
        ],
        "images": [],
        "attributes": []
      },
      "variants": [],
      "searchKeywords": {}
    },
    "staged": {
      "name": {
        "en": "Product with kilo price"
      },
      "categories": [],
      "categoryOrderHints": {},
      "slug": {
        "en": "product_slug_kilo_price"
      },
      "masterVariant": {
        "id": 1,
        "sku": "SKU-1",
        "prices": [
          {
            "value": {
              "currencyCode": "EUR",
              "centAmount": 100
            },
            "id": "<created-product-price-id>",
            "custom": {
              "type": {
                "typeId": "type",
                "id": "<customized-price-type-id>"
              },
              "fields": {
                "kiloPrice": {
                    "currencyCode": "EUR",
                    "centAmount": 1000
                }
              }
            }
          }
        ],
        "images": [],
        "attributes": []
      },
      "variants": [],
      "searchKeywords": {}
    },
    "published": false,
    "hasStagedChanges": false
  },
  "catalogData": {},
  "lastVariantId": 1,
  "createdAt": "2015-10-14T12:55:01.518Z",
  "lastModifiedAt": "2015-10-14T12:55:01.518Z",
  "lastMessageSequenceNumber": 0
}

Update Product with CustomField on Price

Creating new products was not a big deal, but what if I want to update existing products with the kiloPrice attribute that we introduced after the product was created?

That’s also possible with the update actions Set Price Custom Type and Set Price CustomField. In this section we’ll update the following example product that contains only the standard Price so far:

{
  "id": "<product-to-be-updated-id>",
  "version": 1,
  "productType": {
    "typeId": "product-type",
    "id": "<product-type-id>"
  },
  "catalogs": [],
  "masterData": {
    "current": {
      "name": {
        "en": "Product with Standard Price"
      },
      "categories": [],
      "categoryOrderHints": {},
      "slug": {
        "en": "product_slug_standard_price"
      },
      "masterVariant": {
        "id": 1,
        "sku": "SKU-0",
        "prices": [
          {
            "value": {
              "currencyCode": "EUR",
              "centAmount": 100
            },
            "id": "<updated-product-price-id>"
          }
        ],
        "images": [],
        "attributes": []
      },
      "variants": [],
      "searchKeywords": {}
    },
    "staged": {
      "name": {
        "en": "Product with Standard Price"
      },
      "categories": [],
      "categoryOrderHints": {},
      "slug": {
        "en": "product_slug_standard_price"
      },
      "masterVariant": {
        "id": 1,
        "sku": "SKU-0",
        "prices": [
          {
            "value": {
              "currencyCode": "EUR",
              "centAmount": 100
            },
            "id": "<updated-product-price-id>"
          }
        ],
        "images": [],
        "attributes": []
      },
      "variants": [],
      "searchKeywords": {}
    },
    "published": false,
    "hasStagedChanges": false
  },
  "catalogData": {},
  "lastVariantId": 1,
  "createdAt": "2015-10-14T11:52:33.555Z",
  "lastModifiedAt": "2015-10-14T11:52:33.555Z",
  "lastMessageSequenceNumber": 0
}  

Similar to what we did with the ProductDraft in the section about creating the product we need to update the existing Product with the customized Price first before we can actually set the kiloPrice field to it:

We’ll be using the update action Set Price Custom Type for doing this. Let’s assign a price-withKiloPrice-type Price resource to our existing product “SKU-0” by sending the payload below with our update request to the products API endpoint:

POST /{project-id}/products/{id-of-product-SKU-0} with following payload:

#!/bin/sh
curl -sH "Authorization: Bearer ACCESS_TOKEN" https://api.sphere.io/{project-key}/products/{product-id} -d @- << EOF
{
  "version": 1,
  "actions": [
    {
      "action": "setProductPriceCustomType",
      "typeKey": "price-withKiloPrice",
      "priceId":"<updated-product-price-id>"
    }
  ]
}
EOF
{
  "version": 1,
  "actions": [
    {
      "action": "setProductPriceCustomType",
      "typeKey": "price-withKiloPrice",
      "priceId":"<updated-product-price-id>"
    }
  ]
}
 final SetProductPriceCustomType setProductPriceCustomType = SetProductPriceCustomType
         .ofTypeKeyAndObjects("price-withKiloPrice", Collections.emptyMap(), productPriceId);
 final ProductUpdateCommand productUpdateCommand = ProductUpdateCommand.of(productToUpdate, setProductPriceCustomType);
 return client.execute(productUpdateCommand);
<?php
 $request = ProductUpdateRequest::ofIdAndVersion($product->getId(), $product->getVersion());
 $request->addAction(ProductSetPriceCustomTypeAction::ofTypeKey('price-withKiloPrice')->setPriceId($priceId));
 $response = $request->executeWithClient($client);
 $product = $request->mapFromResponse($response);

From the response to the update action you’ll see that the price in the master variant of the staged projection now contains the custom field, but it does not appear in the master variant of the current projection:

{
  "id": "<product-to-be-updated-id>",
  "version": 2,
  "productType": {
    "typeId": "product-type",
    "id": "<product-type-id>"
  },
  "catalogs": [],
  "masterData": {
    "current": {
      "name": {
        "en": "Product with Standard Price"
      },
      "categories": [],
      "categoryOrderHints": {},
      "slug": {
        "en": "product_slug_standard_price"
      },
      "masterVariant": {
        "id": 1,
        "sku": "SKU-0",
        "prices": [
          {
            "value": {
              "currencyCode": "EUR",
              "centAmount": 100
            },
            "id": "<updated-product-price-id>"
          }
        ],
        "images": [],
        "attributes": []
      },
      "variants": [],
      "searchKeywords": {}
    },
    "staged": {
      "name": {
        "en": "Product with Standard Price"
      },
      "categories": [],
      "categoryOrderHints": {},
      "slug": {
        "en": "product_slug_standard_price"
      },
      "masterVariant": {
        "id": 1,
        "sku": "SKU-0",
        "prices": [
          {
            "value": {
              "currencyCode": "EUR",
              "centAmount": 100
            },
            "id": "<updated-product-price-id>",
            "custom": {
              "type": {
                "typeId": "type",
                "id": "<customized-price-type-id>"
              },
              "fields": {}
            }
          }
        ],
        "images": [],
        "attributes": []
      },
      "variants": [],
      "searchKeywords": {}
    },
    "published": false,
    "hasStagedChanges": true
  },
  "catalogData": {},
  "lastVariantId": 1,
  "createdAt": "2015-10-14T11:52:33.555Z",
  "lastModifiedAt": "2015-10-14T16:17:56.430Z",
  "lastMessageSequenceNumber": 0
}

This is no surprise since the platform behaves according to the usual product data workflow in which you update the staged projection first and then you publish the product. After publish also the current projection will contain the custom fields.

As you can see, the customized price has been set, but there is no custom field given so far; the fields are empty:

  fields:{}

So, we’ll have to assign the kiloPrice in another update action called Set Price CustomField.

POST /{project-id}/customers/{id-of-product-SKU-0} with following payload:

#!/bin/sh
curl -sH "Authorization: Bearer ACCESS_TOKEN" https://api.sphere.io/{project-key}/products/{product-id} -d @- << EOF
{
  "version": 2,
  "actions": [
    {
      "action": "setProductPriceCustomField",
      "name": "kiloPrice",
      "priceId": "<updated-product-price-id>",
      "value": {
          "currencyCode": "EUR",
          "centAmount": 2000
      }
    }
  ]
}
EOF
{
  "version": 2,
  "actions": [
    {
      "action": "setProductPriceCustomField",
      "name": "kiloPrice",
      "priceId": "<updated-product-price-id>",
      "value": {
        "currencyCode": "EUR",
        "centAmount": 2000
      }
    }
  ]
}
 final MonetaryAmount monetaryAmount = PriceDraft.of(BigDecimal.valueOf(20), DefaultCurrencyUnits.EUR).getValue();
 final SetProductPriceCustomField setProductPriceCustomFieldUpdateAction = SetProductPriceCustomField
         .ofObject("kiloPrice", monetaryAmount, productPriceId);
 final ProductUpdateCommand productUpdateCommand = ProductUpdateCommand
         .of(productWithPriceWithCustomType, setProductPriceCustomFieldUpdateAction);
 return client.execute(productUpdateCommand);
<?php
 $request = ProductUpdateRequest::ofIdAndVersion($product->getId(), $product->getVersion());
 $request->addAction(
     ProductSetPriceCustomFieldAction::ofName('kiloPrice')
         ->setPriceId($priceId)
         ->setValue(Money::ofCurrencyAndAmount('EUR', 2000))
 );
 $response = $request->executeWithClient($client);
 $product = $request->mapFromResponse($response);

In the response we’ll now find the kiloPrice field with the appropriate value:

  "fields": {
    "kiloPrice": {
      "currencyCode": "EUR",
      "centAmount": 2000
    }
  }  

That’s it. We are now done with updating an existing product price with a custom field.

Query Product with CustomField on Price

With the new ‘kiloPrice’ on our Product we are able to query for this field. Let’s say we’d like to get all the products with a kilo price of 1000 cents. For this we need to use a query predicate on the kiloPrice field of the form kiloPrice(centAmount = 1000).
Since it is a CustomField of a price we’ll need to mark it like that, like so: (prices(custom(fields(kiloPrice(centAmount = 1000)))))

The complete query request on the Products endpoint looks like below:

GET {projectKey}/products?where=masterData(staged(masterVariant(prices(custom(fields(kiloPrice(centAmount = 1000)))))))
curl -sH "Authorization: Bearer ACCESS_TOKEN" https://api.sphere.io/{project-key}/products?where=masterData%28staged%28masterVariant%28prices%28custom%28fields%28kiloPrice%28centAmount+%3D+1000%29%29%29%29%29%29%29
 final ProductQuery productQuery = ProductQuery.of()
         .withPredicates(m -> m.masterData().staged().masterVariant().prices().custom().fields().ofMoney("kiloPrice").centAmount().is(1000L));
 return client.execute(productQuery);
<?php
 $request = ProductQueryRequest::of()
                 ->where('masterData(staged(masterVariant(prices(custom(fields(kiloPrice(centAmount = 1000)))))))');
 $response = $request->executeWithClient($client);
 $product = $request->mapFromResponse($response);

The response to that query should only contain the products with a kilo price of 1000 cents in your project.