
Sap Commerce Cloud
- 52 installs
- 12 repo stars
- Updated March 19, 2026
- emenowicz/sap-commerce-skill
Helps with ai & agent building tasks.
About
sap-commerce-cloud is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- sap-commerce-cloud
- AI & Agent Building
- AI-coding skill
Sap Commerce Cloud by the numbers
- 52 all-time installs (skills.sh)
- Ranked #7,142 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Jul 24, 2026 (Skillselion catalog sync)
npx skills add https://github.com/emenowicz/sap-commerce-skill --skill sap-commerce-cloudAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 52 |
|---|---|
| repo stars | ★ 12 |
| Last updated | March 19, 2026 |
| Repository | emenowicz/sap-commerce-skill ↗ |
What it does
Helps with ai & agent building tasks.
Files
SAP Commerce Development
Overview
SAP Commerce Cloud (formerly Hybris) is an enterprise e-commerce platform built on Java and Spring. This skill provides guidance for extension development, type system modeling, service layer implementation, data management, API customization, accelerator patterns, Composable Storefront (Spartacus/Angular) integration, CCv2 cloud deployment, SAP BTP/Kyma integration, SmartEdit customization, and testing patterns for both Cloud (CCv2) and On-Premise deployments.
Modern Storefront Note: The JSP-based Accelerator storefront (Spring Web Flow, JSP/JSTL) is the legacy approach. The modern recommended storefront is Composable Storefront (formerly Spartacus) — an Angular/TypeScript Single Page Application that communicates with the backend exclusively via OCC REST APIs. New projects should use Composable Storefront. See composable-storefront.md.
Core Architecture
Type System
Data modeling via items.xml defines item types, attributes, relations, and enumerations. Types are compiled into Java classes during build. See type-system.md for syntax and patterns.
Extensions
Modular architecture where functionality lives in extensions. Each extension has extensioninfo.xml for metadata, items.xml for types, and *-spring.xml for beans. See extension-development.md.
Service Layer
Four-layer architecture: Facade (API for controllers) → Service (business logic) → DAO (data access) → Model (generated from types). See service-layer-architecture.md.
Data Management
ImpEx: Scripting language for data import/export. See impex-guide.md. FlexibleSearch: SQL-like query language for items. See flexiblesearch-reference.md.
OCC API
RESTful web services exposing commerce functionality. Controllers use DataMapper for DTO conversion. See occ-api-development.md.
Accelerators (Legacy JSP Storefront)
Pre-built JSP/Spring MVC storefronts: B2C (retail) and B2B (enterprise with approval workflows). Legacy approach — new projects should use Composable Storefront or a headless approach. See accelerator-customization.md.
Composable Storefront (Spartacus)
Angular/TypeScript SPA that replaces JSP accelerators. Communicates exclusively via OCC APIs. Supports PWA, SSR, and feature libraries for B2C/B2B. See composable-storefront.md.
Headless Commerce
SAP Commerce Cloud as a headless backend — OCC APIs consumed by any frontend (React, Vue, mobile apps, etc.). The backend serves product, catalog, cart, order, and user data via REST. Composable Storefront is SAP's reference headless frontend, but custom frontends are supported.
CCv2 Cloud Deployment
SAP Commerce Cloud v2 (CCv2) is the managed cloud offering. Deployment is driven by manifest.json defining extensions, aspects (storefront/api/backoffice), and properties. See ccv2-deployment.md.
SAP BTP Integration
SAP Business Technology Platform integration via API Registry module and SAP BTP Extensions Integration Module (Kyma runtime). Allows microservices/serverless functions to react to Commerce events. See sap-btp-integration.md.
CronJobs & Task Engine
Scheduled and asynchronous job execution. Define AbstractJobPerformable implementations, configure via ServicelayerJob + CronJob + Trigger in ImpEx. See cronjob-task-engine.md.
Business Processes
Stateful workflows for order processing, returns, approvals, and custom flows. XML process definitions with action beans (AbstractSimpleDecisionAction, AbstractAction), wait states, and event triggers. See business-process.md.
Solr Search
Product search and faceted navigation powered by Apache Solr. Configure indexed types, properties, value providers, and boost rules. See solr-search-configuration.md.
Promotions & Rule Engine
Drools-based promotion rules with conditions and actions. Supports order/product/customer promotions, coupons, and custom actions. See promotions-rule-engine.md.
Caching
Multi-layered caching: platform region caches (entity, query, typesystem), Spring @Cacheable, and cluster-aware invalidation. See caching-guide.md.
Backoffice
Administration UI customization via cockpitng: widget configuration, custom editors, search/list views, wizards, and deep links. See backoffice-configuration.md.
Common Workflows
Create a Custom Extension
1. Generate structure with ant extgen or use template from assets/extension-structure/ 2. Configure extensioninfo.xml with dependencies 3. Define types in items.xml 4. Configure beans in *-spring.xml 5. Add to localextensions.xml 6. Build: ant clean all
Reference: extension-development.md | Template: assets/extension-structure/
Define Custom Item Types
1. Create or modify *-items.xml in extension 2. Define itemtype with attributes, relations 3. Build to generate model classes 4. Use in services/DAOs
Reference: type-system.md | Templates: assets/item-type-definition/
Implement Service Layer
1. Create DTO class for data transfer 2. Create DAO interface and implementation with FlexibleSearch 3. Create Service interface and implementation with business logic 4. Create Facade interface and implementation for external API 5. Configure beans in *-spring.xml
Reference: service-layer-architecture.md | Templates: assets/service-layer/
Import Data with ImpEx
1. Create .impex file with header and data rows 2. Use INSERT_UPDATE for create/modify operations 3. Define macros for reusable values 4. Import via HAC or ant importImpex
Reference: impex-guide.md | Templates: assets/impex-scripts/
Query with FlexibleSearch
SELECT {pk} FROM {Product} WHERE {code} = ?code
SELECT {p.pk} FROM {Product AS p JOIN Category AS c ON {p.supercategories} = {c.pk}} WHERE {c.code} = ?categoryReference: flexiblesearch-reference.md | Examples: assets/flexiblesearch-queries/
Customize OCC API
1. Create controller with @Controller and @RequestMapping 2. Create WsDTO for response data 3. Create populator for model-to-DTO conversion 4. Register in *-web-spring.xml
Reference: occ-api-development.md | Templates: assets/occ-customization/
Extend Accelerator Checkout
1. Create custom checkout step implementing CheckoutStep 2. Configure in checkout flow XML 3. Create JSP for step UI 4. Register beans in *-spring.xml
Reference: accelerator-customization.md | Templates: assets/checkout-customization/
Create a Scheduled Job (CronJob)
1. Implement AbstractJobPerformable<CronJobModel> with perform() method 2. Register Spring bean with parent="abstractJobPerformable" 3. Create ServicelayerJob, CronJob, and Trigger via ImpEx 4. Test via HAC > Platform > CronJobs
Reference: cronjob-task-engine.md | Templates: assets/cronjob/
Define a Business Process
1. Create process definition XML in resources/processes/ 2. Implement action beans extending AbstractSimpleDecisionAction 3. Register action beans in Spring with parent="abstractAction" 4. Register process definition via ProcessDefinitionResource Spring bean 5. Start process via BusinessProcessService.createProcess() and startProcess()
Reference: business-process.md | Templates: assets/business-process/
Configure Solr Product Search
1. Set up SolrFacetSearchConfig with server, languages, currencies, catalog versions 2. Define SolrIndexedType for Product 3. Configure SolrIndexedProperty entries (searchable, facet, sortable) 4. Set up indexer CronJobs (full nightly, incremental every 5 min) 5. Test search via HAC > Platform > Solr
Reference: solr-search-configuration.md | Templates: assets/solr-configuration/
Set Up Promotions
1. Create PromotionGroup for your store 2. Define PromotionSourceRule with conditions and actions 3. Configure coupons (SingleCodeCoupon / MultiCodeCoupon) if needed 4. Publish rules to activate them 5. Test in Backoffice > Marketing > Promotion Rules
Reference: promotions-rule-engine.md | Templates: assets/promotions/
Integrate Composable Storefront (Spartacus)
1. Set up OCC endpoints and CORS on the Commerce backend 2. Scaffold Angular app: ng add @spartacus/schematics@latest --base-url <OCC_URL> --base-site=<SITE_ID> --ssr 3. Configure SpartacusConfigurationModule with provideConfig 4. Customize CMS components via cmsComponents mapping in provideConfig 5. Override services by providing custom service classes in feature modules
Reference: composable-storefront.md | Templates: assets/composable-storefront/
Deploy to CCv2
1. Configure manifest.json at repository root with commerceSuiteVersion, extensions, aspects 2. Set environment properties in CCv2 Cloud Portal or property files 3. Push to connected repository to trigger build 4. Promote build through environments (dev → staging → production)
Reference: ccv2-deployment.md
Integrate SAP BTP / Kyma
1. Configure API Registry module to expose Commerce APIs and events 2. Connect Kyma runtime via SAP BTP Extensions Integration Module 3. Implement microservices/lambdas to react to Commerce events 4. Use API Registry for webhook-based outbound communication
Reference: sap-btp-integration.md
Write Tests for Custom Extension
1. Annotate unit tests with @UnitTest, integration tests with @IntegrationTest 2. Unit tests: use Mockito mocks, no Spring context, place in testsrc/ 3. Integration tests: extend ServicelayerTest or ServicelayerTransactionalTest 4. OCC API integration tests: use REST Assured or MockMvc against a running OCC endpoint 5. Load test data via importCsv("/myext/test/testdata.impex", "utf-8") 6. Run locally: ant unittests -Dtestclasses.extensions=myextension 7. Configure CCv2 test run in manifest.json under "tests"
Reference: testing-guide.md
Quick Reference
| Task | Reference | Assets | Script |
|---|---|---|---|
| Extension setup | extension-development.md | assets/extension-structure/ | scripts/generate-extension.sh |
| Type definitions | type-system.md | assets/item-type-definition/ | - |
| Service layer | service-layer-architecture.md | assets/service-layer/ | - |
| Data import | impex-guide.md | assets/impex-scripts/ | scripts/validate-impex.sh |
| Queries | flexiblesearch-reference.md | assets/flexiblesearch-queries/ | scripts/query-items.sh |
| API customization | occ-api-development.md | assets/occ-customization/ | - |
| Checkout/Storefront (legacy) | accelerator-customization.md | assets/checkout-customization/ | - |
| Composable Storefront | composable-storefront.md | assets/composable-storefront/ | - |
| CCv2 deployment | ccv2-deployment.md | - | - |
| SAP BTP/Kyma | sap-btp-integration.md | - | - |
| Testing | testing-guide.md | - | - |
| Spring config | spring-configuration.md | - | - |
| Data patterns | data-modeling-patterns.md | - | - |
| Troubleshooting | troubleshooting-guide.md | - | - |
| CronJobs | cronjob-task-engine.md | assets/cronjob/ | - |
| Business processes | business-process.md | assets/business-process/ | - |
| Solr search | solr-search-configuration.md | assets/solr-configuration/ | - |
| Promotions | promotions-rule-engine.md | assets/promotions/ | - |
| Caching | caching-guide.md | - | - |
| Backoffice | backoffice-configuration.md | - | - |
Resources
scripts/
Utility scripts for common tasks:
generate-extension.sh- Scaffold new extension structurevalidate-impex.sh- Validate ImpEx syntax before importquery-items.sh- Execute FlexibleSearch queries via HAC
references/
Detailed guides for each topic area. Load as needed for in-depth information.
assets/
Production-quality code templates ready to copy and customize. Organized by domain (service-layer, impex-scripts, etc.).
<?xml version="1.0" encoding="utf-8"?>
<!--
Custom business process definition template.
Place in: resources/processes/custom-process.xml
Register via Spring bean (ProcessDefinitionResource) or ImpEx.
Nodes:
- <action>: Executes a Spring bean, follows a transition
- <wait>: Pauses until an event is received
- <end>: Terminates the process
- <split>/<join>: Parallel execution
-->
<process xmlns="http://www.hybris.de/xsd/processdefinition"
name="customFulfillmentProcess"
start="validateOrder"
onError="error">
<!-- Step 1: Validate the order -->
<action id="validateOrder" bean="validateOrderAction">
<transition name="OK" to="checkPayment"/>
<transition name="NOK" to="error"/>
</action>
<!-- Step 2: Check payment status -->
<action id="checkPayment" bean="checkPaymentAction">
<transition name="OK" to="allocateStock"/>
<transition name="NOK" to="waitForPayment"/>
</action>
<!-- Wait for external payment confirmation -->
<wait id="waitForPayment" then="checkPayment" prependProcessCode="true">
<event>PaymentConfirmed</event>
</wait>
<!-- Step 3: Allocate stock for order entries -->
<action id="allocateStock" bean="allocateStockAction">
<transition name="OK" to="sendConfirmation"/>
<transition name="NOK" to="waitForStock"/>
</action>
<!-- Wait for stock availability -->
<wait id="waitForStock" then="allocateStock" prependProcessCode="true">
<event>StockAvailable</event>
</wait>
<!-- Step 4: Send order confirmation email -->
<action id="sendConfirmation" bean="sendConfirmationEmailAction">
<transition name="OK" to="success"/>
<transition name="NOK" to="error"/>
</action>
<!-- End states -->
<end id="success" state="SUCCEEDED">Order fulfilled successfully</end>
<end id="error" state="ERROR">Order fulfillment failed</end>
</process>
package com.example.actions;
import de.hybris.platform.core.model.order.OrderModel;
import de.hybris.platform.orderprocessing.model.OrderProcessModel;
import de.hybris.platform.processengine.action.AbstractSimpleDecisionAction;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Business process action template using AbstractSimpleDecisionAction.
*
* Returns OK or NOK transition based on the action's logic.
* Register as a Spring bean with parent="abstractAction".
*
* For actions with more than two outcomes, extend AbstractAction instead
* and override getTransitions() to declare custom transition names.
*/
public class CustomProcessAction extends AbstractSimpleDecisionAction<OrderProcessModel> {
private static final Logger LOG = LoggerFactory.getLogger(CustomProcessAction.class);
@Override
public Transition executeAction(final OrderProcessModel process) {
final OrderModel order = process.getOrder();
if (order == null) {
LOG.error("Process {} has no order attached", process.getCode());
return Transition.NOK;
}
LOG.info("Processing order: {}", order.getCode());
try {
// TODO: Replace with actual business logic
// Examples:
// - Validate order entries
// - Check payment authorization
// - Reserve inventory
// - Call external fulfillment API
// - Send notification emails
final boolean success = performAction(order);
if (success) {
LOG.info("Action completed successfully for order: {}", order.getCode());
return Transition.OK;
} else {
LOG.warn("Action failed for order: {}", order.getCode());
return Transition.NOK;
}
} catch (final Exception e) {
LOG.error("Unexpected error processing order: {}", order.getCode(), e);
return Transition.NOK;
}
}
private boolean performAction(final OrderModel order) {
// TODO: Implement action logic
return true;
}
}
# Business Process Setup ImpEx
# Registers process definitions and optionally creates test process instances.
# -----------------------------------------------
# Process Definition Registration (alternative to Spring bean)
# -----------------------------------------------
# Typically process definitions are registered via Spring beans
# (ProcessDefinitionResource). Use ImpEx only if Spring is not suitable.
# INSERT_UPDATE ProcessDefinition; code[unique=true] ; resource
# ; customFulfillmentProcess ; jar:com.example.constants.MyExtensionConstants&/myextension/processes/custom-process.xml
# -----------------------------------------------
# Test Process Instances (for development/testing only)
# -----------------------------------------------
# Create order process instances linked to existing orders.
# The process engine will execute them based on the process definition.
# INSERT_UPDATE OrderProcess; code[unique=true] ; processDefinitionName ; order(code)
# ; testFulfillment-order001-$currentTimestamp ; customFulfillmentProcess ; testOrder001
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!--
Business process Spring configuration.
Each action bean must have parent="abstractAction" to inherit
process engine integration (modelService, processParameterHelper, etc.).
-->
<!-- Action beans -->
<bean id="validateOrderAction"
class="com.example.actions.ValidateOrderAction"
parent="abstractAction"/>
<bean id="checkPaymentAction"
class="com.example.actions.CheckPaymentAction"
parent="abstractAction">
<property name="paymentService" ref="paymentService"/>
</bean>
<bean id="allocateStockAction"
class="com.example.actions.AllocateStockAction"
parent="abstractAction">
<property name="stockService" ref="stockService"/>
</bean>
<bean id="sendConfirmationEmailAction"
class="com.example.actions.SendConfirmationEmailAction"
parent="abstractAction">
<property name="emailService" ref="emailService"/>
</bean>
<!--
Process definition resource registration.
This tells the process engine where to find the XML definition.
-->
<bean id="customFulfillmentProcessDefinitionResource"
class="de.hybris.platform.processengine.definition.ProcessDefinitionResource">
<property name="resource" value="classpath:/processes/custom-process.xml"/>
</bean>
</beans>
<?xml version="1.0" encoding="UTF-8"?>
<!--
checkout-flow.xml
Spring Web Flow definition for checkout process.
Place in WEB-INF/config/spring-web-flow.xml
-->
<flow xmlns="http://www.springframework.org/schema/webflow"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/webflow
http://www.springframework.org/schema/webflow/spring-webflow-2.0.xsd">
<!-- Flow variables -->
<var name="checkoutForm" class="com.example.storefront.forms.CheckoutForm"/>
<!-- Secure the flow -->
<secured attributes="ROLE_CUSTOMERGROUP"/>
<!-- Start state - redirect to first step -->
<action-state id="start">
<evaluate expression="checkoutFlowFacade.initializeCheckout()"/>
<transition to="deliveryAddress"/>
</action-state>
<!-- Delivery Address Step -->
<view-state id="deliveryAddress" view="pages/checkout/multi/deliveryAddressPage">
<on-entry>
<evaluate expression="checkoutFlowFacade.getDeliveryAddresses()"
result="flowScope.deliveryAddresses"/>
<evaluate expression="checkoutFlowFacade.getCountries()"
result="flowScope.countries"/>
</on-entry>
<transition on="selectAddress" to="deliveryAddress">
<evaluate expression="checkoutFlowFacade.setDeliveryAddress(requestParameters.addressId)"/>
</transition>
<transition on="next" to="deliveryMethod">
<evaluate expression="checkoutFlowFacade.hasDeliveryAddress()"
result="flowScope.hasAddress"/>
</transition>
<transition on="error" to="deliveryAddress"/>
</view-state>
<!-- Delivery Method Step -->
<view-state id="deliveryMethod" view="pages/checkout/multi/deliveryMethodPage">
<on-entry>
<evaluate expression="checkoutFlowFacade.getDeliveryModes()"
result="flowScope.deliveryModes"/>
</on-entry>
<transition on="selectDeliveryMethod" to="deliveryMethod">
<evaluate expression="checkoutFlowFacade.setDeliveryMode(requestParameters.deliveryModeId)"/>
</transition>
<transition on="next" to="customStep"/>
<transition on="back" to="deliveryAddress"/>
</view-state>
<!-- Custom Step (inserted into flow) -->
<view-state id="customStep" view="pages/checkout/multi/customStepPage">
<on-entry>
<evaluate expression="customCheckoutFacade.getAvailableOptions()"
result="flowScope.customOptions"/>
<evaluate expression="customCheckoutFacade.getSelectedCustomOption()"
result="flowScope.selectedOption"/>
</on-entry>
<transition on="selectOption" to="customStep">
<evaluate expression="customCheckoutFacade.saveCustomOption(requestParameters.optionId)"/>
</transition>
<transition on="next" to="payment"/>
<transition on="back" to="deliveryMethod"/>
<transition on="skip" to="payment">
<!-- Allow skipping if not required -->
</transition>
</view-state>
<!-- Payment Step -->
<view-state id="payment" view="pages/checkout/multi/paymentPage">
<on-entry>
<evaluate expression="checkoutFlowFacade.getPaymentMethods()"
result="flowScope.paymentMethods"/>
</on-entry>
<transition on="addPayment" to="payment">
<evaluate expression="checkoutFlowFacade.createPaymentInfo(paymentForm)"
result="flowScope.paymentInfo"/>
</transition>
<transition on="next" to="review"/>
<transition on="back" to="customStep"/>
</view-state>
<!-- Review Step -->
<view-state id="review" view="pages/checkout/multi/reviewPage">
<on-entry>
<evaluate expression="checkoutFlowFacade.getCheckoutCart()"
result="flowScope.cartData"/>
</on-entry>
<transition on="placeOrder" to="processOrder"/>
<transition on="back" to="payment"/>
</view-state>
<!-- Process Order (action state) -->
<action-state id="processOrder">
<evaluate expression="checkoutFlowFacade.placeOrder()"
result="flowScope.orderData"/>
<transition on="success" to="orderConfirmation"/>
<transition on="error" to="review"/>
</action-state>
<!-- Order Confirmation (end state) -->
<end-state id="orderConfirmation"
view="externalRedirect:contextRelative:/checkout/orderConfirmation/${flowScope.orderData.code}"/>
<!-- Global error handling -->
<global-transitions>
<transition on-exception="java.lang.Exception" to="error"/>
</global-transitions>
<end-state id="error" view="pages/checkout/multi/errorPage"/>
</flow>
<?xml version="1.0" encoding="UTF-8"?>
<!--
checkout-spring.xml
Spring configuration for custom checkout beans.
-->
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- Custom Checkout Facade -->
<alias name="defaultCustomCheckoutFacade" alias="customCheckoutFacade"/>
<bean id="defaultCustomCheckoutFacade"
class="com.example.facades.impl.DefaultCustomCheckoutFacade">
<property name="cartService" ref="cartService"/>
<property name="modelService" ref="modelService"/>
<property name="customOptionService" ref="customOptionService"/>
</bean>
<!-- Custom Checkout Step Controller -->
<bean id="customCheckoutStepController"
class="com.example.storefront.controllers.pages.checkout.steps.CustomCheckoutStepController">
<property name="customCheckoutFacade" ref="customCheckoutFacade"/>
</bean>
<!-- Checkout Step Configuration -->
<bean id="customCheckoutStep"
class="de.hybris.platform.acceleratorstorefrontcommons.checkout.steps.CheckoutStep">
<property name="checkoutGroup" ref="defaultCheckoutGroup"/>
<property name="checkoutStepValidator" ref="deliveryMethodCheckoutStepValidator"/>
<property name="transitions">
<map>
<entry key="previous" value="redirect:/checkout/multi/delivery-method"/>
<entry key="next" value="redirect:/checkout/multi/payment-method"/>
</map>
</property>
<property name="progressBarId" value="customStep"/>
</bean>
<!-- Register custom step in checkout group -->
<bean id="customCheckoutGroupConfiguration"
class="org.springframework.beans.factory.config.MethodInvokingBean">
<property name="targetObject" ref="defaultCheckoutGroup"/>
<property name="targetMethod" value="setCheckoutStepMap"/>
<property name="arguments">
<map merge="true">
<entry key="custom-step" value-ref="customCheckoutStep"/>
</map>
</property>
</bean>
<!-- Progress Bar Configuration -->
<bean id="customStepProgressBar"
class="de.hybris.platform.acceleratorstorefrontcommons.checkout.steps.CheckoutProgressBar">
<property name="id" value="customStep"/>
<property name="label" value="checkout.multi.customStep.title"/>
</bean>
<!-- Step Validator -->
<bean id="customCheckoutStepValidator"
class="com.example.storefront.validators.CustomCheckoutStepValidator">
<property name="customCheckoutFacade" ref="customCheckoutFacade"/>
</bean>
<!-- Form Validator -->
<bean id="customStepFormValidator"
class="com.example.storefront.validators.CustomStepFormValidator"/>
</beans>
/*
* CustomCheckoutFacade.java
* Facade for custom checkout operations.
*/
package com.example.facades;
import java.util.List;
/**
* Facade interface for custom checkout step operations.
*/
public interface CustomCheckoutFacade {
/**
* Get available custom options for the checkout step.
* @return list of option codes
*/
List<String> getAvailableOptions();
/**
* Get the currently selected custom option for the cart.
* @return selected option code or null
*/
String getSelectedCustomOption();
/**
* Save the selected custom option to the cart.
* @param optionCode the selected option code
*/
void saveCustomOption(String optionCode);
/**
* Validate if the given option is valid for selection.
* @param optionCode the option to validate
* @return true if valid
*/
boolean isValidOption(String optionCode);
/**
* Check if custom step is required for current cart.
* @return true if step should be shown
*/
boolean isCustomStepRequired();
}
/*
* CustomCheckoutStep.java
* Custom checkout step controller for accelerator checkout flow.
*/
package com.example.storefront.controllers.pages.checkout.steps;
import com.example.facades.CustomCheckoutFacade;
import com.example.storefront.forms.CustomStepForm;
import de.hybris.platform.acceleratorstorefrontcommons.annotations.PreValidateCheckoutStep;
import de.hybris.platform.acceleratorstorefrontcommons.annotations.RequireHardLogIn;
import de.hybris.platform.acceleratorstorefrontcommons.checkout.steps.CheckoutStep;
import de.hybris.platform.acceleratorstorefrontcommons.constants.WebConstants;
import de.hybris.platform.acceleratorstorefrontcommons.controllers.pages.checkout.steps.AbstractCheckoutStepController;
import de.hybris.platform.cms2.exceptions.CMSItemNotFoundException;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import javax.annotation.Resource;
import javax.validation.Valid;
/**
* Controller for custom checkout step.
* Integrates with Spring Web Flow checkout process.
*/
@Controller
@RequestMapping("/checkout/multi/custom-step")
public class CustomCheckoutStepController extends AbstractCheckoutStepController {
private static final String CUSTOM_STEP = "custom-step";
private static final String CUSTOM_STEP_CMS_PAGE = "customCheckoutStepPage";
@Resource
private CustomCheckoutFacade customCheckoutFacade;
/**
* Enter the custom checkout step.
* Load any required data and display the step form.
*/
@Override
@RequestMapping(method = RequestMethod.GET)
@RequireHardLogIn
@PreValidateCheckoutStep(checkoutStep = CUSTOM_STEP)
public String enterStep(final Model model, final RedirectAttributes redirectAttributes)
throws CMSItemNotFoundException {
// Load existing data if available
final CustomStepForm form = new CustomStepForm();
form.setCustomOption(customCheckoutFacade.getSelectedCustomOption());
model.addAttribute("customStepForm", form);
model.addAttribute("customOptions", customCheckoutFacade.getAvailableOptions());
// CMS page setup
storeCmsPageInModel(model, getContentPageForLabelOrId(CUSTOM_STEP_CMS_PAGE));
setUpMetaDataForContentPage(model, getContentPageForLabelOrId(CUSTOM_STEP_CMS_PAGE));
model.addAttribute(WebConstants.BREADCRUMBS_KEY, getResourceBreadcrumbBuilder()
.getBreadcrumbs("checkout.multi.customStep.breadcrumb"));
return getViewForPage(model);
}
/**
* Process the custom step form submission.
* Validate and save data, then proceed to next step.
*/
@RequestMapping(method = RequestMethod.POST)
@RequireHardLogIn
public String submitStep(@Valid final CustomStepForm form, final BindingResult bindingResult,
final Model model, final RedirectAttributes redirectAttributes)
throws CMSItemNotFoundException {
// Validate form
if (bindingResult.hasErrors()) {
return enterStep(model, redirectAttributes);
}
// Custom validation
if (!customCheckoutFacade.isValidOption(form.getCustomOption())) {
bindingResult.rejectValue("customOption", "checkout.custom.invalid.option");
return enterStep(model, redirectAttributes);
}
// Save the custom data
try {
customCheckoutFacade.saveCustomOption(form.getCustomOption());
} catch (Exception e) {
model.addAttribute("errorMessage", "checkout.custom.save.error");
return enterStep(model, redirectAttributes);
}
// Proceed to next step
return getCheckoutStep().nextStep();
}
/**
* Go back to previous checkout step.
*/
@RequestMapping(value = "/back", method = RequestMethod.GET)
@RequireHardLogIn
public String back(final RedirectAttributes redirectAttributes) {
return getCheckoutStep().previousStep();
}
@Override
protected CheckoutStep getCheckoutStep() {
return getCheckoutStep(CUSTOM_STEP);
}
// Setter for testing
public void setCustomCheckoutFacade(CustomCheckoutFacade customCheckoutFacade) {
this.customCheckoutFacade = customCheckoutFacade;
}
}
<%-- customStepPage.jsp - Custom checkout step JSP page --%>
<%@ page trimDirectiveWhitespaces="true" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<%@ taglib prefix="template" tagdir="/WEB-INF/tags/responsive/template" %>
<%@ taglib prefix="cms" uri="http://hybris.com/tld/cmstags" %>
<%@ taglib prefix="multi-checkout" tagdir="/WEB-INF/tags/responsive/checkout/multi" %>
<spring:htmlEscape defaultHtmlEscape="true"/>
<template:page pageTitle="${pageTitle}">
<div class="row">
<div class="col-sm-6">
<div class="checkout-headline">
<spring:theme code="checkout.multi.customStep.title" text="Custom Options"/>
</div>
<%-- Error messages --%>
<c:if test="${not empty errorMessage}">
<div class="alert alert-danger">
<spring:theme code="${errorMessage}"/>
</div>
</c:if>
<%-- Custom step form --%>
<form:form id="customStepForm"
action="${request.contextPath}/checkout/multi/custom-step"
method="post"
modelAttribute="customStepForm">
<div class="form-group">
<label for="customOption">
<spring:theme code="checkout.multi.customStep.selectOption" text="Select Option"/>
</label>
<form:select path="customOption" id="customOption" class="form-control">
<form:option value="">
<spring:theme code="checkout.multi.customStep.selectOption.placeholder"/>
</form:option>
<c:forEach items="${customOptions}" var="option">
<form:option value="${option.code}">
${option.name}
</form:option>
</c:forEach>
</form:select>
<form:errors path="customOption" cssClass="help-block text-danger"/>
</div>
<div class="form-group">
<label for="customNotes">
<spring:theme code="checkout.multi.customStep.notes" text="Additional Notes"/>
</label>
<form:textarea path="customNotes" id="customNotes"
class="form-control" rows="3"
placeholder="Optional notes..."/>
</div>
<%-- Navigation buttons --%>
<div class="checkout-steps-actions">
<a href="${request.contextPath}/checkout/multi/custom-step/back"
class="btn btn-default">
<spring:theme code="checkout.multi.back" text="Back"/>
</a>
<button type="submit" class="btn btn-primary">
<spring:theme code="checkout.multi.next" text="Next"/>
</button>
</div>
</form:form>
</div>
<%-- Order summary sidebar --%>
<div class="col-sm-6">
<multi-checkout:checkoutOrderSummary cartData="${cartData}"
showDeliveryAddress="true"
showDeliveryMethod="true"/>
</div>
</div>
<%-- Progress indicator --%>
<multi-checkout:checkoutProgressBar steps="${checkoutSteps}"
currentStep="customStep"/>
</template:page>
// custom-banner.component.ts
// Example: override the default BannerComponent with a custom Angular component.
// Register it via provideConfig({ cmsComponents: { BannerComponent: { component: CustomBannerComponent } } })
import { Component, OnInit } from '@angular/core';
import { CmsComponent } from '@spartacus/core';
import { CmsComponentData } from '@spartacus/storefront';
import { Observable } from 'rxjs';
interface CustomBannerData extends CmsComponent {
headline?: string;
media?: { url: string; altText: string };
urlLink?: string;
}
@Component({
selector: 'app-custom-banner',
template: `
<ng-container *ngIf="data$ | async as data">
<a [routerLink]="data.urlLink" class="custom-banner">
<cx-media [container]="data.media"></cx-media>
<h2 class="custom-banner__headline">{{ data.headline }}</h2>
</a>
</ng-container>
`,
styleUrls: ['./custom-banner.component.scss'],
})
export class CustomBannerComponent implements OnInit {
data$: Observable<CustomBannerData>;
constructor(public component: CmsComponentData<CustomBannerData>) {}
ngOnInit(): void {
this.data$ = this.component.data$;
}
}
// custom-feature.module.ts
// Feature module template: registers a custom Angular CMS component override
// and provides its configuration via provideConfig.
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
import { CmsConfig, provideConfig } from '@spartacus/core';
import { MediaModule } from '@spartacus/storefront';
import { CustomBannerComponent } from './custom-banner.component';
@NgModule({
declarations: [CustomBannerComponent],
imports: [
CommonModule,
RouterModule,
MediaModule,
],
providers: [
provideConfig({
cmsComponents: {
// Map Commerce CMS type code → Angular component class
BannerComponent: {
component: CustomBannerComponent,
},
// Add more overrides here as needed
},
} as CmsConfig),
],
})
export class CustomFeatureModule {}
// spartacus-configuration.module.ts
// Root configuration module for Composable Storefront.
// Adjust baseUrl, baseSite, currency and language for your environment.
import { NgModule } from '@angular/core';
import { provideConfig } from '@spartacus/core';
import { layoutConfig, mediaConfig } from '@spartacus/storefront';
@NgModule({
providers: [
provideConfig(layoutConfig),
provideConfig(mediaConfig),
provideConfig({
backend: {
occ: {
baseUrl: 'https://your-commerce-backend.com',
// prefix: '/occ/v2/' // default
},
},
context: {
urlParameters: ['baseSite', 'language', 'currency'],
baseSite: ['electronics-spa'],
currency: ['USD'],
language: ['en'],
},
pwa: {
enabled: true,
addToHomeScreen: true,
},
}),
],
})
export class SpartacusConfigurationModule {}
<?xml version="1.0" encoding="ISO-8859-1"?>
<items xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="items.xsd">
<!--
Custom CronJob type with additional parameters.
Extend CronJob to add configuration attributes that are
accessible in the JobPerformable via the typed CronJob model.
-->
<enumtypes>
<enumtype code="ExportFormat" autocreate="true" generate="true" dynamic="false">
<value code="CSV"/>
<value code="XML"/>
<value code="JSON"/>
</enumtype>
</enumtypes>
<itemtypes>
<itemtype code="DataExportCronJob"
extends="CronJob"
autocreate="true"
generate="true"
jaloclass="com.example.jalo.DataExportCronJob">
<description>CronJob for scheduled data export with configurable parameters.</description>
<attributes>
<attribute qualifier="exportPath" type="java.lang.String">
<description>File system path for export output</description>
<modifiers optional="false"/>
<persistence type="property"/>
</attribute>
<attribute qualifier="maxRecords" type="java.lang.Integer">
<description>Maximum number of records to export per run</description>
<defaultvalue>Integer.valueOf(1000)</defaultvalue>
<persistence type="property"/>
</attribute>
<attribute qualifier="exportFormat" type="ExportFormat">
<description>Output format for the export</description>
<defaultvalue>em().getEnumerationValue("ExportFormat", "CSV")</defaultvalue>
<persistence type="property"/>
</attribute>
</attributes>
</itemtype>
</itemtypes>
</items>
# CronJob Setup ImpEx
# Registers jobs, creates CronJob instances, and configures triggers.
# -----------------------------------------------
# 1. Register ServicelayerJobs (link Spring beans)
# -----------------------------------------------
INSERT_UPDATE ServicelayerJob; code[unique=true] ; springId
; customCleanupJob ; customCleanupJobPerformable
; dataExportJob ; dataExportJobPerformable
# -----------------------------------------------
# 2. Create CronJob instances
# -----------------------------------------------
# Simple CronJob (uses base CronJobModel)
INSERT_UPDATE CronJob; code[unique=true] ; job(code) ; sessionLanguage(isocode); singleExecutable; logToDatabase; logToFile; requestAbortStep
; customCleanupCronJob ; customCleanupJob ; en ; true ; true ; false ; true
# Typed CronJob with custom parameters (uses DataExportCronJobModel)
INSERT_UPDATE DataExportCronJob; code[unique=true] ; job(code) ; exportPath ; maxRecords; exportFormat(code); sessionLanguage(isocode); singleExecutable; logToDatabase
; dailyDataExportCron ; dataExportJob ; /tmp/export/daily ; 5000 ; CSV ; en ; true ; true
# -----------------------------------------------
# 3. Configure Triggers (schedules)
# -----------------------------------------------
# Cleanup job: runs daily at 2:00 AM
INSERT_UPDATE Trigger; cronJob(code)[unique=true]; cronExpression ; active
; customCleanupCronJob ; 0 0 2 * * ? ; true
# Export job: runs daily at 4:00 AM
INSERT_UPDATE Trigger; cronJob(code)[unique=true]; cronExpression ; active
; dailyDataExportCron ; 0 0 4 * * ? ; true
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!--
CronJob Spring bean configuration.
The bean ID here must match the springId in the ServicelayerJob ImpEx.
-->
<!-- Simple CronJob using base CronJobModel -->
<bean id="customCleanupJobPerformable"
class="com.example.jobs.CustomJobPerformable"
parent="abstractJobPerformable">
<!-- Inject any required services -->
<!-- <property name="modelService" ref="modelService"/> -->
<!-- <property name="flexibleSearchService" ref="flexibleSearchService"/> -->
</bean>
<!-- Typed CronJob using custom DataExportCronJobModel -->
<bean id="dataExportJobPerformable"
class="com.example.jobs.DataExportJobPerformable"
parent="abstractJobPerformable">
<!-- <property name="exportService" ref="exportService"/> -->
</bean>
</beans>
package com.example.jobs;
import de.hybris.platform.cronjob.enums.CronJobResult;
import de.hybris.platform.cronjob.enums.CronJobStatus;
import de.hybris.platform.cronjob.model.CronJobModel;
import de.hybris.platform.servicelayer.cronjob.AbstractJobPerformable;
import de.hybris.platform.servicelayer.cronjob.PerformResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Custom job performable template.
*
* Replace CronJobModel with a custom CronJob type if parameters are needed.
* Register as a Spring bean and link via ServicelayerJob in ImpEx.
*/
public class CustomJobPerformable extends AbstractJobPerformable<CronJobModel> {
private static final Logger LOG = LoggerFactory.getLogger(CustomJobPerformable.class);
@Override
public PerformResult perform(final CronJobModel cronJobModel) {
LOG.info("Starting job: {}", cronJobModel.getCode());
try {
int processed = 0;
int errors = 0;
// TODO: Replace with actual business logic
// Example: iterate over items and process them
// final List<ItemModel> items = fetchItemsToProcess();
// for (final ItemModel item : items) {
// if (clearAbortRequestedIfNeeded(cronJobModel)) {
// LOG.info("Job aborted by user request after processing {} items", processed);
// return new PerformResult(CronJobResult.UNKNOWN, CronJobStatus.ABORTED);
// }
// try {
// processItem(item);
// processed++;
// } catch (final Exception e) {
// LOG.error("Error processing item {}", item.getPk(), e);
// errors++;
// }
// }
LOG.info("Job completed. Processed: {}, Errors: {}", processed, errors);
if (errors > 0) {
return new PerformResult(CronJobResult.WARNING, CronJobStatus.FINISHED);
}
return new PerformResult(CronJobResult.SUCCESS, CronJobStatus.FINISHED);
} catch (final Exception e) {
LOG.error("Job failed with unexpected error", e);
return new PerformResult(CronJobResult.ERROR, CronJobStatus.ABORTED);
}
}
@Override
public boolean isAbortable() {
return true;
}
}
<?xml version="1.0" encoding="UTF-8"?>
<!--
extensioninfo.xml
Extension metadata and configuration.
Defines dependencies, modules, and extension properties.
-->
<extensioninfo xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="extensioninfo.xsd">
<extension name="customextension"
classprefix="Custom"
abstractextension="false"
version="1.0.0">
<!-- Platform dependencies - load these extensions first -->
<requires-extension name="commerceservices"/>
<requires-extension name="acceleratorservices"/>
<requires-extension name="commercefacades"/>
<!-- Core module - generates jalo/model classes -->
<coremodule generated="true"
manager="de.hybris.platform.jalo.extension.ExtensionManager"
packageroot="com.example.custom"/>
<!-- Optional: Web module for custom web application -->
<!--
<webmodule webroot="/customextension"
jspcompile="true"/>
-->
<!-- Optional: HMC module for Backoffice integration (legacy) -->
<!--
<hmcmodule additionalclasspath=""
extensionclassname="com.example.custom.hmc.CustomextensionHMCExtension"/>
-->
</extension>
</extensioninfo>
<?xml version="1.0" encoding="ISO-8859-1"?>
<!--
customextension-items.xml
Type system definitions for this extension.
Define item types, relations, enums here.
-->
<items xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="items.xsd">
<!-- Enumeration types -->
<enumtypes>
<enumtype code="CustomStatus" autocreate="true" generate="true" dynamic="false">
<description>Custom status enumeration</description>
<value code="ACTIVE"/>
<value code="INACTIVE"/>
<value code="PENDING"/>
</enumtype>
</enumtypes>
<!-- Item types -->
<itemtypes>
<!-- Custom item type example -->
<itemtype code="CustomItem"
autocreate="true"
generate="true"
jaloclass="com.example.custom.jalo.CustomItem">
<deployment table="CustomItems" typecode="10500"/>
<attributes>
<attribute qualifier="code" type="java.lang.String">
<description>Unique code</description>
<modifiers read="true" write="true" optional="false" unique="true"/>
<persistence type="property"/>
</attribute>
<attribute qualifier="name" type="localized:java.lang.String">
<description>Localized name</description>
<modifiers read="true" write="true" optional="true"/>
<persistence type="property"/>
</attribute>
<attribute qualifier="status" type="CustomStatus">
<description>Current status</description>
<modifiers read="true" write="true" optional="true"/>
<persistence type="property"/>
<defaultvalue>em().getEnumerationValue("CustomStatus","ACTIVE")</defaultvalue>
</attribute>
<attribute qualifier="active" type="java.lang.Boolean">
<description>Is active flag</description>
<modifiers read="true" write="true" optional="true"/>
<persistence type="property"/>
<defaultvalue>Boolean.TRUE</defaultvalue>
</attribute>
</attributes>
</itemtype>
<!-- Extend existing Product type -->
<itemtype code="Product" autocreate="false" generate="false">
<attributes>
<attribute qualifier="customField" type="java.lang.String">
<description>Custom field added to Product</description>
<modifiers read="true" write="true" optional="true"/>
<persistence type="property"/>
</attribute>
</attributes>
</itemtype>
</itemtypes>
</items>
<?xml version="1.0" encoding="UTF-8"?>
<!--
customextension-spring.xml
Spring bean configuration for this extension.
Define DAOs, services, facades, converters here.
-->
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!-- Component scan for annotations -->
<context:component-scan base-package="com.example.custom"/>
<!-- DAO Beans -->
<alias name="defaultCustomItemDAO" alias="customItemDAO"/>
<bean id="defaultCustomItemDAO" class="com.example.custom.daos.impl.DefaultCustomItemDAO">
<property name="flexibleSearchService" ref="flexibleSearchService"/>
</bean>
<!-- Service Beans -->
<alias name="defaultCustomItemService" alias="customItemService"/>
<bean id="defaultCustomItemService" class="com.example.custom.services.impl.DefaultCustomItemService">
<property name="customItemDAO" ref="customItemDAO"/>
<property name="modelService" ref="modelService"/>
</bean>
<!-- Populator Beans -->
<bean id="customItemPopulator" class="com.example.custom.populators.CustomItemPopulator"/>
<!-- Converter Beans -->
<alias name="defaultCustomItemConverter" alias="customItemConverter"/>
<bean id="defaultCustomItemConverter" parent="abstractPopulatingConverter">
<property name="targetClass" value="com.example.custom.data.CustomItemData"/>
<property name="populators">
<list>
<ref bean="customItemPopulator"/>
</list>
</property>
</bean>
<!-- Facade Beans -->
<alias name="defaultCustomItemFacade" alias="customItemFacade"/>
<bean id="defaultCustomItemFacade" class="com.example.custom.facades.impl.DefaultCustomItemFacade">
<property name="customItemService" ref="customItemService"/>
<property name="customItemConverter" ref="customItemConverter"/>
</bean>
<!-- System Setup -->
<bean id="customextensionSystemSetup" class="com.example.custom.setup.CustomextensionSystemSetup">
<property name="modelService" ref="modelService"/>
<property name="flexibleSearchService" ref="flexibleSearchService"/>
</bean>
</beans>
# customextension-locales_en.properties
# English localization for custom extension types and attributes
# CustomItem type
type.CustomItem.name=Custom Item
type.CustomItem.description=Custom item type for extension
# CustomItem attributes
type.CustomItem.code.name=Code
type.CustomItem.code.description=Unique identifier code
type.CustomItem.name.name=Name
type.CustomItem.name.description=Display name (localized)
type.CustomItem.status.name=Status
type.CustomItem.status.description=Current status
type.CustomItem.active.name=Active
type.CustomItem.active.description=Whether item is active
# CustomStatus enum
type.CustomStatus.name=Custom Status
type.CustomStatus.ACTIVE.name=Active
type.CustomStatus.INACTIVE.name=Inactive
type.CustomStatus.PENDING.name=Pending
# Product extension
type.Product.customField.name=Custom Field
type.Product.customField.description=Custom field added by extension
/*
* CustomextensionConstants.java
* Constants class for the extension.
* Contains extension name and other constant values.
*/
package com.example.custom.constants;
/**
* Global constants for the customextension.
*/
public final class CustomextensionConstants {
/** Extension name constant - matches extensioninfo.xml name */
public static final String EXTENSIONNAME = "customextension";
/** Default status code */
public static final String DEFAULT_STATUS = "ACTIVE";
/** Configuration keys */
public static final String CONFIG_ENABLED = "customextension.enabled";
public static final String CONFIG_MAX_ITEMS = "customextension.maxItems";
private CustomextensionConstants() {
// Private constructor to prevent instantiation
}
}
/*
* CustomextensionSystemSetup.java
* System setup class for creating essential and project data.
* Runs during ant initialize and ant updatesystem.
*/
package com.example.custom.setup;
import com.example.custom.constants.CustomextensionConstants;
import de.hybris.platform.core.initialization.SystemSetup;
import de.hybris.platform.core.initialization.SystemSetup.Process;
import de.hybris.platform.core.initialization.SystemSetup.Type;
import de.hybris.platform.core.initialization.SystemSetupContext;
import de.hybris.platform.core.initialization.SystemSetupParameter;
import de.hybris.platform.core.initialization.SystemSetupParameterMethod;
import de.hybris.platform.servicelayer.model.ModelService;
import de.hybris.platform.servicelayer.search.FlexibleSearchService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
/**
* System setup for customextension.
* Creates essential data during initialization and optional sample data.
*/
@SystemSetup(extension = CustomextensionConstants.EXTENSIONNAME)
public class CustomextensionSystemSetup {
private static final Logger LOG = LoggerFactory.getLogger(CustomextensionSystemSetup.class);
private ModelService modelService;
private FlexibleSearchService flexibleSearchService;
/**
* Define setup parameters shown in HAC during update.
*/
@SystemSetupParameterMethod
public List<SystemSetupParameter> getSystemSetupParameters() {
final List<SystemSetupParameter> params = new ArrayList<>();
params.add(createBooleanSystemSetupParameter(
"createSampleData",
"Create Sample Data",
true
));
return params;
}
/**
* Create essential data required for extension to function.
* Runs during both initialize and update.
*/
@SystemSetup(type = Type.ESSENTIAL, process = Process.ALL)
public void createEssentialData() {
LOG.info("Creating essential data for customextension...");
// Create essential configuration, permissions, etc.
// Example: Create required user groups, access rights
LOG.info("Essential data creation complete.");
}
/**
* Create project/sample data.
* Runs during initialize and update if parameter is selected.
*/
@SystemSetup(type = Type.PROJECT, process = Process.ALL)
public void createProjectData(final SystemSetupContext context) {
LOG.info("Creating project data for customextension...");
// Check if sample data should be created
if (context.getParameterMap() != null &&
getBooleanParameter(context, "createSampleData")) {
LOG.info("Creating sample data...");
createSampleCustomItems();
}
LOG.info("Project data creation complete.");
}
/**
* Create sample CustomItem instances.
*/
private void createSampleCustomItems() {
// Implementation would create sample data
// Using modelService.create() and modelService.save()
LOG.info("Sample CustomItems created.");
}
private boolean getBooleanParameter(final SystemSetupContext context, final String key) {
return context.getParameterMap().containsKey(key) &&
Boolean.TRUE.toString().equals(context.getParameterMap().get(key));
}
private SystemSetupParameter createBooleanSystemSetupParameter(
final String key, final String label, final boolean defaultValue) {
final SystemSetupParameter param = new SystemSetupParameter(key);
param.setLabel(label);
param.addValue("true", defaultValue);
param.addValue("false", !defaultValue);
return param;
}
// Setter injection
public void setModelService(final ModelService modelService) {
this.modelService = modelService;
}
public void setFlexibleSearchService(final FlexibleSearchService flexibleSearchService) {
this.flexibleSearchService = flexibleSearchService;
}
}
<?xml version="1.0" encoding="UTF-8"?>
<!--
web.xml
Web application deployment descriptor.
Configure servlets, filters, and listeners here.
-->
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
<display-name>Custom Extension Web Application</display-name>
<description>Web module for custom extension</description>
<!-- Spring Context Loader -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- Spring Context Configuration -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/customextension-web-spring.xml</param-value>
</context-param>
<!-- Spring Dispatcher Servlet -->
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value></param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<!-- Character Encoding Filter -->
<filter>
<filter-name>encodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<param-name>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- Session timeout in minutes -->
<session-config>
<session-timeout>30</session-timeout>
</session-config>
<!-- Welcome file -->
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
</web-app>
-- complex-joins.fxs
-- Advanced FlexibleSearch queries with complex JOINs and subqueries
-- Products with stock levels across warehouses
SELECT {p.pk}, {p.code}, {w.code}, {sl.available}
FROM {Product AS p
JOIN StockLevel AS sl ON {p.pk} = {sl.product}
JOIN Warehouse AS w ON {sl.warehouse} = {w.pk}}
WHERE {sl.available} > 0
ORDER BY {p.code}, {w.code}
-- Products with all their categories (multi-level)
SELECT {p.pk}, {p.code}, {c1.code} AS directCategory, {c2.code} AS parentCategory
FROM {Product AS p
JOIN CategoryProductRelation AS cpr ON {p.pk} = {cpr.target}
JOIN Category AS c1 ON {cpr.source} = {c1.pk}
LEFT JOIN Category AS c2 ON {c1.supercategories} = {c2.pk}}
WHERE {p.catalogVersion} = ?catalogVersion
-- Orders with customer and address details
SELECT {o.pk}, {o.code}, {c.name}, {a.streetname}, {a.town}
FROM {Order AS o
JOIN Customer AS c ON {o.user} = {c.pk}
JOIN Address AS a ON {o.deliveryAddress} = {a.pk}}
WHERE {o.creationtime} >= ?startDate
ORDER BY {o.creationtime} DESC
-- Full product catalog with prices and stock
SELECT {p.pk}, {p.code}, {p.name}, {pr.price}, {pr.currency}, {sl.available}
FROM {Product AS p
LEFT JOIN PriceRow AS pr ON {p.pk} = {pr.product}
LEFT JOIN StockLevel AS sl ON {p.pk} = {sl.product}}
WHERE {p.catalogVersion} = ?catalogVersion
AND ({pr.currency} = ?currency OR {pr.currency} IS NULL)
ORDER BY {p.code}
-- B2B organization structure with users
SELECT {u.pk}, {u.uid}, {u.name}, {unit.uid} AS unitName, {parent.uid} AS parentUnit
FROM {B2BUnit AS unit
JOIN B2BCustomer AS u ON {u.defaultB2BUnit} = {unit.pk}
LEFT JOIN B2BUnit AS parent ON {unit.parentUnit} = {parent.pk}}
ORDER BY {parent.uid}, {unit.uid}, {u.name}
-- Products in multiple categories (intersection)
SELECT {p.pk} FROM {Product AS p}
WHERE EXISTS (
SELECT 1 FROM {CategoryProductRelation AS cpr
JOIN Category AS c ON {cpr.source} = {c.pk}}
WHERE {cpr.target} = {p.pk} AND {c.code} = ?category1
)
AND EXISTS (
SELECT 1 FROM {CategoryProductRelation AS cpr
JOIN Category AS c ON {cpr.source} = {c.pk}}
WHERE {cpr.target} = {p.pk} AND {c.code} = ?category2
)
-- Order history with items summary
SELECT {o.pk}, {o.code}, {o.creationtime}, {o.totalPrice},
COUNT({oe.pk}) AS itemCount, SUM({oe.quantity}) AS totalQuantity
FROM {Order AS o
JOIN OrderEntry AS oe ON {oe.order} = {o.pk}}
WHERE {o.user} = ?user
GROUP BY {o.pk}, {o.code}, {o.creationtime}, {o.totalPrice}
ORDER BY {o.creationtime} DESC
-- CMS pages with slots and components
SELECT {p.pk}, {p.uid}, {cs.uid} AS slotUid, {comp.uid} AS componentUid
FROM {ContentPage AS p
JOIN ContentSlotForPage AS csfp ON {csfp.page} = {p.pk}
JOIN ContentSlot AS cs ON {csfp.contentSlot} = {cs.pk}
JOIN ElementsForSlot AS efs ON {efs.source} = {cs.pk}
JOIN AbstractCMSComponent AS comp ON {efs.target} = {comp.pk}}
WHERE {p.catalogVersion} = ?catalogVersion
ORDER BY {p.uid}, {cs.position}
-- Products never ordered
SELECT {p.pk}, {p.code} FROM {Product AS p}
WHERE {p.catalogVersion} = ?catalogVersion
AND NOT EXISTS (
SELECT 1 FROM {OrderEntry AS oe} WHERE {oe.product} = {p.pk}
)
-- Top selling products
SELECT {oe.product}, SUM({oe.quantity}) AS totalSold
FROM {OrderEntry AS oe
JOIN Order AS o ON {oe.order} = {o.pk}}
WHERE {o.creationtime} >= ?startDate
GROUP BY {oe.product}
ORDER BY totalSold DESC
-- Products with promotions
SELECT DISTINCT {p.pk}, {p.code}, {promo.code} AS promotionCode
FROM {Product AS p
JOIN ProductPromotion AS promo ON {p.pk} IN ({promo.products})}
WHERE {promo.enabled} = true
AND {promo.startDate} <= ?currentDate
AND ({promo.endDate} IS NULL OR {promo.endDate} >= ?currentDate)
-- order-queries.fxs
-- Common FlexibleSearch queries for Order and Cart operations
-- Find order by code
SELECT {pk} FROM {Order} WHERE {code} = ?orderCode
-- Orders for specific user
SELECT {pk} FROM {Order}
WHERE {user} = ?user
ORDER BY {creationtime} DESC
-- Orders by status
SELECT {pk} FROM {Order}
WHERE {status} = ?status
ORDER BY {creationtime} DESC
-- Orders in date range
SELECT {pk} FROM {Order}
WHERE {creationtime} >= ?startDate
AND {creationtime} <= ?endDate
ORDER BY {creationtime} DESC
-- Recent orders (last N days)
SELECT {pk} FROM {Order}
WHERE {creationtime} >= ?sinceDate
ORDER BY {creationtime} DESC
-- Order entries for order
SELECT {pk} FROM {OrderEntry}
WHERE {order} = ?order
ORDER BY {entryNumber} ASC
-- Orders containing specific product
SELECT DISTINCT {o.pk} FROM {Order AS o
JOIN OrderEntry AS oe ON {oe.order} = {o.pk}}
WHERE {oe.product} = ?product
-- Orders by total value
SELECT {pk} FROM {Order}
WHERE {totalPrice} >= ?minTotal
AND {totalPrice} <= ?maxTotal
ORDER BY {totalPrice} DESC
-- Cart for user
SELECT {pk} FROM {Cart}
WHERE {user} = ?user
AND {saveTime} IS NULL
ORDER BY {modifiedtime} DESC
-- Abandoned carts (not modified in X days)
SELECT {pk} FROM {Cart}
WHERE {modifiedtime} < ?abandonedBefore
AND {saveTime} IS NULL
-- Cart entries
SELECT {ce.pk}, {ce.quantity}, {p.code}
FROM {CartEntry AS ce
JOIN Cart AS c ON {ce.order} = {c.pk}
JOIN Product AS p ON {ce.product} = {p.pk}}
WHERE {c.code} = ?cartCode
-- Order count by status
SELECT {status}, COUNT({pk})
FROM {Order}
GROUP BY {status}
-- Daily order totals
SELECT DATE({creationtime}), SUM({totalPrice})
FROM {Order}
WHERE {creationtime} >= ?startDate
GROUP BY DATE({creationtime})
ORDER BY DATE({creationtime})
-- Orders pending fulfillment
SELECT {o.pk} FROM {Order AS o}
WHERE {o.status} = 'CREATED'
AND NOT EXISTS (
SELECT 1 FROM {Consignment AS c}
WHERE {c.order} = {o.pk}
)
-- product-queries.fxs
-- Common FlexibleSearch queries for Product operations
-- Find product by code
SELECT {pk} FROM {Product} WHERE {code} = ?code
-- Find product by code in specific catalog version
SELECT {p.pk} FROM {Product AS p
JOIN CatalogVersion AS cv ON {p.catalogVersion} = {cv.pk}
JOIN Catalog AS c ON {cv.catalog} = {c.pk}}
WHERE {p.code} = ?code
AND {c.id} = ?catalogId
AND {cv.version} = ?versionName
-- Search products by name (case-insensitive)
SELECT {pk} FROM {Product}
WHERE LOWER({name}) LIKE LOWER(?searchText)
ORDER BY {name} ASC
-- Products in category (via relation)
SELECT {p.pk} FROM {Product AS p
JOIN CategoryProductRelation AS cpr ON {p.pk} = {cpr.target}
JOIN Category AS c ON {cpr.source} = {c.pk}}
WHERE {c.code} = ?categoryCode
-- Products with price in range
SELECT DISTINCT {p.pk} FROM {Product AS p
JOIN PriceRow AS pr ON {p.pk} = {pr.product}}
WHERE {pr.price} >= ?minPrice
AND {pr.price} <= ?maxPrice
AND {pr.currency} = ?currency
-- Products by approval status
SELECT {pk} FROM {Product}
WHERE {approvalStatus} = ?status
AND {catalogVersion} = ?catalogVersion
-- Recently modified products
SELECT {pk} FROM {Product}
WHERE {modifiedtime} >= ?since
ORDER BY {modifiedtime} DESC
-- Products without images
SELECT {p.pk} FROM {Product AS p}
WHERE NOT EXISTS (
SELECT 1 FROM {MediaContainer AS mc}
WHERE {mc.pk} IN ({p.galleryImages})
)
-- Featured/promoted products
SELECT {pk} FROM {Product}
WHERE {featured} = true
AND {approvalStatus} = 'approved'
ORDER BY {priority} DESC
-- Products count by category
SELECT {c.code}, COUNT({p.pk})
FROM {Product AS p
JOIN CategoryProductRelation AS cpr ON {p.pk} = {cpr.target}
JOIN Category AS c ON {cpr.source} = {c.pk}}
GROUP BY {c.code}
-- user-queries.fxs
-- Common FlexibleSearch queries for User and Customer operations
-- Find user by UID
SELECT {pk} FROM {User} WHERE {uid} = ?uid
-- Find customer by email
SELECT {pk} FROM {Customer}
WHERE {uid} = ?email OR {contactEmail} = ?email
-- Customers in specific group
SELECT {c.pk} FROM {Customer AS c
JOIN PrincipalGroupRelation AS pgr ON {c.pk} = {pgr.source}
JOIN CustomerGroup AS cg ON {pgr.target} = {cg.pk}}
WHERE {cg.uid} = ?groupId
-- Active customers (logged in recently)
SELECT {pk} FROM {Customer}
WHERE {lastLogin} >= ?sinceDate
ORDER BY {lastLogin} DESC
-- Customers by registration date
SELECT {pk} FROM {Customer}
WHERE {creationtime} >= ?startDate
AND {creationtime} <= ?endDate
ORDER BY {creationtime} DESC
-- Search customers by name
SELECT {pk} FROM {Customer}
WHERE LOWER({name}) LIKE LOWER(?searchText)
OR LOWER({uid}) LIKE LOWER(?searchText)
-- B2B customers by unit
SELECT {pk} FROM {B2BCustomer}
WHERE {defaultB2BUnit} = ?unit
-- B2B unit hierarchy
SELECT {pk} FROM {B2BUnit}
WHERE {parentUnit} = ?parentUnit
-- B2B customers with approval rights
SELECT {c.pk} FROM {B2BCustomer AS c
JOIN PrincipalGroupRelation AS pgr ON {c.pk} = {pgr.source}
JOIN B2BUserGroup AS ug ON {pgr.target} = {ug.pk}}
WHERE {ug.uid} = 'b2bapprovergroup'
-- Customers with saved carts
SELECT DISTINCT {c.user} FROM {Cart AS c}
WHERE {c.saveTime} IS NOT NULL
-- Customer addresses
SELECT {pk} FROM {Address}
WHERE {owner} = ?customer
AND {visibleInAddressBook} = true
-- Default shipping address
SELECT {pk} FROM {Address}
WHERE {owner} = ?customer
AND {shippingAddress} = true
-- Count customers by group
SELECT {cg.uid}, COUNT({c.pk})
FROM {Customer AS c
JOIN PrincipalGroupRelation AS pgr ON {c.pk} = {pgr.source}
JOIN CustomerGroup AS cg ON {pgr.target} = {cg.pk}}
GROUP BY {cg.uid}
-- Inactive customers (no orders)
SELECT {c.pk} FROM {Customer AS c}
WHERE NOT EXISTS (
SELECT 1 FROM {Order AS o} WHERE {o.user} = {c.pk}
)
-- VIP customers (high order value)
SELECT {o.user}, SUM({o.totalPrice}) AS total
FROM {Order AS o}
GROUP BY {o.user}
HAVING SUM({o.totalPrice}) > ?threshold
ORDER BY total DESC
# b2b-organization.impex
# Setup B2B organization structure with units, users, budgets, and approvals
$defaultPassword=Test@123
$lang=en
# B2B Units (Organization Hierarchy)
INSERT_UPDATE B2BUnit;uid[unique=true];name;locName[lang=$lang];description;active[default=true]
;Acme Corp;Acme Corporation;Acme Corporation;Main corporate entity
;Acme Sales;Acme Sales Division;Acme Sales Division;Sales department
;Acme Engineering;Acme Engineering;Acme Engineering;Engineering department
;Acme Finance;Acme Finance;Acme Finance;Finance department
# Unit Hierarchy
UPDATE B2BUnit;uid[unique=true];parentUnit(uid)
;Acme Sales;Acme Corp
;Acme Engineering;Acme Corp
;Acme Finance;Acme Corp
# B2B User Groups
INSERT_UPDATE B2BUserGroup;uid[unique=true];name;unit(uid)
;acmePurchasers;Acme Purchasers;Acme Corp
;acmeApprovers;Acme Approvers;Acme Corp
;acmeAdmins;Acme Administrators;Acme Corp
# B2B Customers
INSERT_UPDATE B2BCustomer;uid[unique=true];password[default=$defaultPassword];name;email;defaultB2BUnit(uid);groups(uid);active[default=true]
;purchaser@acme.com;;John Purchaser;purchaser@acme.com;Acme Sales;acmePurchasers,b2bcustomergroup
;approver@acme.com;;Jane Approver;approver@acme.com;Acme Corp;acmeApprovers,b2bapprovergroup
;admin@acme.com;;Bob Admin;admin@acme.com;Acme Corp;acmeAdmins,b2badmingroup
;engineer@acme.com;;Alice Engineer;engineer@acme.com;Acme Engineering;acmePurchasers,b2bcustomergroup
# Cost Centers
INSERT_UPDATE B2BCostCenter;code[unique=true];name;unit(uid);currency(isocode)[default='USD'];active[default=true]
;ACME_CC_001;Sales Cost Center;Acme Sales
;ACME_CC_002;Engineering Cost Center;Acme Engineering
;ACME_CC_003;Corporate Cost Center;Acme Corp
# Budgets
INSERT_UPDATE B2BBudget;code[unique=true];name;unit(uid);budget;currency(isocode)[default='USD'];dateRange(code);active[default=true]
;ACME_BUDGET_001;Sales Budget 2024;Acme Sales;50000.00;;
;ACME_BUDGET_002;Engineering Budget 2024;Acme Engineering;100000.00;;
;ACME_BUDGET_003;Corporate Budget 2024;Acme Corp;500000.00;;
# Assign Budgets to Cost Centers
UPDATE B2BCostCenter;code[unique=true];budgets(code)
;ACME_CC_001;ACME_BUDGET_001
;ACME_CC_002;ACME_BUDGET_002
;ACME_CC_003;ACME_BUDGET_003
# Order Approval Permissions
INSERT_UPDATE B2BOrderThresholdPermission;code[unique=true];unit(uid);threshold;currency(isocode)[default='USD']
;ACME_5K_ORDER;Acme Corp;5000.00
;ACME_10K_ORDER;Acme Corp;10000.00
;ACME_50K_ORDER;Acme Corp;50000.00
# Assign Permissions to Groups
UPDATE B2BUserGroup;uid[unique=true];permissions(code)
;acmePurchasers;ACME_5K_ORDER
;acmeApprovers;ACME_10K_ORDER,ACME_50K_ORDER
# Customer Addresses
INSERT_UPDATE Address;owner(B2BCustomer.uid)[unique=true];streetname;streetnumber;postalcode;town;country(isocode);company;department;shippingAddress[default=true];billingAddress[default=true]
;purchaser@acme.com;Industrial Blvd;1000;12345;Business City;US;Acme Corp;Sales
;approver@acme.com;Industrial Blvd;1000;12345;Business City;US;Acme Corp;Management
;admin@acme.com;Industrial Blvd;1000;12345;Business City;US;Acme Corp;Administration
# category-hierarchy.impex
# Create category structure with parent-child relationships
$productCatalog=electronicsProductCatalog
$productCV=catalogVersion(catalog(id[default=$productCatalog]),version[default='Online'])[unique=true]
$supercategories=supercategories(code,$productCV)
$lang=en
# Root Categories
INSERT_UPDATE Category;code[unique=true];$productCV;name[lang=$lang];description[lang=$lang];allowedPrincipals(uid)[default='customergroup']
;root;;Root Category;Top level category
;electronics;;Electronics;Electronic devices and accessories
;clothing;;Clothing;Fashion and apparel
;home;;Home & Garden;Home improvement and garden
# Electronics Subcategories
INSERT_UPDATE Category;code[unique=true];$productCV;name[lang=$lang];$supercategories;allowedPrincipals(uid)[default='customergroup']
;computers;;Computers;electronics
;phones;;Phones & Tablets;electronics
;accessories;;Accessories;electronics
;audio;;Audio & Video;electronics
# Computer Subcategories
INSERT_UPDATE Category;code[unique=true];$productCV;name[lang=$lang];$supercategories
;laptops;;Laptops;computers
;desktops;;Desktop Computers;computers
;monitors;;Monitors;computers
;components;;Computer Components;computers
# Phone Subcategories
INSERT_UPDATE Category;code[unique=true];$productCV;name[lang=$lang];$supercategories
;smartphones;;Smartphones;phones
;tablets;;Tablets;phones
;wearables;;Wearables;phones
# Clothing Subcategories
INSERT_UPDATE Category;code[unique=true];$productCV;name[lang=$lang];$supercategories
;mens;;Men's Clothing;clothing
;womens;;Women's Clothing;clothing
;kids;;Kids' Clothing;clothing
;sportswear;;Sportswear;clothing
# Home Subcategories
INSERT_UPDATE Category;code[unique=true];$productCV;name[lang=$lang];$supercategories
;furniture;;Furniture;home
;garden;;Garden;home
;kitchen;;Kitchen;home
;decor;;Home Decor;home
# Navigation structure (optional)
$contentCV=catalogVersion(CatalogVersion.catalog(Catalog.id[default='electronicsContentCatalog']),CatalogVersion.version[default='Online'])
INSERT_UPDATE CMSNavigationNode;uid[unique=true];$contentCV;name;parent(uid,$contentCV);links(&linkRef)
;ElectronicsNavNode;;Electronics;;
;ComputersNavNode;;Computers;ElectronicsNavNode;
;PhonesNavNode;;Phones;ElectronicsNavNode;
# cms-content.impex
# Create CMS pages, slots, and components
$contentCatalog=electronicsContentCatalog
$contentCV=catalogVersion(CatalogVersion.catalog(Catalog.id[default=$contentCatalog]),CatalogVersion.version[default='Online'])[unique=true]
$lang=en
# Page Templates
INSERT_UPDATE PageTemplate;uid[unique=true];name;$contentCV;frontendTemplateName;active[default=true]
;LandingPageTemplate;Landing Page Template;;layout/landingLayout1Page
;ContentPageTemplate;Content Page Template;;layout/contentLayout1Page
;ProductListPageTemplate;Product List Page Template;;layout/productListPage
# Content Slots
INSERT_UPDATE ContentSlot;uid[unique=true];name;$contentCV;active[default=true]
;HeaderSlot;Header Slot;
;FooterSlot;Footer Slot;
;SidebarSlot;Sidebar Slot;
;MainContentSlot;Main Content Slot;
;BannerSlot;Banner Slot;
# Content Slot for Templates
INSERT_UPDATE ContentSlotForTemplate;uid[unique=true];$contentCV;pageTemplate(uid,$contentCV);contentSlot(uid,$contentCV);position[unique=true];allowOverwrite
;HeaderSlot-LandingPage;;LandingPageTemplate;HeaderSlot;Header;true
;FooterSlot-LandingPage;;LandingPageTemplate;FooterSlot;Footer;true
;MainContentSlot-LandingPage;;LandingPageTemplate;MainContentSlot;Section1;true
# CMS Pages
INSERT_UPDATE ContentPage;uid[unique=true];name;$contentCV;masterTemplate(uid,$contentCV);defaultPage[default=true];approvalStatus(code)[default='approved'];homepage[default=false]
;homepage;Homepage;;LandingPageTemplate;true;;true
;faq;FAQ Page;;ContentPageTemplate
;aboutus;About Us;;ContentPageTemplate
;contact;Contact Us;;ContentPageTemplate
# CMS Paragraph Components
INSERT_UPDATE CMSParagraphComponent;uid[unique=true];name;$contentCV;content[lang=$lang]
;WelcomeParagraph;Welcome Text;;"<h1>Welcome to Our Store</h1><p>Discover amazing products at great prices.</p>"
;FAQContent;FAQ Content;;"<h2>Frequently Asked Questions</h2><p>Find answers to common questions.</p>"
;AboutUsContent;About Us Content;;"<h2>About Us</h2><p>We are a leading e-commerce company.</p>"
# Banner Components
INSERT_UPDATE SimpleBannerComponent;uid[unique=true];name;$contentCV;urlLink
;HomeBanner;Homepage Banner;;/electronics
;SaleBanner;Sale Banner;;/sale
# Add components to slots
INSERT_UPDATE ContentSlotForPage;uid[unique=true];$contentCV;page(uid,$contentCV);contentSlot(uid,$contentCV);position
;MainContentSlot-Homepage;;homepage;MainContentSlot;Section1
INSERT_UPDATE ElementsForSlot;source(uid,$contentCV)[unique=true];target(uid,$contentCV)[unique=true];sequenceNumber
;MainContentSlot;WelcomeParagraph;1
;MainContentSlot;HomeBanner;2
# product-import.impex
# Import products with categories, prices, and media
# Macros
$productCatalog=electronicsProductCatalog
$productCV=catalogVersion(catalog(id[default=$productCatalog]),version[default='Online'])[unique=true]
$supercategories=supercategories(code,catalogVersion(catalog(id[default=$productCatalog]),version[default='Online']))
$prices=@priceRow[translator=de.hybris.platform.impex.jalo.translators.ItemPKTranslator]
$approved=approvalStatus(code)[default='approved']
$lang=en
# Create Categories
INSERT_UPDATE Category;code[unique=true];$productCV;name[lang=$lang];allowedPrincipals(uid)[default='customergroup']
;electronics;;Electronics
;computers;;Computers
;phones;;Phones
# Create Products
INSERT_UPDATE Product;code[unique=true];$productCV;name[lang=$lang];description[lang=$lang];$supercategories;$approved;unit(code)[default='pieces']
;LAPTOP001;;Gaming Laptop;High-performance gaming laptop with RTX graphics;computers
;LAPTOP002;;Business Laptop;Professional laptop for work;computers
;PHONE001;;Smartphone Pro;Latest smartphone with advanced camera;phones
;PHONE002;;Budget Phone;Affordable smartphone;phones
# Create Price Rows
$currency=currency(isocode)[default='USD']
INSERT_UPDATE PriceRow;product(code,$productCV)[unique=true];$currency;price;unit(code)[default='pieces'];net[default=false]
;LAPTOP001;;1299.99
;LAPTOP002;;899.99
;PHONE001;;999.99
;PHONE002;;299.99
# Create Stock Levels
$warehouse=warehouse(code)[default='default']
INSERT_UPDATE StockLevel;productCode[unique=true];$warehouse;available;inStockStatus(code)[default='forceInStock']
;LAPTOP001;;50
;LAPTOP002;;100
;PHONE001;;200
;PHONE002;;500
# Create Media
$mediaCV=catalogVersion(catalog(id[default=$productCatalog]),version[default='Online'])
$thumbnail=thumbnail(code,$mediaCV)
$picture=picture(code,$mediaCV)
INSERT_UPDATE Media;code[unique=true];$mediaCV;mime[default='image/jpeg'];folder(qualifier)[default='images']
;laptop001_image;;
;laptop002_image;;
;phone001_image;;
;phone002_image;;
# Assign media to products
UPDATE Product;code[unique=true];$productCV;$thumbnail;$picture
;LAPTOP001;;laptop001_image;laptop001_image
;LAPTOP002;;laptop002_image;laptop002_image
;PHONE001;;phone001_image;phone001_image
;PHONE002;;phone002_image;phone002_image
# user-setup.impex
# Setup users, user groups, and permissions
$defaultPassword=Test@123
# Create Customer Groups
INSERT_UPDATE CustomerGroup;uid[unique=true];locname[lang=en];description
;regularCustomers;Regular Customers;Standard customer group
;goldCustomers;Gold Customers;Premium customers with benefits
;platinumCustomers;Platinum Customers;VIP customers
# Setup group hierarchy
UPDATE CustomerGroup;uid[unique=true];groups(uid)
;goldCustomers;customergroup
;platinumCustomers;customergroup
;regularCustomers;customergroup
# Create Sample Customers
INSERT_UPDATE Customer;uid[unique=true];password[default=$defaultPassword];name;groups(uid);sessionLanguage(isocode)[default='en'];sessionCurrency(isocode)[default='USD']
;john.doe@example.com;;John Doe;regularCustomers
;jane.smith@example.com;;Jane Smith;goldCustomers
;bob.premium@example.com;;Bob Premium;platinumCustomers
# Create Customer Addresses
INSERT_UPDATE Address;owner(Customer.uid)[unique=true];streetname;streetnumber;postalcode;town;country(isocode);shippingAddress[default=true];billingAddress[default=true]
;john.doe@example.com;Main Street;123;10001;New York;US
;jane.smith@example.com;Oak Avenue;456;90210;Beverly Hills;US
;bob.premium@example.com;Park Lane;789;SW1A 1AA;London;GB
# Create Employee Users
INSERT_UPDATE Employee;uid[unique=true];password[default=$defaultPassword];name;groups(uid)
;admin@company.com;;Admin User;admingroup
;manager@company.com;;Store Manager;employeegroup
;support@company.com;;Support Staff;employeegroup
# Create User Rights (optional)
INSERT_UPDATE UserRight;code[unique=true];itemtype(code);attribute;positive
;read_products;Product;;true
;read_orders;Order;;true
;write_orders;Order;;true
# Assign rights to groups
INSERT_UPDATE PrincipalGroupRelation;source(uid)[unique=true];target(uid)[unique=true]
;goldCustomers;customergroup
;platinumCustomers;goldCustomers
<?xml version="1.0" encoding="ISO-8859-1"?>
<!--
Enumeration Examples
Demonstrates static and dynamic enumerations
-->
<items xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="items.xsd">
<enumtypes>
<!--
Static Enum: TicketPriority
- Fixed set of values, cannot be extended at runtime
- Use when values are known and stable
- dynamic="false" (default)
-->
<enumtype code="TicketPriority" autocreate="true" generate="true" dynamic="false">
<description>Support ticket priority levels</description>
<value code="LOW">
<description>Low priority - response within 72 hours</description>
</value>
<value code="MEDIUM">
<description>Medium priority - response within 24 hours</description>
</value>
<value code="HIGH">
<description>High priority - response within 4 hours</description>
</value>
<value code="CRITICAL">
<description>Critical - immediate response required</description>
</value>
</enumtype>
<!--
Static Enum: TicketStatus
- Workflow states for support tickets
-->
<enumtype code="TicketStatus" autocreate="true" generate="true" dynamic="false">
<description>Support ticket status</description>
<value code="OPEN"/>
<value code="IN_PROGRESS"/>
<value code="WAITING_ON_CUSTOMER"/>
<value code="RESOLVED"/>
<value code="CLOSED"/>
</enumtype>
<!--
Static Enum: PaymentMethodType
- Payment method categories
-->
<enumtype code="PaymentMethodType" autocreate="true" generate="true" dynamic="false">
<description>Payment method types</description>
<value code="CREDIT_CARD"/>
<value code="DEBIT_CARD"/>
<value code="BANK_TRANSFER"/>
<value code="PAYPAL"/>
<value code="INVOICE"/>
<value code="CASH_ON_DELIVERY"/>
</enumtype>
<!--
Dynamic Enum: ShippingCarrier
- Can be extended at runtime via ImpEx or API
- Use when new values may be added without rebuild
- dynamic="true"
-->
<enumtype code="ShippingCarrier" autocreate="true" generate="true" dynamic="true">
<description>Shipping carriers (extensible at runtime)</description>
<value code="DHL"/>
<value code="UPS"/>
<value code="FEDEX"/>
<value code="USPS"/>
</enumtype>
<!--
Dynamic Enum: ReturnReason
- Return reasons can be customized per business
-->
<enumtype code="ReturnReason" autocreate="true" generate="true" dynamic="true">
<description>Product return reasons (extensible)</description>
<value code="DEFECTIVE"/>
<value code="WRONG_ITEM"/>
<value code="NOT_AS_DESCRIBED"/>
<value code="CHANGED_MIND"/>
<value code="OTHER"/>
</enumtype>
</enumtypes>
<itemtypes>
<!--
SupportTicket: Uses enum attributes
-->
<itemtype code="SupportTicket" autocreate="true" generate="true">
<deployment table="SupportTickets" typecode="10030"/>
<attributes>
<attribute qualifier="ticketId" type="java.lang.String">
<modifiers read="true" write="true" optional="false" unique="true"/>
<persistence type="property"/>
</attribute>
<attribute qualifier="subject" type="java.lang.String">
<modifiers read="true" write="true" optional="false"/>
<persistence type="property"/>
</attribute>
<!-- Using TicketPriority enum -->
<attribute qualifier="priority" type="TicketPriority">
<description>Ticket priority level</description>
<modifiers read="true" write="true" optional="false"/>
<persistence type="property"/>
<!-- Default value using em() helper -->
<defaultvalue>em().getEnumerationValue("TicketPriority","MEDIUM")</defaultvalue>
</attribute>
<!-- Using TicketStatus enum -->
<attribute qualifier="status" type="TicketStatus">
<description>Current ticket status</description>
<modifiers read="true" write="true" optional="false"/>
<persistence type="property"/>
<defaultvalue>em().getEnumerationValue("TicketStatus","OPEN")</defaultvalue>
</attribute>
</attributes>
<indexes>
<index name="priorityIdx">
<key attribute="priority"/>
</index>
<index name="statusIdx">
<key attribute="status"/>
</index>
</indexes>
</itemtype>
<!--
Shipment: Uses dynamic enum
-->
<itemtype code="Shipment" autocreate="true" generate="true">
<deployment table="Shipments" typecode="10031"/>
<attributes>
<attribute qualifier="trackingNumber" type="java.lang.String">
<modifiers read="true" write="true" optional="false" unique="true"/>
<persistence type="property"/>
</attribute>
<!-- Using dynamic ShippingCarrier enum -->
<attribute qualifier="carrier" type="ShippingCarrier">
<description>Shipping carrier (new carriers can be added at runtime)</description>
<modifiers read="true" write="true" optional="false"/>
<persistence type="property"/>
</attribute>
</attributes>
</itemtype>
</itemtypes>
</items>
<?xml version="1.0" encoding="ISO-8859-1"?>
<!--
Extended Product Type Example
Demonstrates extending existing Product type with custom attributes
-->
<items xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="items.xsd">
<!-- Define custom enum for product availability -->
<enumtypes>
<enumtype code="ProductAvailabilityStatus" autocreate="true" generate="true" dynamic="false">
<description>Product availability status</description>
<value code="IN_STOCK"/>
<value code="LOW_STOCK"/>
<value code="OUT_OF_STOCK"/>
<value code="DISCONTINUED"/>
<value code="PREORDER"/>
</enumtype>
</enumtypes>
<itemtypes>
<!--
Brand: Related item type for product brand
-->
<itemtype code="Brand" autocreate="true" generate="true">
<deployment table="Brands" typecode="10010"/>
<attributes>
<attribute qualifier="code" type="java.lang.String">
<modifiers read="true" write="true" optional="false" unique="true"/>
<persistence type="property"/>
</attribute>
<attribute qualifier="name" type="localized:java.lang.String">
<modifiers read="true" write="true" optional="true"/>
<persistence type="property"/>
</attribute>
<attribute qualifier="logo" type="Media">
<modifiers read="true" write="true" optional="true"/>
<persistence type="property"/>
</attribute>
</attributes>
</itemtype>
<!--
CustomProduct: Extends the standard Product type
- Inherits all Product attributes (name, code, catalog, etc.)
- Adds custom attributes for business needs
-->
<itemtype code="CustomProduct"
extends="Product"
autocreate="true"
generate="true"
jaloclass="com.example.jalo.CustomProduct">
<!-- Separate table for custom attributes (joined inheritance) -->
<deployment table="CustomProducts" typecode="10002"/>
<attributes>
<!-- Localized attribute - supports multiple languages -->
<attribute qualifier="shortDescription" type="localized:java.lang.String">
<description>Short product description (localized)</description>
<modifiers read="true" write="true" optional="true"/>
<persistence type="property"/>
</attribute>
<!-- Enum attribute - uses ProductAvailabilityStatus -->
<attribute qualifier="availabilityStatus" type="ProductAvailabilityStatus">
<description>Current availability status</description>
<modifiers read="true" write="true" optional="true"/>
<persistence type="property"/>
<defaultvalue>em().getEnumerationValue("ProductAvailabilityStatus","IN_STOCK")</defaultvalue>
</attribute>
<!-- Reference to another item type -->
<attribute qualifier="brand" type="Brand">
<description>Product brand</description>
<modifiers read="true" write="true" optional="true"/>
<persistence type="property"/>
</attribute>
<!-- Boolean for featured products -->
<attribute qualifier="featured" type="java.lang.Boolean">
<description>Is this a featured product</description>
<modifiers read="true" write="true" optional="true"/>
<persistence type="property"/>
<defaultvalue>Boolean.FALSE</defaultvalue>
</attribute>
<!-- Double for custom pricing field -->
<attribute qualifier="wholesalePrice" type="java.lang.Double">
<description>Wholesale price (B2B)</description>
<modifiers read="true" write="true" optional="true"/>
<persistence type="property"/>
</attribute>
<!-- Dynamic attribute - computed at runtime -->
<attribute qualifier="displayName" type="java.lang.String">
<description>Computed display name (brand + name)</description>
<modifiers read="true" write="false" optional="true"/>
<!-- Dynamic attributes use attributeHandler -->
<persistence type="dynamic" attributeHandler="customProductDisplayNameHandler"/>
</attribute>
</attributes>
<indexes>
<index name="availabilityStatusIdx">
<key attribute="availabilityStatus"/>
</index>
<index name="featuredIdx">
<key attribute="featured"/>
</index>
</indexes>
</itemtype>
</itemtypes>
</items>
<?xml version="1.0" encoding="ISO-8859-1"?>
<!--
Relation Examples
Demonstrates one-to-many and many-to-many relationships
-->
<items xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="items.xsd">
<itemtypes>
<!-- Store item type for one-to-many example -->
<itemtype code="Store" autocreate="true" generate="true">
<deployment table="Stores" typecode="10020"/>
<attributes>
<attribute qualifier="code" type="java.lang.String">
<modifiers read="true" write="true" optional="false" unique="true"/>
<persistence type="property"/>
</attribute>
<attribute qualifier="name" type="java.lang.String">
<modifiers read="true" write="true" optional="false"/>
<persistence type="property"/>
</attribute>
</attributes>
</itemtype>
<!-- StoreEmployee for one-to-many relation -->
<itemtype code="StoreEmployee" autocreate="true" generate="true">
<deployment table="StoreEmployees" typecode="10021"/>
<attributes>
<attribute qualifier="employeeId" type="java.lang.String">
<modifiers read="true" write="true" optional="false" unique="true"/>
<persistence type="property"/>
</attribute>
<attribute qualifier="name" type="java.lang.String">
<modifiers read="true" write="true" optional="false"/>
<persistence type="property"/>
</attribute>
<!-- store attribute created by relation -->
</attributes>
</itemtype>
<!-- ProductTag for many-to-many example -->
<itemtype code="ProductTag" autocreate="true" generate="true">
<deployment table="ProductTags" typecode="10022"/>
<attributes>
<attribute qualifier="code" type="java.lang.String">
<modifiers read="true" write="true" optional="false" unique="true"/>
<persistence type="property"/>
</attribute>
<attribute qualifier="name" type="localized:java.lang.String">
<modifiers read="true" write="true" optional="true"/>
<persistence type="property"/>
</attribute>
</attributes>
</itemtype>
</itemtypes>
<relations>
<!--
One-to-Many Relation: Store to StoreEmployee
- One Store has many StoreEmployees
- Each StoreEmployee belongs to one Store
- partof="true" means employees are deleted with store
-->
<relation code="Store2StoreEmployee" localized="false" autocreate="true">
<description>Store to Employee relationship</description>
<!-- Source: Store (the "one" side) -->
<sourceElement type="Store"
qualifier="employees"
cardinality="many"
collectiontype="list"
ordered="true">
<description>Employees working at this store</description>
<modifiers read="true" write="true" optional="true" partof="true"/>
</sourceElement>
<!-- Target: StoreEmployee (the "many" side) -->
<targetElement type="StoreEmployee"
qualifier="store"
cardinality="one">
<description>Store where employee works</description>
<modifiers read="true" write="true" optional="false"/>
</targetElement>
</relation>
<!--
Many-to-Many Relation: Product to ProductTag
- Products can have multiple tags
- Tags can be assigned to multiple products
- Requires deployment table for link records
-->
<relation code="Product2ProductTag" localized="false" autocreate="true">
<description>Product to Tag many-to-many relationship</description>
<!-- Deployment creates a link table -->
<deployment table="Prod2Tag" typecode="20100"/>
<!-- Source: Product -->
<sourceElement type="Product"
qualifier="tags"
cardinality="many"
collectiontype="set">
<description>Tags assigned to this product</description>
<modifiers read="true" write="true" optional="true"/>
</sourceElement>
<!-- Target: ProductTag -->
<targetElement type="ProductTag"
qualifier="products"
cardinality="many"
collectiontype="set">
<description>Products with this tag</description>
<modifiers read="true" write="true" optional="true"/>
</targetElement>
</relation>
<!--
Self-Referencing Relation: Employee Hierarchy
- Employee can have a manager (another Employee)
- Employee can have direct reports
-->
<relation code="Employee2Manager" localized="false" autocreate="true">
<description>Employee manager hierarchy</description>
<deployment table="Emp2Mgr" typecode="20200"/>
<sourceElement type="StoreEmployee"
qualifier="directReports"
cardinality="many"
collectiontype="set">
<description>Employees reporting to this manager</description>
<modifiers read="true" write="true" optional="true"/>
</sourceElement>
<targetElement type="StoreEmployee"
qualifier="manager"
cardinality="one">
<description>Manager of this employee</description>
<modifiers read="true" write="true" optional="true"/>
</targetElement>
</relation>
</relations>
</items>
<?xml version="1.0" encoding="ISO-8859-1"?>
<!--
Simple Item Type Example
Demonstrates basic item type with primitive attributes
-->
<items xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="items.xsd">
<itemtypes>
<!--
CustomerFeedback: A simple item type demonstrating primitive attributes
- Unique code identifier
- String, Integer, Date, Boolean attributes
- Optional and required fields
-->
<itemtype code="CustomerFeedback"
autocreate="true"
generate="true"
jaloclass="com.example.jalo.CustomerFeedback">
<!-- Database deployment configuration -->
<!-- typecode must be unique (10000-32767 for custom types) -->
<deployment table="CustomerFeedbacks" typecode="10001"/>
<attributes>
<!-- Unique identifier - required, searchable -->
<attribute qualifier="code" type="java.lang.String">
<description>Unique feedback code</description>
<modifiers read="true" write="true" optional="false" unique="true" search="true"/>
<persistence type="property"/>
</attribute>
<!-- String attribute - customer name -->
<attribute qualifier="customerName" type="java.lang.String">
<description>Name of the customer</description>
<modifiers read="true" write="true" optional="false"/>
<persistence type="property"/>
</attribute>
<!-- Integer attribute - rating score -->
<attribute qualifier="rating" type="java.lang.Integer">
<description>Rating from 1 to 5</description>
<modifiers read="true" write="true" optional="false"/>
<persistence type="property"/>
<defaultvalue>Integer.valueOf(3)</defaultvalue>
</attribute>
<!-- Date attribute - submission date -->
<attribute qualifier="submissionDate" type="java.util.Date">
<description>Date feedback was submitted</description>
<modifiers read="true" write="true" optional="true"/>
<persistence type="property"/>
</attribute>
<!-- Boolean attribute - verified flag -->
<attribute qualifier="verified" type="java.lang.Boolean">
<description>Whether feedback has been verified</description>
<modifiers read="true" write="true" optional="true"/>
<persistence type="property"/>
<defaultvalue>Boolean.FALSE</defaultvalue>
</attribute>
<!-- Long text attribute - feedback comment -->
<attribute qualifier="comment" type="java.lang.String">
<description>Detailed feedback comment</description>
<modifiers read="true" write="true" optional="true"/>
<!-- Use big column type for long text -->
<persistence type="property">
<columntype database="oracle">
<value>CLOB</value>
</columntype>
<columntype database="mysql">
<value>TEXT</value>
</columntype>
<columntype>
<value>HYBRIS.LONG_STRING</value>
</columntype>
</persistence>
</attribute>
</attributes>
<!-- Define indexes for frequently queried attributes -->
<indexes>
<index name="customerNameIdx">
<key attribute="customerName"/>
</index>
<index name="ratingIdx">
<key attribute="rating"/>
</index>
</indexes>
</itemtype>
</itemtypes>
</items>
<?xml version="1.0" encoding="UTF-8"?>
<!--
custom-web-spring.xml
Spring MVC configuration for OCC web module.
Place in web/webroot/WEB-INF/ directory.
-->
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!-- Enable annotation-driven MVC -->
<mvc:annotation-driven>
<mvc:message-converters>
<!-- JSON converter -->
<bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
<property name="objectMapper" ref="objectMapper"/>
</bean>
<!-- XML converter -->
<bean class="org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter"/>
</mvc:message-converters>
</mvc:annotation-driven>
<!-- Component scan for controllers -->
<context:component-scan base-package="com.example.controllers"/>
<!-- Object mapper configuration -->
<bean id="objectMapper" class="com.fasterxml.jackson.databind.ObjectMapper">
<property name="serializationInclusion" value="NON_NULL"/>
</bean>
<!-- Exception resolver -->
<bean class="org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver">
<property name="messageConverters">
<list>
<bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
<property name="objectMapper" ref="objectMapper"/>
</bean>
</list>
</property>
</bean>
<!-- CORS configuration -->
<mvc:cors>
<mvc:mapping path="/**"
allowed-origins="*"
allowed-methods="GET, POST, PUT, DELETE, OPTIONS"
allowed-headers="*"
allow-credentials="true"
max-age="3600"/>
</mvc:cors>
<!-- Content negotiation -->
<bean id="contentNegotiationManager"
class="org.springframework.web.accept.ContentNegotiationManagerFactoryBean">
<property name="favorPathExtension" value="false"/>
<property name="favorParameter" value="false"/>
<property name="ignoreAcceptHeader" value="false"/>
<property name="defaultContentType" value="application/json"/>
<property name="mediaTypes">
<map>
<entry key="json" value="application/json"/>
<entry key="xml" value="application/xml"/>
</map>
</property>
</bean>
</beans>
/*
* CustomProductController.java
* REST controller for custom product endpoints.
* Demonstrates OCC API patterns with Swagger documentation.
*/
package com.example.controllers;
import com.example.dto.CustomProductWsDTO;
import com.example.dto.CustomProductListWsDTO;
import com.example.facades.CustomProductFacade;
import com.example.facades.data.CustomProductData;
import de.hybris.platform.commercewebservicescommons.dto.product.ProductWsDTO;
import de.hybris.platform.webservicescommons.mapping.DataMapper;
import de.hybris.platform.webservicescommons.swagger.ApiBaseSiteIdParam;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;
import de.hybris.platform.servicelayer.exceptions.UnknownIdentifierException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
/**
* REST controller for custom product operations.
*
* URL Pattern: /occ/v2/{baseSiteId}/customproducts
*/
@Controller
@RequestMapping("/{baseSiteId}/customproducts")
@Api(tags = "Custom Products")
public class CustomProductController {
@Resource
private CustomProductFacade customProductFacade;
@Resource
private DataMapper dataMapper;
/**
* GET /customproducts
* Retrieve list of custom products with optional filtering.
*/
@RequestMapping(method = RequestMethod.GET)
@ResponseBody
@ApiOperation(
value = "Get custom products",
notes = "Returns a list of custom products with pagination support"
)
@ApiBaseSiteIdParam
public CustomProductListWsDTO getCustomProducts(
@ApiParam(value = "Base site identifier", required = true)
@PathVariable String baseSiteId,
@ApiParam(value = "Search query")
@RequestParam(required = false) String query,
@ApiParam(value = "Current page number", defaultValue = "0")
@RequestParam(defaultValue = "0") int currentPage,
@ApiParam(value = "Page size", defaultValue = "20")
@RequestParam(defaultValue = "20") int pageSize,
@ApiParam(value = "Response field level", defaultValue = "DEFAULT")
@RequestParam(defaultValue = "DEFAULT") String fields) {
List<CustomProductData> products = customProductFacade.searchProducts(query, currentPage, pageSize);
CustomProductListWsDTO result = new CustomProductListWsDTO();
result.setProducts(dataMapper.mapAsList(products, CustomProductWsDTO.class, fields));
result.setTotalCount(customProductFacade.getTotalCount(query));
return result;
}
/**
* GET /customproducts/{productCode}
* Retrieve single custom product by code.
*/
@RequestMapping(value = "/{productCode}", method = RequestMethod.GET)
@ResponseBody
@ApiOperation(
value = "Get custom product by code",
notes = "Returns detailed information about a specific custom product"
)
@ApiResponses({
@ApiResponse(code = 200, message = "Product found"),
@ApiResponse(code = 404, message = "Product not found")
})
public CustomProductWsDTO getCustomProduct(
@ApiParam(value = "Base site identifier", required = true)
@PathVariable String baseSiteId,
@ApiParam(value = "Product code", required = true)
@PathVariable String productCode,
@ApiParam(value = "Response field level", defaultValue = "DEFAULT")
@RequestParam(defaultValue = "DEFAULT") String fields) {
CustomProductData productData = customProductFacade.getProductForCode(productCode);
return dataMapper.map(productData, CustomProductWsDTO.class, fields);
}
/**
* POST /customproducts
* Create a new custom product.
*/
@RequestMapping(method = RequestMethod.POST)
@ResponseStatus(HttpStatus.CREATED)
@ResponseBody
@ApiOperation(
value = "Create custom product",
notes = "Creates a new custom product and returns the created resource"
)
@ApiResponses({
@ApiResponse(code = 201, message = "Product created successfully"),
@ApiResponse(code = 400, message = "Invalid request data")
})
public CustomProductWsDTO createCustomProduct(
@ApiParam(value = "Base site identifier", required = true)
@PathVariable String baseSiteId,
@ApiParam(value = "Product data", required = true)
@RequestBody CustomProductWsDTO productDto) {
CustomProductData productData = dataMapper.map(productDto, CustomProductData.class);
CustomProductData createdProduct = customProductFacade.createProduct(productData);
return dataMapper.map(createdProduct, CustomProductWsDTO.class, "FULL");
}
/**
* PUT /customproducts/{productCode}
* Update existing custom product.
*/
@RequestMapping(value = "/{productCode}", method = RequestMethod.PUT)
@ResponseBody
@ApiOperation(
value = "Update custom product",
notes = "Updates an existing custom product"
)
public CustomProductWsDTO updateCustomProduct(
@PathVariable String baseSiteId,
@PathVariable String productCode,
@RequestBody CustomProductWsDTO productDto) {
CustomProductData productData = dataMapper.map(productDto, CustomProductData.class);
productData.setCode(productCode);
CustomProductData updatedProduct = customProductFacade.updateProduct(productData);
return dataMapper.map(updatedProduct, CustomProductWsDTO.class, "FULL");
}
/**
* DELETE /customproducts/{productCode}
* Delete a custom product.
*/
@RequestMapping(value = "/{productCode}", method = RequestMethod.DELETE)
@ResponseStatus(HttpStatus.NO_CONTENT)
@ApiOperation(value = "Delete custom product")
public void deleteCustomProduct(
@PathVariable String baseSiteId,
@PathVariable String productCode) {
customProductFacade.deleteProduct(productCode);
}
@ExceptionHandler(UnknownIdentifierException.class)
@ResponseBody
public ResponseEntity<String> handleUnknownIdentifier(final UnknownIdentifierException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ex.getMessage());
}
@ExceptionHandler(IllegalArgumentException.class)
@ResponseBody
public ResponseEntity<String> handleIllegalArgument(final IllegalArgumentException ex) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ex.getMessage());
}
// Setter for testing
public void setCustomProductFacade(CustomProductFacade customProductFacade) {
this.customProductFacade = customProductFacade;
}
public void setDataMapper(DataMapper dataMapper) {
this.dataMapper = dataMapper;
}
}
/*
* CustomProductPopulator.java
* Populator for converting CustomProductData to CustomProductWsDTO.
* Used by converters in the OCC layer.
*/
package com.example.populators;
import com.example.dto.CustomProductWsDTO;
import com.example.facades.data.CustomProductData;
import de.hybris.platform.converters.Populator;
import de.hybris.platform.servicelayer.dto.converter.ConversionException;
/**
* Populates CustomProductWsDTO from CustomProductData.
*
* Populators fill specific fields in target objects.
* Multiple populators can be chained for different field levels.
*/
public class CustomProductPopulator implements Populator<CustomProductData, CustomProductWsDTO> {
@Override
public void populate(final CustomProductData source, final CustomProductWsDTO target)
throws ConversionException {
if (source == null) {
throw new ConversionException("Source cannot be null");
}
// Basic fields
target.setCode(source.getCode());
target.setName(source.getName());
target.setDescription(source.getDescription());
target.setSummary(source.getSummary());
target.setUrl(source.getUrl());
// Stock information
target.setStockStatus(source.getStockStatus());
target.setStockLevel(source.getStockLevel());
target.setPurchasable(source.isPurchasable());
// Category and brand
target.setCategoryCode(source.getCategoryCode());
target.setCategoryName(source.getCategoryName());
target.setBrandName(source.getBrandName());
// Reviews
target.setAverageRating(source.getAverageRating());
target.setNumberOfReviews(source.getNumberOfReviews());
// Custom fields
target.setCustomField(source.getCustomField());
target.setCustomStatus(source.getCustomStatus());
// Image URL (simple field)
target.setImageUrl(source.getImageUrl());
}
}
/*
* CustomProductWsDTO.java
* Web Service DTO for custom product API responses.
* Uses Swagger annotations for API documentation.
*/
package com.example.dto;
import de.hybris.platform.commercewebservicescommons.dto.product.PriceWsDTO;
import de.hybris.platform.commercewebservicescommons.dto.product.ImageWsDTO;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import java.io.Serializable;
import java.util.List;
/**
* WsDTO for custom product representation in OCC API.
*
* Field levels:
* - BASIC: code, name
* - DEFAULT: code, name, description, price
* - FULL: all fields
*/
@ApiModel(value = "CustomProduct", description = "Custom product representation")
public class CustomProductWsDTO implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "Unique product code", required = true, example = "CUSTOM001")
private String code;
@ApiModelProperty(value = "Product name", example = "Custom Product Name")
private String name;
@ApiModelProperty(value = "Product description")
private String description;
@ApiModelProperty(value = "Short summary")
private String summary;
@ApiModelProperty(value = "Product URL")
private String url;
@ApiModelProperty(value = "Price information")
private PriceWsDTO price;
@ApiModelProperty(value = "Stock availability status", example = "inStock")
private String stockStatus;
@ApiModelProperty(value = "Available stock quantity")
private Integer stockLevel;
@ApiModelProperty(value = "Whether product can be purchased")
private Boolean purchasable;
@ApiModelProperty(value = "Product images")
private List<ImageWsDTO> images;
@ApiModelProperty(value = "Primary image URL")
private String imageUrl;
@ApiModelProperty(value = "Category code")
private String categoryCode;
@ApiModelProperty(value = "Category name")
private String categoryName;
@ApiModelProperty(value = "Brand name")
private String brandName;
@ApiModelProperty(value = "Average customer rating", example = "4.5")
private Double averageRating;
@ApiModelProperty(value = "Number of customer reviews")
private Integer numberOfReviews;
@ApiModelProperty(value = "Custom field specific to this product type")
private String customField;
@ApiModelProperty(value = "Custom status")
private String customStatus;
// Getters and Setters
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getSummary() {
return summary;
}
public void setSummary(String summary) {
this.summary = summary;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public PriceWsDTO getPrice() {
return price;
}
public void setPrice(PriceWsDTO price) {
this.price = price;
}
public String getStockStatus() {
return stockStatus;
}
public void setStockStatus(String stockStatus) {
this.stockStatus = stockStatus;
}
public Integer getStockLevel() {
return stockLevel;
}
public void setStockLevel(Integer stockLevel) {
this.stockLevel = stockLevel;
}
public Boolean getPurchasable() {
return purchasable;
}
public void setPurchasable(Boolean purchasable) {
this.purchasable = purchasable;
}
public List<ImageWsDTO> getImages() {
return images;
}
public void setImages(List<ImageWsDTO> images) {
this.images = images;
}
public String getImageUrl() {
return imageUrl;
}
public void setImageUrl(String imageUrl) {
this.imageUrl = imageUrl;
}
public String getCategoryCode() {
return categoryCode;
}
public void setCategoryCode(String categoryCode) {
this.categoryCode = categoryCode;
}
public String getCategoryName() {
return categoryName;
}
public void setCategoryName(String categoryName) {
this.categoryName = categoryName;
}
public String getBrandName() {
return brandName;
}
public void setBrandName(String brandName) {
this.brandName = brandName;
}
public Double getAverageRating() {
return averageRating;
}
public void setAverageRating(Double averageRating) {
this.averageRating = averageRating;
}
public Integer getNumberOfReviews() {
return numberOfReviews;
}
public void setNumberOfReviews(Integer numberOfReviews) {
this.numberOfReviews = numberOfReviews;
}
public String getCustomField() {
return customField;
}
public void setCustomField(String customField) {
this.customField = customField;
}
public String getCustomStatus() {
return customStatus;
}
public void setCustomStatus(String customStatus) {
this.customStatus = customStatus;
}
}
<?xml version="1.0" encoding="UTF-8"?>
<!--
occ-extension-spring.xml
Spring configuration for OCC extension beans.
Place in resources/ directory of your OCC extension.
-->
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- ================================ -->
<!-- POPULATORS -->
<!-- ================================ -->
<bean id="customProductPopulator"
class="com.example.populators.CustomProductPopulator"/>
<bean id="customProductPricePopulator"
class="com.example.populators.CustomProductPricePopulator">
<property name="priceDataFactory" ref="priceDataFactory"/>
<property name="commonI18NService" ref="commonI18NService"/>
</bean>
<bean id="customProductImagePopulator"
class="com.example.populators.CustomProductImagePopulator">
<property name="imageConverter" ref="imageConverter"/>
</bean>
<!-- ================================ -->
<!-- CONVERTERS -->
<!-- ================================ -->
<!-- Basic converter - minimal fields -->
<alias name="defaultBasicCustomProductConverter" alias="basicCustomProductConverter"/>
<bean id="defaultBasicCustomProductConverter" parent="abstractPopulatingConverter">
<property name="targetClass" value="com.example.dto.CustomProductWsDTO"/>
<property name="populators">
<list>
<ref bean="customProductPopulator"/>
</list>
</property>
</bean>
<!-- Full converter - all fields -->
<alias name="defaultCustomProductConverter" alias="customProductConverter"/>
<bean id="defaultCustomProductConverter" parent="abstractPopulatingConverter">
<property name="targetClass" value="com.example.dto.CustomProductWsDTO"/>
<property name="populators">
<list>
<ref bean="customProductPopulator"/>
<ref bean="customProductPricePopulator"/>
<ref bean="customProductImagePopulator"/>
</list>
</property>
</bean>
<!-- ================================ -->
<!-- FIELD LEVEL MAPPING -->
<!-- ================================ -->
<!-- Define which fields to include at each level -->
<bean parent="fieldSetLevelMapping">
<property name="dtoClass" value="com.example.dto.CustomProductWsDTO"/>
<property name="levelMapping">
<map>
<entry key="BASIC" value="code,name"/>
<entry key="DEFAULT" value="code,name,description,price,stockStatus,purchasable"/>
<entry key="FULL" value="code,name,description,summary,url,price,stockStatus,stockLevel,purchasable,images,imageUrl,categoryCode,categoryName,brandName,averageRating,numberOfReviews,customField,customStatus"/>
</map>
</property>
</bean>
<!-- List DTO field mapping -->
<bean parent="fieldSetLevelMapping">
<property name="dtoClass" value="com.example.dto.CustomProductListWsDTO"/>
<property name="levelMapping">
<map>
<entry key="BASIC" value="products(BASIC),totalCount"/>
<entry key="DEFAULT" value="products(DEFAULT),totalCount"/>
<entry key="FULL" value="products(FULL),totalCount"/>
</map>
</property>
</bean>
<!-- ================================ -->
<!-- CONTROLLERS -->
<!-- ================================ -->
<bean id="customProductController"
class="com.example.controllers.CustomProductController">
<property name="customProductFacade" ref="customProductFacade"/>
<property name="dataMapper" ref="dataMapper"/>
</bean>
<!-- ================================ -->
<!-- VALIDATORS -->
<!-- ================================ -->
<bean id="customProductValidator"
class="com.example.validators.CustomProductValidator"/>
<!-- ================================ -->
<!-- EXCEPTION HANDLERS -->
<!-- ================================ -->
<!-- Add custom exception handling -->
<bean id="customExceptionHandler"
class="com.example.handlers.CustomExceptionHandler"/>
</beans>