This is page 36 of 43. Use http://codebase.md/taurgis/sfcc-dev-mcp?page={x} to view the full context. # Directory Structure ``` ├── .DS_Store ├── .github │ ├── dependabot.yml │ ├── instructions │ │ ├── mcp-node-tests.instructions.md │ │ └── mcp-yml-tests.instructions.md │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.yml │ │ ├── config.yml │ │ ├── documentation.yml │ │ ├── feature_request.yml │ │ └── question.yml │ ├── PULL_REQUEST_TEMPLATE │ │ ├── bug_fix.md │ │ ├── documentation.md │ │ └── new_tool.md │ ├── pull_request_template.md │ └── workflows │ ├── ci.yml │ ├── deploy-pages.yml │ ├── publish.yml │ └── update-docs.yml ├── .gitignore ├── .husky │ └── pre-commit ├── aegis.config.docs-only.json ├── aegis.config.json ├── aegis.config.with-dw.json ├── AGENTS.md ├── ai-instructions │ ├── claude-desktop │ │ └── claude_custom_instructions.md │ ├── cursor │ │ └── .cursor │ │ └── rules │ │ ├── debugging-workflows.mdc │ │ ├── hooks-development.mdc │ │ ├── isml-templates.mdc │ │ ├── job-framework.mdc │ │ ├── performance-optimization.mdc │ │ ├── scapi-endpoints.mdc │ │ ├── security-patterns.mdc │ │ ├── sfcc-development.mdc │ │ ├── sfra-controllers.mdc │ │ ├── sfra-models.mdc │ │ ├── system-objects.mdc │ │ └── testing-patterns.mdc │ └── github-copilot │ └── copilot-instructions.md ├── CHANGELOG.md ├── CONTRIBUTING.md ├── docs │ ├── best-practices │ │ ├── cartridge_creation.md │ │ ├── isml_templates.md │ │ ├── job_framework.md │ │ ├── localserviceregistry.md │ │ ├── ocapi_hooks.md │ │ ├── performance.md │ │ ├── scapi_custom_endpoint.md │ │ ├── scapi_hooks.md │ │ ├── security.md │ │ ├── sfra_client_side_js.md │ │ ├── sfra_controllers.md │ │ ├── sfra_models.md │ │ └── sfra_scss.md │ ├── dw_campaign │ │ ├── ABTest.md │ │ ├── ABTestMgr.md │ │ ├── ABTestSegment.md │ │ ├── AmountDiscount.md │ │ ├── ApproachingDiscount.md │ │ ├── BonusChoiceDiscount.md │ │ ├── BonusDiscount.md │ │ ├── Campaign.md │ │ ├── CampaignMgr.md │ │ ├── CampaignStatusCodes.md │ │ ├── Coupon.md │ │ ├── CouponMgr.md │ │ ├── CouponRedemption.md │ │ ├── CouponStatusCodes.md │ │ ├── Discount.md │ │ ├── DiscountPlan.md │ │ ├── FixedPriceDiscount.md │ │ ├── FixedPriceShippingDiscount.md │ │ ├── FreeDiscount.md │ │ ├── FreeShippingDiscount.md │ │ ├── PercentageDiscount.md │ │ ├── PercentageOptionDiscount.md │ │ ├── PriceBookPriceDiscount.md │ │ ├── Promotion.md │ │ ├── PromotionMgr.md │ │ ├── PromotionPlan.md │ │ ├── SlotContent.md │ │ ├── SourceCodeGroup.md │ │ ├── SourceCodeInfo.md │ │ ├── SourceCodeStatusCodes.md │ │ └── TotalFixedPriceDiscount.md │ ├── dw_catalog │ │ ├── Catalog.md │ │ ├── CatalogMgr.md │ │ ├── Category.md │ │ ├── CategoryAssignment.md │ │ ├── CategoryLink.md │ │ ├── PriceBook.md │ │ ├── PriceBookMgr.md │ │ ├── Product.md │ │ ├── ProductActiveData.md │ │ ├── ProductAttributeModel.md │ │ ├── ProductAvailabilityLevels.md │ │ ├── ProductAvailabilityModel.md │ │ ├── ProductInventoryList.md │ │ ├── ProductInventoryMgr.md │ │ ├── ProductInventoryRecord.md │ │ ├── ProductLink.md │ │ ├── ProductMgr.md │ │ ├── ProductOption.md │ │ ├── ProductOptionModel.md │ │ ├── ProductOptionValue.md │ │ ├── ProductPriceInfo.md │ │ ├── ProductPriceModel.md │ │ ├── ProductPriceTable.md │ │ ├── ProductSearchHit.md │ │ ├── ProductSearchModel.md │ │ ├── ProductSearchRefinementDefinition.md │ │ ├── ProductSearchRefinements.md │ │ ├── ProductSearchRefinementValue.md │ │ ├── ProductVariationAttribute.md │ │ ├── ProductVariationAttributeValue.md │ │ ├── ProductVariationModel.md │ │ ├── Recommendation.md │ │ ├── SearchModel.md │ │ ├── SearchRefinementDefinition.md │ │ ├── SearchRefinements.md │ │ ├── SearchRefinementValue.md │ │ ├── SortingOption.md │ │ ├── SortingRule.md │ │ ├── Store.md │ │ ├── StoreGroup.md │ │ ├── StoreInventoryFilter.md │ │ ├── StoreInventoryFilterValue.md │ │ ├── StoreMgr.md │ │ ├── Variant.md │ │ └── VariationGroup.md │ ├── dw_content │ │ ├── Content.md │ │ ├── ContentMgr.md │ │ ├── ContentSearchModel.md │ │ ├── ContentSearchRefinementDefinition.md │ │ ├── ContentSearchRefinements.md │ │ ├── ContentSearchRefinementValue.md │ │ ├── Folder.md │ │ ├── Library.md │ │ ├── MarkupText.md │ │ └── MediaFile.md │ ├── dw_crypto │ │ ├── CertificateRef.md │ │ ├── CertificateUtils.md │ │ ├── Cipher.md │ │ ├── Encoding.md │ │ ├── JWE.md │ │ ├── JWEHeader.md │ │ ├── JWS.md │ │ ├── JWSHeader.md │ │ ├── KeyRef.md │ │ ├── Mac.md │ │ ├── MessageDigest.md │ │ ├── SecureRandom.md │ │ ├── Signature.md │ │ ├── WeakCipher.md │ │ ├── WeakMac.md │ │ ├── WeakMessageDigest.md │ │ ├── WeakSignature.md │ │ └── X509Certificate.md │ ├── dw_customer │ │ ├── AddressBook.md │ │ ├── AgentUserMgr.md │ │ ├── AgentUserStatusCodes.md │ │ ├── AuthenticationStatus.md │ │ ├── Credentials.md │ │ ├── Customer.md │ │ ├── CustomerActiveData.md │ │ ├── CustomerAddress.md │ │ ├── CustomerCDPData.md │ │ ├── CustomerContextMgr.md │ │ ├── CustomerGroup.md │ │ ├── CustomerList.md │ │ ├── CustomerMgr.md │ │ ├── CustomerPasswordConstraints.md │ │ ├── CustomerPaymentInstrument.md │ │ ├── CustomerStatusCodes.md │ │ ├── EncryptedObject.md │ │ ├── ExternalProfile.md │ │ ├── OrderHistory.md │ │ ├── ProductList.md │ │ ├── ProductListItem.md │ │ ├── ProductListItemPurchase.md │ │ ├── ProductListMgr.md │ │ ├── ProductListRegistrant.md │ │ ├── Profile.md │ │ └── Wallet.md │ ├── dw_extensions.applepay │ │ ├── ApplePayHookResult.md │ │ └── ApplePayHooks.md │ ├── dw_extensions.facebook │ │ ├── FacebookFeedHooks.md │ │ └── FacebookProduct.md │ ├── dw_extensions.paymentrequest │ │ ├── PaymentRequestHookResult.md │ │ └── PaymentRequestHooks.md │ ├── dw_extensions.payments │ │ ├── SalesforceBancontactPaymentDetails.md │ │ ├── SalesforceCardPaymentDetails.md │ │ ├── SalesforceEpsPaymentDetails.md │ │ ├── SalesforceIdealPaymentDetails.md │ │ ├── SalesforceKlarnaPaymentDetails.md │ │ ├── SalesforcePaymentDetails.md │ │ ├── SalesforcePaymentIntent.md │ │ ├── SalesforcePaymentMethod.md │ │ ├── SalesforcePaymentRequest.md │ │ ├── SalesforcePaymentsHooks.md │ │ ├── SalesforcePaymentsMgr.md │ │ ├── SalesforcePaymentsSiteConfiguration.md │ │ ├── SalesforcePayPalOrder.md │ │ ├── SalesforcePayPalOrderAddress.md │ │ ├── SalesforcePayPalOrderPayer.md │ │ ├── SalesforcePayPalPaymentDetails.md │ │ ├── SalesforceSepaDebitPaymentDetails.md │ │ └── SalesforceVenmoPaymentDetails.md │ ├── dw_extensions.pinterest │ │ ├── PinterestAvailability.md │ │ ├── PinterestFeedHooks.md │ │ ├── PinterestOrder.md │ │ ├── PinterestOrderHooks.md │ │ └── PinterestProduct.md │ ├── dw_io │ │ ├── CSVStreamReader.md │ │ ├── CSVStreamWriter.md │ │ ├── File.md │ │ ├── FileReader.md │ │ ├── FileWriter.md │ │ ├── InputStream.md │ │ ├── OutputStream.md │ │ ├── PrintWriter.md │ │ ├── RandomAccessFileReader.md │ │ ├── Reader.md │ │ ├── StringWriter.md │ │ ├── Writer.md │ │ ├── XMLIndentingStreamWriter.md │ │ ├── XMLStreamConstants.md │ │ ├── XMLStreamReader.md │ │ └── XMLStreamWriter.md │ ├── dw_job │ │ ├── JobExecution.md │ │ └── JobStepExecution.md │ ├── dw_net │ │ ├── FTPClient.md │ │ ├── FTPFileInfo.md │ │ ├── HTTPClient.md │ │ ├── HTTPRequestPart.md │ │ ├── Mail.md │ │ ├── SFTPClient.md │ │ ├── SFTPFileInfo.md │ │ ├── WebDAVClient.md │ │ └── WebDAVFileInfo.md │ ├── dw_object │ │ ├── ActiveData.md │ │ ├── CustomAttributes.md │ │ ├── CustomObject.md │ │ ├── CustomObjectMgr.md │ │ ├── Extensible.md │ │ ├── ExtensibleObject.md │ │ ├── Note.md │ │ ├── ObjectAttributeDefinition.md │ │ ├── ObjectAttributeGroup.md │ │ ├── ObjectAttributeValueDefinition.md │ │ ├── ObjectTypeDefinition.md │ │ ├── PersistentObject.md │ │ ├── SimpleExtensible.md │ │ └── SystemObjectMgr.md │ ├── dw_order │ │ ├── AbstractItem.md │ │ ├── AbstractItemCtnr.md │ │ ├── Appeasement.md │ │ ├── AppeasementItem.md │ │ ├── Basket.md │ │ ├── BasketMgr.md │ │ ├── BonusDiscountLineItem.md │ │ ├── CouponLineItem.md │ │ ├── CreateAgentBasketLimitExceededException.md │ │ ├── CreateBasketFromOrderException.md │ │ ├── CreateCouponLineItemException.md │ │ ├── CreateOrderException.md │ │ ├── CreateTemporaryBasketLimitExceededException.md │ │ ├── GiftCertificate.md │ │ ├── GiftCertificateLineItem.md │ │ ├── GiftCertificateMgr.md │ │ ├── GiftCertificateStatusCodes.md │ │ ├── Invoice.md │ │ ├── InvoiceItem.md │ │ ├── LineItem.md │ │ ├── LineItemCtnr.md │ │ ├── Order.md │ │ ├── OrderAddress.md │ │ ├── OrderItem.md │ │ ├── OrderMgr.md │ │ ├── OrderPaymentInstrument.md │ │ ├── OrderProcessStatusCodes.md │ │ ├── PaymentCard.md │ │ ├── PaymentInstrument.md │ │ ├── PaymentMethod.md │ │ ├── PaymentMgr.md │ │ ├── PaymentProcessor.md │ │ ├── PaymentStatusCodes.md │ │ ├── PaymentTransaction.md │ │ ├── PriceAdjustment.md │ │ ├── PriceAdjustmentLimitTypes.md │ │ ├── ProductLineItem.md │ │ ├── ProductShippingCost.md │ │ ├── ProductShippingLineItem.md │ │ ├── ProductShippingModel.md │ │ ├── Return.md │ │ ├── ReturnCase.md │ │ ├── ReturnCaseItem.md │ │ ├── ReturnItem.md │ │ ├── Shipment.md │ │ ├── ShipmentShippingCost.md │ │ ├── ShipmentShippingModel.md │ │ ├── ShippingLineItem.md │ │ ├── ShippingLocation.md │ │ ├── ShippingMethod.md │ │ ├── ShippingMgr.md │ │ ├── ShippingOrder.md │ │ ├── ShippingOrderItem.md │ │ ├── SumItem.md │ │ ├── TaxGroup.md │ │ ├── TaxItem.md │ │ ├── TaxMgr.md │ │ ├── TrackingInfo.md │ │ └── TrackingRef.md │ ├── dw_order.hooks │ │ ├── CalculateHooks.md │ │ ├── OrderHooks.md │ │ ├── PaymentHooks.md │ │ ├── ReturnHooks.md │ │ └── ShippingOrderHooks.md │ ├── dw_rpc │ │ ├── SOAPUtil.md │ │ ├── Stub.md │ │ └── WebReference.md │ ├── dw_suggest │ │ ├── BrandSuggestions.md │ │ ├── CategorySuggestions.md │ │ ├── ContentSuggestions.md │ │ ├── CustomSuggestions.md │ │ ├── ProductSuggestions.md │ │ ├── SearchPhraseSuggestions.md │ │ ├── SuggestedCategory.md │ │ ├── SuggestedContent.md │ │ ├── SuggestedPhrase.md │ │ ├── SuggestedProduct.md │ │ ├── SuggestedTerm.md │ │ ├── SuggestedTerms.md │ │ ├── Suggestions.md │ │ └── SuggestModel.md │ ├── dw_svc │ │ ├── FTPService.md │ │ ├── FTPServiceDefinition.md │ │ ├── HTTPFormService.md │ │ ├── HTTPFormServiceDefinition.md │ │ ├── HTTPService.md │ │ ├── HTTPServiceDefinition.md │ │ ├── LocalServiceRegistry.md │ │ ├── Result.md │ │ ├── Service.md │ │ ├── ServiceCallback.md │ │ ├── ServiceConfig.md │ │ ├── ServiceCredential.md │ │ ├── ServiceDefinition.md │ │ ├── ServiceProfile.md │ │ ├── ServiceRegistry.md │ │ ├── SOAPService.md │ │ └── SOAPServiceDefinition.md │ ├── dw_system │ │ ├── AgentUserStatusCodes.md │ │ ├── Cache.md │ │ ├── CacheMgr.md │ │ ├── HookMgr.md │ │ ├── InternalObject.md │ │ ├── JobProcessMonitor.md │ │ ├── Log.md │ │ ├── Logger.md │ │ ├── LogNDC.md │ │ ├── OrganizationPreferences.md │ │ ├── Pipeline.md │ │ ├── PipelineDictionary.md │ │ ├── RemoteInclude.md │ │ ├── Request.md │ │ ├── RequestHooks.md │ │ ├── Response.md │ │ ├── RESTErrorResponse.md │ │ ├── RESTResponseMgr.md │ │ ├── RESTSuccessResponse.md │ │ ├── SearchStatus.md │ │ ├── Session.md │ │ ├── Site.md │ │ ├── SitePreferences.md │ │ ├── Status.md │ │ ├── StatusItem.md │ │ ├── System.md │ │ └── Transaction.md │ ├── dw_util │ │ ├── ArrayList.md │ │ ├── Assert.md │ │ ├── BigInteger.md │ │ ├── Bytes.md │ │ ├── Calendar.md │ │ ├── Collection.md │ │ ├── Currency.md │ │ ├── DateUtils.md │ │ ├── Decimal.md │ │ ├── FilteringCollection.md │ │ ├── Geolocation.md │ │ ├── HashMap.md │ │ ├── HashSet.md │ │ ├── Iterator.md │ │ ├── LinkedHashMap.md │ │ ├── LinkedHashSet.md │ │ ├── List.md │ │ ├── Locale.md │ │ ├── Map.md │ │ ├── MapEntry.md │ │ ├── MappingKey.md │ │ ├── MappingMgr.md │ │ ├── PropertyComparator.md │ │ ├── SecureEncoder.md │ │ ├── SecureFilter.md │ │ ├── SeekableIterator.md │ │ ├── Set.md │ │ ├── SortedMap.md │ │ ├── SortedSet.md │ │ ├── StringUtils.md │ │ ├── Template.md │ │ └── UUIDUtils.md │ ├── dw_value │ │ ├── EnumValue.md │ │ ├── MimeEncodedText.md │ │ ├── Money.md │ │ └── Quantity.md │ ├── dw_web │ │ ├── ClickStream.md │ │ ├── ClickStreamEntry.md │ │ ├── Cookie.md │ │ ├── Cookies.md │ │ ├── CSRFProtection.md │ │ ├── Form.md │ │ ├── FormAction.md │ │ ├── FormElement.md │ │ ├── FormElementValidationResult.md │ │ ├── FormField.md │ │ ├── FormFieldOption.md │ │ ├── FormFieldOptions.md │ │ ├── FormGroup.md │ │ ├── FormList.md │ │ ├── FormListItem.md │ │ ├── Forms.md │ │ ├── HttpParameter.md │ │ ├── HttpParameterMap.md │ │ ├── LoopIterator.md │ │ ├── PageMetaData.md │ │ ├── PageMetaTag.md │ │ ├── PagingModel.md │ │ ├── Resource.md │ │ ├── URL.md │ │ ├── URLAction.md │ │ ├── URLParameter.md │ │ ├── URLRedirect.md │ │ ├── URLRedirectMgr.md │ │ └── URLUtils.md │ ├── sfra │ │ ├── account.md │ │ ├── address.md │ │ ├── billing.md │ │ ├── cart.md │ │ ├── categories.md │ │ ├── content.md │ │ ├── locale.md │ │ ├── order.md │ │ ├── payment.md │ │ ├── price-default.md │ │ ├── price-range.md │ │ ├── price-tiered.md │ │ ├── product-bundle.md │ │ ├── product-full.md │ │ ├── product-line-items.md │ │ ├── product-search.md │ │ ├── product-tile.md │ │ ├── querystring.md │ │ ├── render.md │ │ ├── request.md │ │ ├── response.md │ │ ├── server.md │ │ ├── shipping.md │ │ ├── store.md │ │ ├── stores.md │ │ └── totals.md │ └── TopLevel │ ├── APIException.md │ ├── arguments.md │ ├── Array.md │ ├── ArrayBuffer.md │ ├── BigInt.md │ ├── Boolean.md │ ├── ConversionError.md │ ├── DataView.md │ ├── Date.md │ ├── Error.md │ ├── ES6Iterator.md │ ├── EvalError.md │ ├── Fault.md │ ├── Float32Array.md │ ├── Float64Array.md │ ├── Function.md │ ├── Generator.md │ ├── global.md │ ├── Int16Array.md │ ├── Int32Array.md │ ├── Int8Array.md │ ├── InternalError.md │ ├── IOError.md │ ├── Iterable.md │ ├── Iterator.md │ ├── JSON.md │ ├── Map.md │ ├── Math.md │ ├── Module.md │ ├── Namespace.md │ ├── Number.md │ ├── Object.md │ ├── QName.md │ ├── RangeError.md │ ├── ReferenceError.md │ ├── RegExp.md │ ├── Set.md │ ├── StopIteration.md │ ├── String.md │ ├── Symbol.md │ ├── SyntaxError.md │ ├── SystemError.md │ ├── TypeError.md │ ├── Uint16Array.md │ ├── Uint32Array.md │ ├── Uint8Array.md │ ├── Uint8ClampedArray.md │ ├── URIError.md │ ├── WeakMap.md │ ├── WeakSet.md │ ├── XML.md │ ├── XMLList.md │ └── XMLStreamError.md ├── docs-site │ ├── .gitignore │ ├── App.tsx │ ├── components │ │ ├── Badge.tsx │ │ ├── BreadcrumbSchema.tsx │ │ ├── CodeBlock.tsx │ │ ├── Collapsible.tsx │ │ ├── ConfigBuilder.tsx │ │ ├── ConfigHero.tsx │ │ ├── ConfigModeTabs.tsx │ │ ├── icons.tsx │ │ ├── Layout.tsx │ │ ├── LightCodeContainer.tsx │ │ ├── NewcomerCTA.tsx │ │ ├── NextStepsStrip.tsx │ │ ├── OnThisPage.tsx │ │ ├── Search.tsx │ │ ├── SEO.tsx │ │ ├── Sidebar.tsx │ │ ├── StructuredData.tsx │ │ ├── ToolCard.tsx │ │ ├── ToolFilters.tsx │ │ ├── Typography.tsx │ │ └── VersionBadge.tsx │ ├── constants.tsx │ ├── index.html │ ├── main.tsx │ ├── metadata.json │ ├── package-lock.json │ ├── package.json │ ├── pages │ │ ├── AIInterfacesPage.tsx │ │ ├── ConfigurationPage.tsx │ │ ├── DevelopmentPage.tsx │ │ ├── ExamplesPage.tsx │ │ ├── FeaturesPage.tsx │ │ ├── HomePage.tsx │ │ ├── SecurityPage.tsx │ │ ├── ToolsPage.tsx │ │ └── TroubleshootingPage.tsx │ ├── postcss.config.js │ ├── public │ │ ├── .well-known │ │ │ └── security.txt │ │ ├── 404.html │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon.png │ │ ├── explain-product-pricing-methods-no-mcp.png │ │ ├── explain-product-pricing-methods.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon.ico │ │ ├── llms.txt │ │ ├── robots.txt │ │ ├── site.webmanifest │ │ └── sitemap.xml │ ├── README.md │ ├── scripts │ │ ├── generate-search-index.js │ │ ├── generate-sitemap.js │ │ └── search-dev.js │ ├── src │ │ └── styles │ │ ├── input.css │ │ └── prism-theme.css │ ├── tailwind.config.js │ ├── tsconfig.json │ ├── types.ts │ ├── utils │ │ ├── search.ts │ │ └── toolsData.ts │ └── vite.config.ts ├── eslint.config.js ├── jest.config.js ├── LICENSE ├── package-lock.json ├── package.json ├── README.md ├── scripts │ └── convert-docs.js ├── SECURITY.md ├── server.json ├── src │ ├── clients │ │ ├── base │ │ │ ├── http-client.ts │ │ │ ├── oauth-token.ts │ │ │ └── ocapi-auth-client.ts │ │ ├── best-practices-client.ts │ │ ├── cartridge-generation-client.ts │ │ ├── docs │ │ │ ├── class-content-parser.ts │ │ │ ├── class-name-resolver.ts │ │ │ ├── documentation-scanner.ts │ │ │ ├── index.ts │ │ │ └── referenced-types-extractor.ts │ │ ├── docs-client.ts │ │ ├── log-client.ts │ │ ├── logs │ │ │ ├── index.ts │ │ │ ├── log-analyzer.ts │ │ │ ├── log-client.ts │ │ │ ├── log-constants.ts │ │ │ ├── log-file-discovery.ts │ │ │ ├── log-file-reader.ts │ │ │ ├── log-formatter.ts │ │ │ ├── log-processor.ts │ │ │ ├── log-types.ts │ │ │ └── webdav-client-manager.ts │ │ ├── ocapi │ │ │ ├── code-versions-client.ts │ │ │ ├── site-preferences-client.ts │ │ │ └── system-objects-client.ts │ │ ├── ocapi-client.ts │ │ └── sfra-client.ts │ ├── config │ │ ├── configuration-factory.ts │ │ └── dw-json-loader.ts │ ├── core │ │ ├── handlers │ │ │ ├── abstract-log-tool-handler.ts │ │ │ ├── base-handler.ts │ │ │ ├── best-practices-handler.ts │ │ │ ├── cartridge-handler.ts │ │ │ ├── client-factory.ts │ │ │ ├── code-version-handler.ts │ │ │ ├── docs-handler.ts │ │ │ ├── job-log-handler.ts │ │ │ ├── job-log-tool-config.ts │ │ │ ├── log-handler.ts │ │ │ ├── log-tool-config.ts │ │ │ ├── sfra-handler.ts │ │ │ ├── system-object-handler.ts │ │ │ └── validation-helpers.ts │ │ ├── server.ts │ │ └── tool-definitions.ts │ ├── index.ts │ ├── main.ts │ ├── services │ │ ├── file-system-service.ts │ │ ├── index.ts │ │ └── path-service.ts │ ├── tool-configs │ │ ├── best-practices-tool-config.ts │ │ ├── cartridge-tool-config.ts │ │ ├── code-version-tool-config.ts │ │ ├── docs-tool-config.ts │ │ ├── job-log-tool-config.ts │ │ ├── log-tool-config.ts │ │ ├── sfra-tool-config.ts │ │ └── system-object-tool-config.ts │ ├── types │ │ └── types.ts │ └── utils │ ├── cache.ts │ ├── job-log-tool-config.ts │ ├── job-log-utils.ts │ ├── log-cache.ts │ ├── log-tool-config.ts │ ├── log-tool-constants.ts │ ├── log-tool-utils.ts │ ├── logger.ts │ ├── ocapi-url-builder.ts │ ├── path-resolver.ts │ ├── query-builder.ts │ ├── utils.ts │ └── validator.ts ├── tests │ ├── __mocks__ │ │ ├── docs-client.ts │ │ ├── src │ │ │ └── clients │ │ │ └── base │ │ │ └── http-client.js │ │ └── webdav.js │ ├── base-handler.test.ts │ ├── base-http-client.test.ts │ ├── best-practices-handler.test.ts │ ├── cache.test.ts │ ├── cartridge-handler.test.ts │ ├── class-content-parser.test.ts │ ├── class-name-resolver.test.ts │ ├── client-factory.test.ts │ ├── code-version-handler.test.ts │ ├── code-versions-client.test.ts │ ├── config.test.ts │ ├── configuration-factory.test.ts │ ├── docs-handler.test.ts │ ├── documentation-scanner.test.ts │ ├── file-system-service.test.ts │ ├── job-log-handler.test.ts │ ├── job-log-utils.test.ts │ ├── log-client.test.ts │ ├── log-handler.test.ts │ ├── log-processor.test.ts │ ├── logger.test.ts │ ├── mcp │ │ ├── AGENTS.md │ │ ├── node │ │ │ ├── activate-code-version-advanced.full-mode.programmatic.test.js │ │ │ ├── code-versions.full-mode.programmatic.test.js │ │ │ ├── generate-cartridge-structure.docs-only.programmatic.test.js │ │ │ ├── get-available-best-practice-guides.docs-only.programmatic.test.js │ │ │ ├── get-available-sfra-documents.programmatic.test.js │ │ │ ├── get-best-practice-guide.docs-only.programmatic.test.js │ │ │ ├── get-hook-reference.docs-only.programmatic.test.js │ │ │ ├── get-job-execution-summary.full-mode.programmatic.test.js │ │ │ ├── get-job-log-entries.full-mode.programmatic.test.js │ │ │ ├── get-latest-debug.full-mode.programmatic.test.js │ │ │ ├── get-latest-error.full-mode.programmatic.test.js │ │ │ ├── get-latest-info.full-mode.programmatic.test.js │ │ │ ├── get-latest-job-log-files.full-mode.programmatic.test.js │ │ │ ├── get-latest-warn.full-mode.programmatic.test.js │ │ │ ├── get-log-file-contents.full-mode.programmatic.test.js │ │ │ ├── get-sfcc-class-documentation.docs-only.programmatic.test.js │ │ │ ├── get-sfcc-class-info.docs-only.programmatic.test.js │ │ │ ├── get-sfra-categories.docs-only.programmatic.test.js │ │ │ ├── get-sfra-document.programmatic.test.js │ │ │ ├── get-sfra-documents-by-category.docs-only.programmatic.test.js │ │ │ ├── get-system-object-definition.full-mode.programmatic.test.js │ │ │ ├── get-system-object-definitions.docs-only.programmatic.test.js │ │ │ ├── get-system-object-definitions.full-mode.programmatic.test.js │ │ │ ├── list-log-files.full-mode.programmatic.test.js │ │ │ ├── list-sfcc-classes.docs-only.programmatic.test.js │ │ │ ├── search-best-practices.docs-only.programmatic.test.js │ │ │ ├── search-custom-object-attribute-definitions.full-mode.programmatic.test.js │ │ │ ├── search-job-logs-by-name.full-mode.programmatic.test.js │ │ │ ├── search-job-logs.full-mode.programmatic.test.js │ │ │ ├── search-logs.full-mode.programmatic.test.js │ │ │ ├── search-sfcc-classes.docs-only.programmatic.test.js │ │ │ ├── search-sfcc-methods.docs-only.programmatic.test.js │ │ │ ├── search-sfra-documentation.docs-only.programmatic.test.js │ │ │ ├── search-site-preferences.full-mode.programmatic.test.js │ │ │ ├── search-system-object-attribute-definitions.full-mode.programmatic.test.js │ │ │ ├── search-system-object-attribute-groups.full-mode.programmatic.test.js │ │ │ ├── summarize-logs.full-mode.programmatic.test.js │ │ │ ├── tools.docs-only.programmatic.test.js │ │ │ └── tools.full-mode.programmatic.test.js │ │ ├── README.md │ │ ├── test-fixtures │ │ │ └── dw.json │ │ └── yaml │ │ ├── activate-code-version.docs-only.test.mcp.yml │ │ ├── activate-code-version.full-mode.test.mcp.yml │ │ ├── get_latest_error.test.mcp.yml │ │ ├── get-available-best-practice-guides.docs-only.test.mcp.yml │ │ ├── get-available-best-practice-guides.full-mode.test.mcp.yml │ │ ├── get-available-sfra-documents.docs-only.test.mcp.yml │ │ ├── get-available-sfra-documents.full-mode.test.mcp.yml │ │ ├── get-best-practice-guide.docs-only.test.mcp.yml │ │ ├── get-best-practice-guide.full-mode.test.mcp.yml │ │ ├── get-code-versions.docs-only.test.mcp.yml │ │ ├── get-code-versions.full-mode.test.mcp.yml │ │ ├── get-hook-reference.docs-only.test.mcp.yml │ │ ├── get-hook-reference.full-mode.test.mcp.yml │ │ ├── get-job-execution-summary.full-mode.test.mcp.yml │ │ ├── get-job-log-entries.full-mode.test.mcp.yml │ │ ├── get-latest-debug.full-mode.test.mcp.yml │ │ ├── get-latest-error.full-mode.test.mcp.yml │ │ ├── get-latest-info.full-mode.test.mcp.yml │ │ ├── get-latest-job-log-files.full-mode.test.mcp.yml │ │ ├── get-latest-warn.full-mode.test.mcp.yml │ │ ├── get-log-file-contents.full-mode.test.mcp.yml │ │ ├── get-sfcc-class-documentation.docs-only.test.mcp.yml │ │ ├── get-sfcc-class-documentation.full-mode.test.mcp.yml │ │ ├── get-sfcc-class-info.docs-only.test.mcp.yml │ │ ├── get-sfcc-class-info.full-mode.test.mcp.yml │ │ ├── get-sfra-categories.docs-only.test.mcp.yml │ │ ├── get-sfra-categories.full-mode.test.mcp.yml │ │ ├── get-sfra-document.docs-only.test.mcp.yml │ │ ├── get-sfra-document.full-mode.test.mcp.yml │ │ ├── get-sfra-documents-by-category.docs-only.test.mcp.yml │ │ ├── get-sfra-documents-by-category.full-mode.test.mcp.yml │ │ ├── get-system-object-definition.docs-only.test.mcp.yml │ │ ├── get-system-object-definition.full-mode.test.mcp.yml │ │ ├── get-system-object-definitions.docs-only.test.mcp.yml │ │ ├── get-system-object-definitions.full-mode.test.mcp.yml │ │ ├── list-log-files.full-mode.test.mcp.yml │ │ ├── list-sfcc-classes.docs-only.test.mcp.yml │ │ ├── list-sfcc-classes.full-mode.test.mcp.yml │ │ ├── search-best-practices.docs-only.test.mcp.yml │ │ ├── search-best-practices.full-mode.test.mcp.yml │ │ ├── search-custom-object-attribute-definitions.docs-only.test.mcp.yml │ │ ├── search-custom-object-attribute-definitions.test.mcp.yml │ │ ├── search-job-logs-by-name.full-mode.test.mcp.yml │ │ ├── search-job-logs.full-mode.test.mcp.yml │ │ ├── search-logs.full-mode.test.mcp.yml │ │ ├── search-sfcc-classes.docs-only.test.mcp.yml │ │ ├── search-sfcc-classes.full-mode.test.mcp.yml │ │ ├── search-sfcc-methods.docs-only.test.mcp.yml │ │ ├── search-sfcc-methods.full-mode.test.mcp.yml │ │ ├── search-sfra-documentation.docs-only.test.mcp.yml │ │ ├── search-sfra-documentation.full-mode.test.mcp.yml │ │ ├── search-site-preferences.docs-only.test.mcp.yml │ │ ├── search-site-preferences.full-mode.test.mcp.yml │ │ ├── search-system-object-attribute-definitions.docs-only.test.mcp.yml │ │ ├── search-system-object-attribute-definitions.full-mode.test.mcp.yml │ │ ├── search-system-object-attribute-groups.docs-only.test.mcp.yml │ │ ├── search-system-object-attribute-groups.full-mode.test.mcp.yml │ │ ├── summarize-logs.full-mode.test.mcp.yml │ │ ├── tools.docs-only.test.mcp.yml │ │ └── tools.full-mode.test.mcp.yml │ ├── oauth-token.test.ts │ ├── ocapi-auth-client.test.ts │ ├── ocapi-client.test.ts │ ├── path-service.test.ts │ ├── query-builder.test.ts │ ├── referenced-types-extractor.test.ts │ ├── servers │ │ ├── sfcc-mock-server │ │ │ ├── mock-data │ │ │ │ └── ocapi │ │ │ │ ├── code-versions.json │ │ │ │ ├── custom-object-attributes-customapi.json │ │ │ │ ├── custom-object-attributes-globalsettings.json │ │ │ │ ├── custom-object-attributes-versionhistory.json │ │ │ │ ├── site-preferences-ccv.json │ │ │ │ ├── site-preferences-fastforward.json │ │ │ │ ├── site-preferences-sfra.json │ │ │ │ ├── site-preferences-storefront.json │ │ │ │ ├── site-preferences-system.json │ │ │ │ ├── system-object-attribute-groups-campaign.json │ │ │ │ ├── system-object-attribute-groups-category.json │ │ │ │ ├── system-object-attribute-groups-order.json │ │ │ │ ├── system-object-attribute-groups-product.json │ │ │ │ ├── system-object-attribute-groups-sitepreferences.json │ │ │ │ ├── system-object-attributes-customeraddress.json │ │ │ │ ├── system-object-attributes-product-expanded.json │ │ │ │ ├── system-object-attributes-product.json │ │ │ │ ├── system-object-definition-category.json │ │ │ │ ├── system-object-definition-customer.json │ │ │ │ ├── system-object-definition-customeraddress.json │ │ │ │ ├── system-object-definition-order.json │ │ │ │ ├── system-object-definition-product.json │ │ │ │ ├── system-object-definitions-old.json │ │ │ │ └── system-object-definitions.json │ │ │ ├── package-lock.json │ │ │ ├── package.json │ │ │ ├── README.md │ │ │ ├── scripts │ │ │ │ └── setup-logs.js │ │ │ ├── server.js │ │ │ └── src │ │ │ ├── app.js │ │ │ ├── config │ │ │ │ └── server-config.js │ │ │ ├── middleware │ │ │ │ ├── auth.js │ │ │ │ ├── cors.js │ │ │ │ └── logging.js │ │ │ ├── routes │ │ │ │ ├── ocapi │ │ │ │ │ ├── code-versions-handler.js │ │ │ │ │ ├── oauth-handler.js │ │ │ │ │ ├── ocapi-error-utils.js │ │ │ │ │ ├── ocapi-utils.js │ │ │ │ │ ├── site-preferences-handler.js │ │ │ │ │ └── system-objects-handler.js │ │ │ │ ├── ocapi.js │ │ │ │ └── webdav.js │ │ │ └── utils │ │ │ ├── mock-data-loader.js │ │ │ └── webdav-xml.js │ │ └── sfcc-mock-server-manager.ts │ ├── sfcc-mock-server.test.ts │ ├── site-preferences-client.test.ts │ ├── system-objects-client.test.ts │ ├── utils.test.ts │ ├── validation-helpers.test.ts │ └── validator.test.ts ├── tsconfig.json └── tsconfig.test.json ``` # Files -------------------------------------------------------------------------------- /docs-site/pages/ExamplesPage.tsx: -------------------------------------------------------------------------------- ```typescript import React from 'react'; import { NavLink } from 'react-router-dom'; import SEO from '../components/SEO'; import BreadcrumbSchema from '../components/BreadcrumbSchema'; import StructuredData from '../components/StructuredData'; import CodeBlock, { InlineCode } from '../components/CodeBlock'; import { H1, PageSubtitle } from '../components/Typography'; import { SITE_DATES } from '../constants'; const ModeBadge: React.FC<{ children: React.ReactNode; variant?: 'docs' | 'full' | 'mixed' }> = ({ children, variant = 'docs' }) => { const styles: Record<string, string> = { docs: 'bg-green-100 text-green-700 border-green-300', full: 'bg-blue-100 text-blue-700 border-blue-300', mixed: 'bg-purple-100 text-purple-700 border-purple-300' }; return <span className={`inline-flex items-center gap-1 text-xs font-medium px-2 py-1 rounded-md border ${styles[variant]}`}>{children}</span>; }; const SectionCard: React.FC<{ children: React.ReactNode; gradient?: string; id?: string; title?: string; icon?: string; subtitle?: string }> = ({ children, gradient = 'from-slate-50 via-white to-slate-50', id, title, icon, subtitle }) => ( <section id={id} className="relative mb-14 scroll-mt-20"> <div className={`absolute inset-0 bg-gradient-to-br ${gradient} rounded-2xl`} /> <div className="relative rounded-2xl border border-slate-200/70 bg-white/70 backdrop-blur-sm p-6 md:p-8 shadow-sm"> {title && ( <div className="mb-5"> <h3 className="text-2xl md:text-3xl font-bold tracking-tight text-slate-900 flex items-start gap-3"> <span className="text-xl md:text-2xl" aria-hidden="true">{icon}</span> <span>{title}</span> </h3> {subtitle && <p className="mt-2 text-slate-600 text-sm md:text-base leading-relaxed max-w-3xl">{subtitle}</p>} </div> )} {children} </div> </section> ); const PromptBlock: React.FC<{ prompt: string; intent?: string; }> = ({ prompt, intent }) => ( <div className="rounded-lg border border-slate-200 bg-slate-50 p-4 mb-5"> <p className="text-slate-700 text-sm leading-relaxed"> <span className="font-semibold text-slate-900">Prompt:</span> "{prompt}"{intent && <span className="block mt-1 text-xs text-slate-500">Intent: {intent}</span>} </p> </div> ); const StepsList: React.FC<{ steps: Array<{ label: string; tool?: string; mode?: 'docs' | 'full'; note?: string; }> }> = ({ steps }) => ( <ol className="list-decimal pl-6 space-y-2 text-sm md:text-base"> {steps.map((s, i) => ( <li key={i} className="leading-relaxed"> <span className="font-medium text-slate-800">{s.label}</span> {s.tool && ( <span className="block mt-1 font-mono text-xs md:text-[13px] bg-slate-900 text-slate-100 rounded px-2 py-1 w-fit">{s.tool}</span> )} {s.note && <div className="mt-1 text-xs text-slate-500">{s.note}</div>} {s.mode && <div className="mt-1"><ModeBadge variant={s.mode === 'docs' ? 'docs' : 'full'}>{s.mode === 'docs' ? 'Docs Mode' : 'Full Mode'}</ModeBadge></div>} </li> ))} </ol> ); const ExamplesPage: React.FC = () => { const examplesStructuredData = { "@context": "https://schema.org", "@type": "TechArticle", "headline": "Examples & Workflows - SFCC Development MCP Server", "description": "Prompt-first, end-to-end examples showing how to leverage the SFCC Development MCP Server with AI assistants for real development tasks.", "author": { "@type": "Person", "name": "Thomas Theunen" }, "publisher": { "@type": "Person", "name": "Thomas Theunen" }, "datePublished": SITE_DATES.PUBLISHED, "dateModified": SITE_DATES.MODIFIED, "url": "https://sfcc-mcp-dev.rhino-inquisitor.com/examples/", "educationalLevel": "intermediate", "learningResourceType": "tutorial" }; return ( <div className="max-w-6xl mx-auto px-6 py-8"> <SEO title="Examples & Workflows" description="Prompt-first, end-to-end examples showing how to leverage the SFCC Development MCP Server with AI assistants for real development tasks. Focus on actionable, production-grade outputs." keywords="SFCC examples, Commerce Cloud prompts, MCP workflows, SFCC AI prompts, cartridge generation, log analysis examples" canonical="/examples/" ogType="article" /> <BreadcrumbSchema items={[ { name: "Home", url: "/" }, { name: "Examples", url: "/examples/" } ]} /> <StructuredData data={examplesStructuredData} /> <div className="text-center mb-14"> <H1 id="examples">💡 Prompt-First Examples</H1> <PageSubtitle> Copy a prompt. Paste it into your AI assistant. Get a production-quality answer. Each section shows the exact request, the tool usage behind the scenes, and the kind of response you should expect. </PageSubtitle> <p className="text-sm text-slate-500 max-w-2xl mx-auto">No repetition of feature marketing here—only concrete, minimal, high-signal examples.</p> <p className="mt-4 text-[11px] uppercase tracking-wide text-slate-400">Surface: <strong>36+ specialized tools</strong> (docs, best practices, SFRA, cartridge gen, runtime logs, job logs, system & custom objects, site preferences, code versions)</p> </div> <SectionCard id="explore-class" title="Explore an SFCC Class Quickly" icon="📚" gradient="from-green-50 via-white to-emerald-50" subtitle="Use documentation tools to pull authoritative class context, then build with confidence." > <PromptBlock prompt="Show me everything important about dw.catalog.Product, including methods I actually use in pricing or variation contexts." intent="Documentation deep dive → actionable subset" /> <div className="mb-4 flex flex-wrap gap-2"> <ModeBadge>Docs Mode</ModeBadge> </div> <StepsList steps={[ { label: 'Search for candidate classes (confirmation)', tool: 'search_sfcc_classes {"query": "product"}', mode: 'docs' }, { label: 'Fetch class details', tool: 'get_sfcc_class_info {"className": "dw.catalog.Product"}', mode: 'docs' }, { label: 'Cross reference pricing model', tool: 'get_sfcc_class_info {"className": "dw.catalog.PriceModel"}', mode: 'docs', note: 'Inspect pricing methods' } ]} /> <div className="mt-6"> <p className="text-sm font-semibold text-slate-700 mb-2">Assistant Consolidation (grounded in tool output):</p> <CodeBlock language="markdown" code={`### Focused Summary: dw.catalog.Product Pulled full class definition (get_sfcc_class_info → dw.catalog.Product) and reduced to what typically drives storefront pricing / availability / variation logic. Identity & Routing: • ID, name, pageURL for canonical + SEO. • onlineFlag vs online (flag is raw, online applies scheduling window). Pricing: • priceModel.getPrice().value / currencyCode for base price display. • Use getPriceModel(optionModel) when option selections adjust pricing. • Guard: priceModel can be null on offline or misconfigured variants. Availability: • availabilityModel.isInStock() and availabilityModel.getATS()?.value for inventory messaging. • Avoid deprecated isAvailable()/getAvailableFlag(); rely on availabilityModel. Variation Handling: • isMaster() / isVariant() to branch logic. • variationModel.getVariants() (iterate then filter by onlineFlag). • Image fallback: getImage('small', 0) (uses variant images, then master fallback); avoid deprecated no-arg getImage(). Navigation / Category Context: • onlineCategories preferred for building live nav trees; allCategories includes offline / non-site assignments. Common Pitfalls & Mitigations: 1. Null priceModel → defensive optional chaining. 2. Using deprecated availability helpers → switch to availabilityModel. 3. Assuming images exist → null check getImage before URL access. 4. Iterating variants without online gating → filter by v.onlineFlag. Minimal Access Snippet: \`var pm = p.getPriceModel();\nvar price = pm ? pm.getPrice().value : null;\nvar inStock = p.getAvailabilityModel().isInStock();\nif (p.isMaster()) {\n var variants = p.getVariationModel().getVariants().toArray().filter(v => v.onlineFlag);\n}\` `} /> </div> </SectionCard> <SectionCard id="focused-class-exploration" title="Focused Class Exploration with Filtering" icon="🔍" gradient="from-emerald-50 via-white to-teal-50" subtitle="Use enhanced filtering and search to find exactly what you need from SFCC classes." > <PromptBlock prompt="I need to understand only the getter methods in dw.catalog.Product that relate to pricing or availability. Don't show me everything." intent="Targeted exploration with precise filtering" /> <div className="mb-4 flex flex-wrap gap-2"> <ModeBadge>Docs Mode</ModeBadge> </div> <StepsList steps={[ { label: 'Show only methods with price-related terms', tool: 'get_sfcc_class_info {"className": "dw.catalog.Product", "includeProperties": false, "includeConstants": false, "includeDescription": false, "includeInheritance": false, "search": "price"}', mode: 'docs' }, { label: 'Show only methods with availability terms', tool: 'get_sfcc_class_info {"className": "dw.catalog.Product", "includeProperties": false, "includeConstants": false, "includeDescription": false, "includeInheritance": false, "search": "availab"}', mode: 'docs' }, { label: 'Compare with full getter methods', tool: 'get_sfcc_class_info {"className": "dw.catalog.Product", "includeProperties": false, "includeConstants": false, "includeDescription": false, "includeInheritance": false, "search": "get"}', mode: 'docs', note: 'See complete getter landscape' } ]} /> <div className="mt-6"> <p className="text-sm font-semibold text-slate-700 mb-2">Focused Results (price-related methods only):</p> <CodeBlock language="markdown" code={`### dw.catalog.Product - Price Methods Only **Methods:** • getPriceModel() : PriceModel - Returns pricing model for current product • getPriceModel(ProductOptionModel) : PriceModel - Returns pricing with option adjustments **Key Benefits:** - Clean, targeted view without noise from 50+ other methods - Focus on exactly what's needed for pricing logic - Search functionality quickly surfaces relevant APIs`} /> <div className="mt-4 p-3 bg-blue-50 rounded-lg"> <p className="text-sm text-blue-800"><strong>Pro Tip:</strong> Use <code>search</code> parameter with partial terms like "get", "set", "price", "availab" to quickly find relevant methods. Combine with <code>includeProperties: false</code> to focus only on methods.</p> </div> </div> </SectionCard> <SectionCard id="controller-from-docs" title="Generate a Controller from Documentation Context" icon="🛠️" gradient="from-blue-50 via-white to-indigo-50" subtitle="Documentation → intent → runnable controller with guard rails." > <PromptBlock prompt="Using SFRA patterns, create a Product-Show like controller action that handles missing products, logs errors, and renders a view model with price + availability." intent="Transform docs into production-safe code" /> <ModeBadge>Docs Mode</ModeBadge> <div className="mt-4 text-sm text-slate-600">Assistant internally referenced previously fetched class info; no additional tool calls required now.</div> <div className="mt-6"> <CodeBlock language="javascript" code={`// app_storefront_base/cartridge/controllers/CustomProduct.js 'use strict'; var server = require('server'); var ProductMgr = require('dw/catalog/ProductMgr'); var Logger = require('dw/system/Logger'); var cache = require('*/cartridge/scripts/middleware/cache'); server.get('Show', cache.applyDefaultCache, function (req, res, next) { var pid = req.querystring.pid; if (!pid) { res.setStatusCode(400); res.render('error/badrequest'); return next(); } try { var product = ProductMgr.getProduct(pid); if (!product || !product.onlineFlag) { res.setStatusCode(404); res.render('error/notfound'); return next(); } var availability = product.getAvailabilityModel(); var priceModel = product.getPriceModel(); var viewModel = { id: product.ID, name: product.name, price: priceModel ? priceModel.getPrice().value : null, currency: priceModel ? priceModel.getPrice().currencyCode : null, inStock: availability.isInStock(), ats: availability.getATS && availability.getATS().value, images: product.getImages('small', 0).toArray().map(function(i){return i.URL;}) }; res.render('product/customProduct', viewModel); } catch (e) { Logger.error('CustomProduct.Show error: {0}', e.message); res.setStatusCode(500); res.render('error/general'); } return next(); }); module.exports = server.exports();`} /> </div> </SectionCard> <SectionCard id="cartridge-generation" title="Scaffold a Feature Cartridge" icon="📦" gradient="from-purple-50 via-white to-fuchsia-50" subtitle="Go from blank folder to structured integration base." > <PromptBlock prompt="Generate a cartridge structure for a shipment tracking integration using hooks + a service layer and note follow-up tasks." intent="Structural generation + next actions" /> <div className="mb-4 flex flex-wrap gap-2"><ModeBadge>Docs Mode</ModeBadge></div> <StepsList steps={[ { label: 'Generate skeleton', tool: 'generate_cartridge_structure {"cartridgeName": "int_tracking"}', mode: 'docs' }, { label: 'Request best practice follow-up', tool: 'get_best_practice_guide {"guideName": "cartridge_creation"}', mode: 'docs', note: 'Assistant merges guidance' } ]} /> <div className="mt-6 space-y-8"> <div> <p className="text-sm font-semibold text-slate-700 mb-2">Generated Tree (excerpt):</p> <CodeBlock language="text" code={`int_tracking/ └── cartridge/ ├── controllers/ │ └── Tracking.js ├── scripts/ │ ├── hooks/ │ │ └── shipment.js │ └── services/ │ └── TrackingService.js ├── templates/default/tracking/ │ └── trackingstatus.isml └── static/default/js/tracking/`} /> </div> <div> <p className="text-sm font-semibold text-slate-700 mb-2">Assistant Adds Next Steps:</p> <CodeBlock language="markdown" code={`1. Configure LocalServiceRegistry service: tracking.api 2. Implement hooks: updateShippingInfo, exportStatus 3. Add ISML partial to PDP for shipment ETA 4. Add logging category: TrackingIntegration 5. Write integration test harness for mocked API responses `} /> </div> </div> </SectionCard> <SectionCard id="full-mode-diff" title="Enhance with Live Logs (Full Mode)" icon="🧪" gradient="from-blue-50 via-white to-sky-50" subtitle="Same style of prompt—but now the assistant executes log tooling to ground its answer." > <PromptBlock prompt="Checkout is intermittently failing with payment declines—identify root causes from recent logs and propose concrete mitigations." intent="Diagnostic + remediation planning" /> <div className="flex flex-wrap gap-2 mb-4"><ModeBadge variant="full">Full Mode</ModeBadge></div> <StepsList steps={[ { label: 'Daily context snapshot', tool: 'summarize_logs {"date": "20250916"}', mode: 'full', note: 'High-level counts first' }, { label: 'Attempt error retrieval', tool: 'get_latest_error {"limit": 15}', mode: 'full', note: 'May return none → fall back to warnings' }, { label: 'Focus on recurring warning pattern', tool: 'search_logs {"pattern": "cookie_hint", "logLevel": "warn", "limit": 5}', mode: 'full', note: 'Isolate noisy asset issue' }, { label: 'Cross-check security guidelines (token lifecycle / logging hygiene)', tool: 'get_best_practice_guide {"guideName": "security"}', mode: 'docs' } ]} /> <div className="mt-6"> <p className="text-sm font-semibold text-slate-700 mb-2">Assistant Narrative (validated with real tool set):</p> <CodeBlock language="markdown" code={`### Live Log Triage (No Errors Detected Today) 1. summarize_logs → Errors: 0, Warnings: 27, no debug/info noise. Healthy baseline but warning cluster present. 2. get_latest_error → Returned none (expected—error file absent). Pivoting to warnings. 3. search_logs (pattern=cookie_hint, level=warn) → Multiple repeats: "Content asset with ID cookie_hint is offline" across distinct request contexts. Assessment: • Repeated offline content asset warnings inflate noise floor; risk of masking future meaningful warnings. • No payment/auth related issues surfaced; current checkout remediation unnecessary. Recommended Actions: • Restore or intentionally remove the cookie_hint asset; if intentionally removed, suppress via conditional include. • Add lightweight monitoring rule: if warning count for a single asset > X per hour, raise housekeeping task instead of polluting logs. • Keep error path watch in place; re-run summarize_logs after remediation to confirm warning reduction. If Errors Had Been Present (Pattern Skeleton): • Step 1 would still be summarize_logs for counts → then targeted search_logs for signatures (e.g., AUTH_TIMEOUT, TOKEN_EXPIRED) followed by best practice cross-reference. `} /> </div> </SectionCard> <SectionCard id="job-log-analysis" title="Investigate a Failing Job" icon="🧵" gradient="from-amber-50 via-white to-yellow-50" subtitle="Targeted job log inspection with focused summarization." > <PromptBlock prompt="Analyze the latest CatalogFeed job execution – summarize step durations, failures, and recommend optimizations." intent="Temporal + failure analysis" /> <div className="flex flex-wrap gap-2 mb-4"><ModeBadge variant="full">Full Mode</ModeBadge></div> <StepsList steps={[ { label: 'Discover job logs (filtered)', tool: 'search_job_logs_by_name {"jobName": "CatalogFeed", "limit": 5 }', mode: 'full', note: 'Find specific job log filenames' }, { label: 'List latest job logs (context)', tool: 'get_latest_job_log_files {"limit": 5 }', mode: 'full', note: 'Recency overview (no name filter)' }, { label: 'Fetch recent entries', tool: 'get_job_log_entries {"jobName": "CatalogFeed", "limit": 50, "level": "all" }', mode: 'full', note: 'Multi-level snapshot' }, { label: 'Search for errors', tool: 'search_job_logs {"pattern": "ERROR", "jobName": "CatalogFeed", "limit": 20 }', mode: 'full', note: 'If none → treat as healthy case' }, { label: 'Execution summary', tool: 'get_job_execution_summary {"jobName": "CatalogFeed"}', mode: 'full' } ]} /> <div className="mt-6"> <CodeBlock language="markdown" code={`### CatalogFeed Execution Analysis (Validated Flow) Flow Used: 1. search_job_logs_by_name → confirm job exists. 2. get_latest_job_log_files → establish relative recency. 3. get_job_log_entries (limit 50, all levels) → capture operational window. 4. search_job_logs (pattern=ERROR) → branch: errors vs healthy. 5. get_job_execution_summary → aggregate timing + status. Illustrative Degraded Scenario (for instructional value): Duration: 7m42s (↑18% vs rolling average) Step Highlights: • LOAD_PRODUCTS: 2m10s (warning — 312 fallback fetches suggests cache miss storm) • EXPORT: 15 socket timeout retries before success (dominant latency source) Primary Bottleneck: External export endpoint + un-jittered linear retry compounding latency. Recommendations (priority order): 1. Reduce batch size 200→120 to shrink payload variance. 2. Exponential backoff + jitter (≤800ms, cap 6) for EXPORT retries. 3. Parallelize transform stage (2 workers) given idle CPU. 4. Emit structured metrics: step_duration_ms, retry_count to confirm remediation. Healthy Case Note: If error search returns 0 (common for maintenance/cleanup jobs) emit concise summary only and skip remediation suggestions. `} /> </div> </SectionCard> <SectionCard id="system-object-attributes" title="Surface High-Value Custom Attributes" icon="🧬" gradient="from-rose-50 via-white to-pink-50" subtitle="Focus on signal: only the attributes you will likely act on in code." > <PromptBlock prompt="List only non-system custom Product attributes that impact pricing, display, or integrations—include access examples." intent="Selective attribute curation" /> <div className="flex flex-wrap gap-2 mb-4"><ModeBadge variant="full">Full Mode</ModeBadge></div> <StepsList steps={[ { label: 'Search attribute definitions', tool: 'search_system_object_attribute_definitions {"objectType": "Product", "searchRequest": {"query": {"match_all_query": {}}}}', mode: 'full' }, { label: 'Filter & rank', note: 'Assistant filters system vs custom, groups by usage relevance' }, ]} /> <div className="mt-6"> <CodeBlock language="markdown" code={`### Curated Product Attribute Focus (Grounded in Live Metadata) Source: search_system_object_attribute_definitions → 113 total attributes (first 30 sampled). We extracted *custom* (system=false) fields + a few pivotal system ones (clearly flagged) that materially influence merchandising, variation, UX, integration, or feed logic. Purely operational / audit (UUID, creationDate) or low-impact marketing fluff excluded. Legend: • Cat: Category of impact (Disp = Display/UX, Var = Variation, Facet = Faceting/Search, Int = Integration/Channel, Enr = Enrichment, Dim = Dimensions/Shipping) • MV = multi-value (enum_of_string or multi_value_type true) • SYS = system attribute retained for high value (otherwise we focus on custom) | Attribute | Cat | Type | Flags | Rationale | Access Snippet | |-----------|-----|------|-------|-----------|----------------| | brand (SYS) | Facet/Disp | string | visible | Drives brand badges & facet grouping; high-frequency filter. | \`p.brand\` (system field, not in p.custom) | | color | Var | string | custom | Variation presentation / swatch resolution; stored custom but effectively a core merchandising dimension. | \`p.custom.color\` | | availableForInStorePickup | Int | boolean | custom, site-agnostic | Gating for store pickup workflows & conditional UI messaging. | \`if (p.custom.availableForInStorePickup) showPickup();\` | | batteryLife | Enr/Disp | string | custom, visible | PDP spec table + comparison view; enriches SEO content. | \`p.custom.batteryLife\` | | batteryType | Enr/Disp | string | custom, visible | Hardware spec grouping; feed enrichment for certain channels. | \`p.custom.batteryType\` | | bootType | Facet | enum_of_string | custom, MV | Multi-select refinement facet; influences search narrowing logic. | \`p.custom.bootType && p.custom.bootType.toArray()\` | | bottomType | Facet | enum_of_string | custom, MV | Apparel categorization & layered navigation. | \`(p.custom.bottomType||[]).toArray()\` | | consoleWarranty | Int/Disp | string | custom | Extended service offering display + export to warranty provider feed. | \`p.custom.consoleWarranty\` | | digitalCameraFeatures | Facet/Enr | enum_of_string | custom, MV, visible | Feature badges + facet refinement; high cardinality set. | \`p.custom.digitalCameraFeatures?.toArray()\` | | digitalCameraPixels | Disp/Enr | string | custom, visible | Marketing resolution highlight; conditional comparison logic. | \`p.custom.digitalCameraPixels\` | | digitalCameraType | Facet | string | custom, visible | Primary camera classification facet. | \`p.custom.digitalCameraType\` | | digitalCameraWarranty | Int | string | custom | Feed & PDP legal/service disclosure. | \`p.custom.digitalCameraWarranty\` | | dimDepth | Dim | string | custom, visible | Shipping dimensional weight calc if external DIM service absent. | \`Number(p.custom.dimDepth)\` | | dimHeight | Dim | string | custom, visible | Parcel sizing / volumetric charge triggers. | \`Number(p.custom.dimHeight)\` | | dimWeight | Dim | string | custom, visible | Fallback for missing system weight or override scenario. | \`Number(p.custom.dimWeight)\` | | dimWidth | Dim | string | custom, visible | Combined with others for cubic volume. | \`Number(p.custom.dimWidth)\` | | displaySize | Disp | string | custom, visible | Prominence in electronics PDP hero section + compare grid. | \`p.custom.displaySize\` | | facebookEnabled (SYS) | Int | boolean | system, site_specific | Channel feed toggle – dictates inclusion in FB catalog export. | \`p.facebookEnabled\` | | gameGenre | Facet | enum_of_string | custom, MV, visible | Multi-select discovery facet for gaming taxonomy. | \`p.custom.gameGenre?.toArray()\` | | gameRating | Facet/Compliance | string | custom, visible | Age rating badge + compliance gating (e.g., age verification). | \`p.custom.gameRating\` | | gameSystemType | Facet | string | custom, visible | Platform segmentation (PS/Xbox/Nintendo) – major navigation axis. | \`p.custom.gameSystemType\` | | gpsFeatures | Facet/Enr | enum_of_string | custom, MV, visible | Feature-level refinement & badge cluster. | \`p.custom.gpsFeatures?.toArray()\` | | Wool | Enr/Disp | string | custom, localizable, visible | Care instruction overlay & localized PDP sustainability note. | \`p.custom.Wool\` | Why exclude others like EAN / UPC (system, typically handled upstream) or UUID (audit only)? They rarely drive conditional storefront logic once imported and are better treated as pass-through feed fields. Implementation Notes: 1. Multi-value handling: Always null-check before toArray(); some may be undefined on non-merchandised variants. 2. Numeric coercion: DIM fields are strings – coerce with Number() and guard NaN before calculations. 3. Site specificity: facebookEnabled is site_specific – avoid caching decisions across sites without keying by site ID. 4. Variation interplay: color selection should reference variationAttribute values; treat p.custom.color as display fallback not authoritative source. 5. Performance: When projecting many attributes into a model, prefer a single transform object rather than repeated p.custom dereferencing inside loops. Minimal Extraction Snippet: \`var c = p.custom;\nvar model = {\n brand: p.brand,\n color: c.color,\n pickupEligible: !!c.availableForInStorePickup,\n dims: ['dimHeight','dimWidth','dimDepth','dimWeight'].reduce(function(acc,k){var v=c[k]; if(v && !isNaN(v)) acc[k.replace('dim','').toLowerCase()]=Number(v); return acc;}, {}),\n features: (c.digitalCameraFeatures && c.digitalCameraFeatures.toArray()) || [],\n care: c.Wool,\n fbActive: p.facebookEnabled\n};\nif (c.bootType) model.bootType = c.bootType.toArray();\nif (c.gameGenre) model.gameGenre = c.gameGenre.toArray();\` `} /> </div> </SectionCard> <SectionCard id="site-preference-search" title="Discover Site Preferences Safely" icon="⚙️" gradient="from-indigo-50 via-white to-purple-50" subtitle="Locate configuration switches before you hardcode assumptions." > <PromptBlock prompt="Find all site preferences in the checkout group related to tax or shipping so I can reference them defensively in code." intent="Configuration discovery" /> <div className="flex flex-wrap gap-2 mb-4"><ModeBadge variant="full">Full Mode</ModeBadge></div> <StepsList steps={[ { label: 'List groups (if unknown)', tool: 'search_system_object_attribute_groups {"objectType": "SitePreferences", "searchRequest": {"query": {"match_all_query": {}}}}', mode: 'full', note: 'Optional first pass – discover group IDs' }, { label: 'Search preferences in group', tool: 'search_site_preferences {"groupId": "checkout", "searchRequest": {"query": {"text_query": {"fields": ["id","display_name","description"], "search_phrase": "tax"}}, "count": 50}}', mode: 'full' }, { label: 'Broaden to shipping', tool: 'search_site_preferences {"groupId": "checkout", "searchRequest": {"query": {"text_query": {"fields": ["id","display_name","description"], "search_phrase": "ship"}}, "count": 50}}', mode: 'full' } ]} /> <div className="mt-6"> <CodeBlock language="markdown" code={`### Interpreting Results Each preference includes: id, display_name, value_type, (optionally) value definition. Security: Password-type preference values are masked by default. Usage Pattern: \`var prefs = Site.getCurrent().getPreferences().getCustom();\nvar enableAltTax = prefs.enableAlternativeTaxEngine;\nif (enableAltTax) { /* branch logic */ }\` Defensive Access Tips: 1. Guard optional boolean flags with !! to normalize (e.g., !!prefs.enableGiftWrap) 2. Never log preference values directly (especially STRING/LONG_TEXT types containing tokens) 3. Document critical preference dependencies in controller/module JSDoc `} /> </div> </SectionCard> <SectionCard id="custom-object-attributes" title="Target Custom Object Attributes" icon="🧱" gradient="from-teal-50 via-white to-cyan-50" subtitle="Search attributes precisely instead of browsing blindly." > <PromptBlock prompt="For custom object type Global_String, list only searchable non-system attributes and show how to query one in script." intent="Metadata → usage" /> <div className="flex flex-wrap gap-2 mb-4"><ModeBadge variant="full">Full Mode</ModeBadge></div> <StepsList steps={[ { label: 'Query searchable attributes', tool: 'search_custom_object_attribute_definitions {"objectType": "Global_String", "searchRequest": {"query": {"bool_query": {"must": [{"term_query": {"fields": ["searchable"], "operator": "is", "values": ["true"]}}]}}, "count": 100}}', mode: 'full' }, { label: '(Optional) broaden if empty', note: 'Fallback: use match_all_query then client-side filter' } ]} /> <div className="mt-6"> <CodeBlock language="markdown" code={`### Usage Extraction Attribute Sample: altValueMapping (type=string, searchable=true) Script Access: \`var CustomObjectMgr = require('dw/object/CustomObjectMgr');\nvar co = CustomObjectMgr.getCustomObject('Global_String', key);\nvar mapped = co && co.custom.altValueMapping;\` Guidelines: - Null-check custom object fetch before dereferencing .custom - Prefer specific term_query filtering serverside rather than large client-side scans - If attribute becomes critical path, add defensive logging (category=MetadataAccess, level=debug) selectively (avoid value leakage) `} /> </div> </SectionCard> <SectionCard id="micro-job-log-triage" title="Micro Job Log Triage" icon="🩺" gradient="from-red-50 via-white to-rose-50" subtitle="Fast 3-step health read of a job without noise." > <PromptBlock prompt="Give me a 3-step minimal health read for the nightly InventorySync job and only call tools you truly need." intent="Minimal observation pipeline" /> <div className="flex flex-wrap gap-2 mb-4"><ModeBadge variant="full">Full Mode</ModeBadge></div> <StepsList steps={[ { label: 'Confirm presence', tool: 'search_job_logs_by_name {"jobName": "InventorySync", "limit": 3 }', mode: 'full' }, { label: 'Get recent entries (tail)', tool: 'get_job_log_entries {"jobName": "InventorySync", "limit": 40 }', mode: 'full', note: 'Mixed levels in single job file' }, { label: 'Execution summary', tool: 'get_job_execution_summary {"jobName": "InventorySync"}', mode: 'full', note: 'Roll-up status + durations' } ]} /> <div className="mt-6"> <CodeBlock language="markdown" code={`### Minimal InventorySync Health Snapshot Outcome: SUCCESS (duration 2m14s, 0 errors, 3 warnings) Warnings Focus: • 2x RETRY: External API 429 backoffs (acceptable within thresholds) • 1x Deprecated attribute reference (non-blocking; schedule fix) Next Actions (Only If Persistent): 1. Track retry ratio vs baseline (add metric instrumentation) 2. Replace deprecated attribute before Q4 freeze `} /> </div> </SectionCard> {/* End-to-End section removed per request */} <SectionCard id="prompt-patterns" title="Prompt Patterns & Anti-Patterns" icon="🧭" gradient="from-slate-50 via-white to-slate-100" subtitle="Tight prompts yield grounded, production-usable outputs." > <div className="grid md:grid-cols-2 gap-8"> <div> <h4 className="font-semibold text-slate-800 mb-2">Effective Patterns</h4> <ul className="space-y-2 text-sm list-disc pl-5"> <li><strong>Role + Scope:</strong> "Act as SFRA dev – generate..."</li> <li><strong>Constraint:</strong> "Only include attributes affecting pricing."</li> <li><strong>Mode Awareness:</strong> "Use live logs to confirm before proposing fixes."</li> <li><strong>Transformation:</strong> "Summarize for junior dev handoff."</li> </ul> </div> <div> <h4 className="font-semibold text-slate-800 mb-2">Anti-Patterns</h4> <ul className="space-y-2 text-sm list-disc pl-5"> <li>"Explain everything about SFCC" (Too broad)</li> <li>"Fix checkout" (No signal / context)</li> <li>Omitting objective (no success definition)</li> <li>Forgetting mode capabilities (asks for logs in docs-only)</li> </ul> </div> </div> <div className="mt-6"> <p className="text-sm font-semibold text-slate-700 mb-2">Prompt Refinement Example:</p> <CodeBlock language="markdown" code={`Weak: "Help with product page" Improved: "Generate an SFRA controller extension to enrich Product-Show with cached ATS and badge if sustainabilityRating >=3. Provide only changed code + template snippet."`} /> </div> </SectionCard> <div className="text-center mt-20"> <p className="text-lg text-slate-700 mb-6 font-medium">Ready to try these yourself?</p> <div className="flex flex-col sm:flex-row gap-4 justify-center"> <NavLink to="/ai-interfaces/" className="group no-underline hover:no-underline bg-gradient-to-r from-blue-600 to-purple-600 text-white px-8 py-4 rounded-xl font-semibold text-lg shadow-lg hover:shadow-xl transition-all duration-300 transform hover:-translate-y-1"> Configure Your AI Client <span className="ml-2 group-hover:translate-x-1 inline-block transition-transform">→</span> </NavLink> <NavLink to="/tools/" className="no-underline hover:no-underline border-2 border-slate-300 text-slate-700 px-8 py-4 rounded-xl font-semibold text-lg hover:border-blue-500 hover:text-blue-600 transition-all duration-300"> Browse All Tools </NavLink> </div> </div> </div> ); }; export default ExamplesPage; ``` -------------------------------------------------------------------------------- /docs/best-practices/job_framework.md: -------------------------------------------------------------------------------- ```markdown # Salesforce B2C Commerce Job Framework: Best Practices & Development Guide This guide provides comprehensive best practices for developing custom jobs within the Salesforce B2C Commerce Cloud Job Framework. Master these principles to build robust, scalable, and high-performing batch processing solutions that can handle enterprise-scale data operations. **IMPORTANT**: Before developing custom jobs, consult the **Performance and Stability Best Practices** and **Security Best Practices** guides from this MCP server. Understanding performance optimization strategies, database-friendly APIs, and security guidelines is essential for building production-ready job solutions. ## Core Concepts ### The SFCC Job Framework Architecture The Job Framework is the cornerstone of back-end automation for any e-commerce implementation on SFCC. It provides a robust, scalable mechanism for executing asynchronous processes essential for site operations, data management, and system integration. **Key Components:** - **Jobs**: Highest-level containers that encapsulate complete business processes - **Flows**: Logical sequences of steps within a job (can be sequential or parallel) - **Steps**: Smallest units of execution representing discrete tasks ### Modern Job Development Paradigms SFCC offers two distinct development models for custom jobs: | Aspect | Task-Oriented ("Normal") | Chunk-Oriented | |--------|-------------------------|----------------| | **Best For** | Simple, monolithic tasks; quick operations | Large-scale data processing | | **Data Volume** | Low (prone to timeouts with large datasets) | High (designed for massive datasets) | | **Progress Monitoring** | Limited (running or finished) | Granular (updated per chunk) | | **Transaction Control** | Typically one transaction | Fine-grained per chunk | | **Code Complexity** | Low (single main function) | Moderate (callback functions) | | **Resumability** | Difficult (requires full restart) | Easier (failures isolated to chunks) | ## Task-Oriented Jobs: Best Practices ### When to Use Task-Oriented Jobs Choose task-oriented jobs for: - Single configuration file downloads - Single API calls to external services - Quick, targeted database updates - Tasks where calculating progress is impractical ### Essential Implementation Patterns #### 1. Proper Script Module Structure ```javascript 'use strict'; var Status = require('dw/system/Status'); var Transaction = require('dw/system/Transaction'); var Logger = require('dw/system/Logger'); /** * Main function for task-oriented job step * @param {Object} parameters - Parameters from Business Manager * @param {dw.job.JobStepExecution} stepExecution - Job context and logging * @returns {dw.system.Status} - Exit status */ exports.execute = function (parameters, stepExecution) { var logger = Logger.getLogger('jobs', 'TaskOrientedJob'); try { logger.info('Starting job with parameters: {0}', JSON.stringify(parameters)); // Your business logic here var result = performBusinessLogic(parameters, logger); if (result.success) { logger.info('Job completed successfully'); return new Status(Status.OK); } else { logger.warn('Job completed with warnings: {0}', result.message); return new Status(Status.OK, 'OK', result.message); } } catch (e) { logger.error('Job failed with error: {0}', e.message); return new Status(Status.ERROR, 'ERROR', e.message); } }; function performBusinessLogic(parameters, logger) { // Implementation here return { success: true, message: 'Operation completed' }; } ``` #### 2. Advanced Status Control for Flow Management ```javascript // Status codes must be either OK or ERROR only exports.execute = function (parameters, stepExecution) { var logger = Logger.getLogger('jobs', 'StatusControlJob'); try { var inputFile = new File(File.IMPEX + parameters.fileName); if (!inputFile.exists()) { logger.info('Input file not found: {0}', parameters.fileName); // Use OK status with descriptive message for conditional flow control return new Status(Status.OK, 'OK', 'Input file was not present - skipping processing'); } if (inputFile.length() === 0) { logger.info('Input file is empty: {0}', parameters.fileName); return new Status(Status.OK, 'OK', 'No data to process - file is empty'); } // Process file... return new Status(Status.OK, 'OK', 'File processed successfully'); } catch (e) { logger.error('Processing failed: {0}', e.message); return new Status(Status.ERROR, 'ERROR', e.message); } }; ``` #### 3. Proper Transaction Management ```javascript // RECOMMENDED: Use Transaction.wrap for automatic lifecycle management exports.execute = function (parameters, stepExecution) { var logger = Logger.getLogger('jobs', 'TransactionJob'); try { var category = CatalogMgr.getCategory(parameters.categoryID); var products = ProductMgr.queryProductsInCatalog(category.getCatalog()); var updateCount = 0; // Transaction.wrap handles begin/commit/rollback automatically Transaction.wrap(function () { while (products.hasNext()) { var product = products.next(); // Filter by category assignment and online status if (product.isOnline() && product.isAssignedToCategory(category)) { product.setOnlineFlag(false); updateCount++; } } }); products.close(); logger.info('Updated {0} products', updateCount); return new Status(Status.OK); } catch (e) { logger.error('Transaction failed: {0}', e.message); return new Status(Status.ERROR); } }; ``` ### Task-Oriented Job Configuration #### steptypes.json Structure ```json { "step-types": { "script-module-step": [ { "@type-id": "custom.DeactivateProducts", "@supports-site-context": true, "@supports-organization-context": false, "description": "Deactivates all online products in a specified category", "module": "plugin_examplecartridge/cartridge/scripts/jobs/deactivateProducts.js", "function": "execute", "parameters": [ { "@name": "categoryID", "@type": "string", "@required": true, "description": "ID of the category containing products to deactivate" } ], "status-codes": { "status": [ { "@code": "OK", "description": "Products deactivated successfully" }, { "@code": "ERROR", "description": "An error occurred during processing" } ] } } ] } } ``` ## Chunk-Oriented Jobs: Enterprise-Scale Processing ### The Chunking Philosophy Chunk-oriented jobs are SFCC's architectural solution for processing large datasets without hitting platform limits. They break massive tasks into manageable segments, providing: - **Stability**: Eliminates memory and timeout issues - **Performance**: Fine-tuned transaction management - **Monitoring**: Granular progress tracking - **Resilience**: Isolated failure handling ### Core Lifecycle Functions #### Required Functions (Data Pipeline) ```javascript 'use strict'; var File = require('dw/io/File'); var FileReader = require('dw/io/FileReader'); var CSVStreamReader = require('dw/io/CSVStreamReader'); var ProductMgr = require('dw/catalog/ProductMgr'); var Transaction = require('dw/system/Transaction'); var Logger = require('dw/system/Logger'); var csvReader; var totalLines = 0; var processedLines = 0; var logger = Logger.getLogger('jobs', 'BulkProductUpdate'); /** * Setup function - opens input file and prepares for processing */ exports.beforeStep = function (parameters, stepExecution) { try { var inputFile = new File(File.IMPEX + parameters.inputFileName); if (!inputFile.exists()) { throw new Error('Input file not found: ' + parameters.inputFileName); } var fileReader = new FileReader(inputFile); csvReader = new CSVStreamReader(fileReader); // Skip header row if present if (parameters.hasHeader === 'true') { csvReader.readNext(); } logger.info('Started processing file: {0}', parameters.inputFileName); } catch (e) { logger.error('Setup failed: {0}', e.message); throw e; } }; /** * Returns total count for progress monitoring (optional but recommended) */ exports.getTotalCount = function (parameters, stepExecution) { // Return estimated count for progress tracking // This could be based on file size or pre-calculated value return totalLines > 0 ? totalLines : null; }; /** * Reads next item from data source * @returns {Object|null} Next item or null when finished */ exports.read = function (parameters, stepExecution) { var line = csvReader.readNext(); if (line && line.length > 0) { return { productID: line[0], newOnlineStatus: line[1] === 'true', lineNumber: ++processedLines }; } return null; // Signals end of data }; /** * Processes a single item * @param {Object} item - Item from read() * @returns {Object|null} Processed item or null to filter out */ exports.process = function (item, parameters, stepExecution) { try { var product = ProductMgr.getProduct(item.productID); if (!product) { return { productID: item.productID, status: 'ERROR', message: 'Product not found', lineNumber: item.lineNumber }; } if (typeof item.newOnlineStatus !== 'boolean') { return { productID: item.productID, status: 'SKIPPED', message: 'Invalid online status value', lineNumber: item.lineNumber }; } return { product: product, productID: item.productID, newOnlineStatus: item.newOnlineStatus, status: 'READY', lineNumber: item.lineNumber }; } catch (e) { logger.warn('Error processing product {0}: {1}', item.productID, e.message); return { productID: item.productID, status: 'ERROR', message: e.message, lineNumber: item.lineNumber }; } }; /** * Writes a chunk of processed items * @param {dw.util.ArrayList} chunk - Collection of processed items */ exports.write = function (chunk, parameters, stepExecution) { var successCount = 0; var errorCount = 0; for (var i = 0; i < chunk.size(); i++) { var item = chunk.get(i); if (item.status === 'READY') { try { // Update product attribute - transaction will be committed in afterChunk item.product.setOnlineFlag(item.newOnlineStatus); successCount++; } catch (e) { errorCount++; logger.error('Failed to update product {0}: {1}', item.productID, e.message); } } else { errorCount++; } } logger.info('Chunk processed: {0} success, {1} errors', successCount, errorCount); }; /** * Pre-chunk setup to begin a transaction (Optional but recommended) */ exports.beforeChunk = function (parameters, stepExecution) { // Begin transaction for this chunk Transaction.begin(); }; /** * Commits transaction after successful chunk processing (Optional but recommended) */ exports.afterChunk = function (parameters, stepExecution) { // CRITICAL: Commit transaction per chunk for resilience Transaction.commit(); }; /** * Cleanup function - closes resources */ exports.afterStep = function (success, parameters, stepExecution) { if (csvReader) { csvReader.close(); } logger.info('Processing completed. Total lines processed: {0}', processedLines); if (!success) { logger.error('Job completed with errors'); } }; ``` #### Optional Lifecycle Hooks ```javascript /** * Post-step reporting (optional) */ exports.afterStep = function (success, parameters, stepExecution) { var logger = Logger.getLogger('jobs', 'ReportingJob'); if (success) { // Send success notification sendProcessingReport(parameters, 'SUCCESS', processedLines); } else { // Handle failure case sendProcessingReport(parameters, 'FAILED', processedLines); } }; ``` ### Chunk-Oriented Job Configuration #### steptypes.json Structure ```json { "step-types": { "chunk-script-module-step": [ { "@type-id": "custom.BulkProductUpdate", "@supports-site-context": true, "@supports-organization-context": false, "description": "Updates product online status from CSV file in chunks", "module": "plugin_examplecartridge/cartridge/scripts/jobs/bulkProductUpdate.js", "before-step-function": "beforeStep", "total-count-function": "getTotalCount", "before-chunk-function": "beforeChunk", "read-function": "read", "process-function": "process", "write-function": "write", "after-chunk-function": "afterChunk", "after-step-function": "afterStep", "chunk-size": 200, "transactional": false, "parameters": [ { "@name": "inputFileName", "@type": "string", "@required": true, "description": "Name of CSV file in IMPEX folder" }, { "@name": "hasHeader", "@type": "boolean", "@required": false, "description": "Whether CSV file has header row" } ], "status-codes": { "status": [ { "@code": "OK", "description": "Processing completed successfully" }, { "@code": "ERROR", "description": "An error occurred during processing" } ] } } ] } } ``` ## Performance Optimization Strategies ### Memory Management Best Practices #### 1. Always Use Streaming APIs ```javascript // ❌ WRONG: Loading entire file into memory var content = new FileReader(file).readBytes(); var lines = content.toString().split('\n'); // ✅ CORRECT: Stream processing var csvReader = new CSVStreamReader(new FileReader(file)); var line; while ((line = csvReader.readNext()) !== null) { // Process one line at a time } csvReader.close(); ``` #### 2. Proper Iterator Management ```javascript // ✅ CORRECT: Always close iterators exports.execute = function (parameters, stepExecution) { var products = null; try { products = ProductMgr.queryAllSiteProducts(); while (products.hasNext()) { var product = products.next(); // Process product } } finally { if (products) { products.close(); // Critical: prevents memory leaks } } }; ``` #### 3. Avoid Object Retention ```javascript // ❌ WRONG: Accumulating objects in memory var allProducts = []; while (products.hasNext()) { allProducts.push(products.next()); // Memory will grow linearly } // ✅ CORRECT: Process and release while (products.hasNext()) { var product = products.next(); processProduct(product); // Product reference goes out of scope and can be garbage collected } ``` ### Transaction Management Strategies #### 1. Optimal Chunk Sizing ```javascript // Chunk size guidelines based on operation complexity: // - Simple attribute updates: 500-1000 items // - Complex object creation: 100-300 items // - File I/O operations: 200-500 items // Example configuration for different scenarios: "chunk-size": 250 // Good balance for most operations ``` #### 2. Transaction Per Chunk Pattern ```javascript exports.beforeChunk = function (parameters, stepExecution) { Transaction.begin(); }; exports.write = function (chunk, parameters, stepExecution) { // Perform all database modifications here for (var i = 0; i < chunk.size(); i++) { var item = chunk.get(i); // Database updates happen within the transaction updateDatabaseObject(item); } }; exports.afterChunk = function (parameters, stepExecution) { try { Transaction.commit(); // Commit successful chunk } catch (e) { Transaction.rollback(); // Rollback failed chunk throw e; // Re-throw to mark chunk as failed } }; ``` ### Efficient API Usage Patterns #### 1. Minimize API Calls in Loops ```javascript // ❌ WRONG: API call per iteration for (var i = 0; i < productIDs.length; i++) { var product = ProductMgr.getProduct(productIDs[i]); // Expensive! processProduct(product); } // ✅ BETTER: Batch processing where possible var ProductSearchModel = require('dw/catalog/ProductSearchModel'); var productSearchModel = new ProductSearchModel(); productSearchModel.setSearchPhrase(productIDs.join(' OR ')); var products = productSearchModel.getProductSearchHits(); ``` #### 2. Smart Caching Strategies ```javascript var categoryCache = {}; function getCachedCategory(categoryID) { if (!categoryCache[categoryID]) { categoryCache[categoryID] = CatalogMgr.getCategory(categoryID); } return categoryCache[categoryID]; } ``` #### 3. 🔥 Critical Performance Pattern: Leverage SeekableIterator Built-in Count **ALWAYS use the SeekableIterator's built-in count property instead of creating separate iterators for counting.** A common but severely inefficient anti-pattern is implementing getTotalCount() by creating a dedicated iterator just to count records (e.g., creating a second ProductMgr.queryAllSiteProducts() call). SFCC's SeekableIterator provides a getCount() method and count property that returns the total count without any additional database queries or iteration. ##### ✅ OPTIMAL Pattern: ```javascript var products; // Declare at module level // Initialize iterator once in beforeStep exports.beforeStep = function(parameters) { products = ProductMgr.queryAllSiteProductsSorted(); // Single query } // Use iterator's built-in count - no additional overhead! exports.getTotalCount = function() { if (products && products.getCount) { var totalCount = products.getCount(); // Instant, no DB hit return totalCount >= 0 ? totalCount : null; } return null; } // Use the same iterator for reading exports.read = function() { return products.hasNext() ? products.next() : null; } ``` ##### ❌ SEVERELY INEFFICIENT Anti-Pattern: ```javascript // This creates TWO expensive database queries! exports.getTotalCount = function() { var counter = ProductMgr.queryAllSiteProducts(); // Unnecessary Query #1 var count = 0; while (counter.hasNext()) { counter.next(); count++; } // Manual counting! counter.close(); return count; } exports.read = function() { if (!products) { products = ProductMgr.queryAllSiteProductsSorted(); // Query #2 } return products.hasNext() ? products.next() : null; } ``` ##### 📊 Performance Impact The anti-pattern increases database load by **100%** and can add **1-5 minutes** to job startup for large catalogs. Using SeekableIterator.getCount() provides instant total count retrieval with zero additional overhead. ##### 🎯 Key Insight SFCC's query methods return SeekableIterator instances that already know their total count from the database query execution. Leveraging this built-in capability eliminates the need for manual counting entirely while providing accurate progress tracking for Business Manager users. ## Security Best Practices ### Secure Logging Practices ```javascript // ❌ WRONG: Logging sensitive data logger.info('Processing customer: {0}', customer.getProfile().getEmail()); // ✅ CORRECT: Log only necessary identifiers logger.info('Processing customer ID: {0}', customer.getProfile().getCustomerNo()); ``` ### Input Validation and Sanitization ```javascript exports.process = function (item, parameters, stepExecution) { var logger = Logger.getLogger('jobs', 'ReportingJob'); // Validate input data if (!item.productID || typeof item.productID !== 'string') { return { status: 'ERROR', message: 'Invalid product ID format' }; } // Sanitize string inputs var sanitizedID = item.productID.trim().substring(0, 100); // Validate business rules if (item.price !== undefined && (isNaN(item.price) || item.price < 0)) { return { status: 'ERROR', message: 'Invalid price value' }; } // Process validated data return processValidatedItem(sanitizedID, item); }; ``` ## Debugging and Troubleshooting ### Common Issues and Solutions #### 1. Memory-Related Failures ```javascript // OutOfMemoryError prevention checklist: // ✓ Use streaming APIs for file processing // ✓ Close all SeekableIterators // ✓ Avoid accumulating objects in global scope // ✓ Set appropriate chunk size (start with 250) // ✓ Process items and release references quickly exports.read = function (parameters, stepExecution) { // Good: Read one item at a time return csvReader.readNext(); }; exports.afterStep = function (success, parameters, stepExecution) { // Critical: Clean up resources if (csvReader) csvReader.close(); if (xmlReader) xmlReader.close(); if (iterator) iterator.close(); }; ``` #### 2. Transaction Timeout Prevention ```javascript // Prevent long-running transactions exports.beforeChunk = function (parameters, stepExecution) { Transaction.begin(); // Keep transaction scope limited to single chunk }; exports.afterChunk = function (parameters, stepExecution) { Transaction.commit(); // Transaction released - prevents long-running locks }; ``` #### 3. Effective Error Handling ```javascript exports.process = function (item, parameters, stepExecution) { var logger = stepExecution.log; try { return processItem(item); } catch (e) { // Log detailed error for debugging logger.error('Failed to process item {0}: {1}', item.id, e.message + '\n' + e.stack); // Return error result instead of throwing return { id: item.id, status: 'ERROR', message: e.message }; } }; ``` ### Diagnostic Tools and Techniques #### 1. Comprehensive Logging Strategy ```javascript exports.beforeStep = function (parameters, stepExecution) { var logger = Logger.getLogger('jobs', 'ReportingJob'); logger.info('Job started with parameters: {0}', JSON.stringify(parameters)); // Memory monitoring not available in SFCC environment }; exports.afterChunk = function (parameters, stepExecution) { var logger = Logger.getLogger('jobs', 'ReportingJob'); // Memory monitoring not available in SFCC environment logger.info('Chunk completed successfully'); }; ``` #### 2. Performance Monitoring ```javascript var startTime = Date.now(); var itemCount = 0; exports.process = function (item, parameters, stepExecution) { itemCount++; // Log performance metrics periodically if (itemCount % 1000 === 0) { var elapsed = Date.now() - startTime; var rate = itemCount / (elapsed / 1000); Logger.getLogger('jobs', 'ReportingJob'); .info('Processed {0} items at {1} items/second', itemCount, rate.toFixed(2)); } return processItem(item); }; ``` ## Deployment and Configuration Best Practices ### Steptypes.json Configuration File #### File Placement and Structure Custom step types are registered in the SFCC system using a `steptypes.json` file. This file defines the step types, their parameters, and status codes for your cartridge. **Critical Requirements:** - File must be named **exactly** `steptypes.json` - Must be placed in the **root folder** of your custom cartridge (not inside the cartridge subfolder) - Only **one** `steptypes.json` file per cartridge is allowed - Cannot coexist with `steptypes.xml` - choose one format only **Correct File Structure:** ``` my_cartridge/ ├── cartridge/ │ ├── controllers/ │ ├── scripts/ │ ├── (other cartridge folders) │ └── my_cartridge.properties └── steptypes.json ← Must be here, not inside cartridge/ ``` #### Root Structure All steptypes.json files must follow this root structure: ```json { "step-types": { "script-module-step": [], "chunk-script-module-step": [], "pipeline-step": [] } } ``` **Note**: You must define at least one step type category, but all three are optional as long as one is present. ### Task-Oriented Job Configuration (script-module-step) #### Complete Attribute Reference ```json { "step-types": { "script-module-step": [ { "@type-id": "custom.DeactivateProducts", "@supports-parallel-execution": true, "@supports-site-context": true, "@supports-organization-context": false, "description": "Deactivates all online products in a specified category", "module": "plugin_examplecartridge/cartridge/scripts/jobs/deactivateProducts.js", "function": "execute", "transactional": false, "timeout-in-seconds": 900, "parameters": { "parameter": [ { "@name": "categoryID", "@type": "string", "@required": true, "description": "ID of the category containing products to deactivate" }, { "@name": "dryRun", "@type": "boolean", "@required": false, "description": "If true, only logs what would be deactivated" } ] }, "status-codes": { "status": [ { "@code": "OK", "description": "Products deactivated successfully" }, { "@code": "ERROR", "description": "An error occurred during processing" } ] } } ] } } ``` #### Attribute Details | Attribute | Required | Description | Validation Rules | |-----------|----------|-------------|------------------| | `@type-id` | ✅ **Required** | Unique identifier for the step type | Must start with `custom.`, max 100 chars, no whitespace, unique across all cartridges | | `@supports-parallel-execution` | Optional | Enables parallel execution with other steps | `true` or `false` (default: `true`) | | `@supports-site-context` | Optional | Can be used in site-scoped flows | `true` or `false` (default: `true`) | | `@supports-organization-context` | Optional | Can be used in organization-scoped flows | `true` or `false` (default: `true`) | | `description` | Optional | Internal description (not shown in Business Manager) | Max 4000 characters | | `module` | ✅ **Required** | Path to script module | No leading/trailing whitespace | | `function` | ✅ **Required** | Function name to execute | No leading/trailing whitespace, defaults to `execute` | | `transactional` | Optional | Wraps execution in single transaction | `true` or `false` (default: `false`) | | `timeout-in-seconds` | Optional | Execution timeout limit | Integer > 0, no default | | `parameters` | Optional | User-configurable parameters | Contains `parameter` array | | `status-codes` | Optional | Custom status code definitions | Contains `status` array | **Important Notes:** - `@supports-site-context` and `@supports-organization-context` cannot both be `true` or both be `false` - `@type-id` cannot conflict with system step types like `ExecutePipeline` or `IncludeStepsFromJob` - Setting `transactional: true` creates one large transaction - avoid for performance reasons ### Chunk-Oriented Job Configuration (chunk-script-module-step) #### Complete Attribute Reference ```json { "step-types": { "chunk-script-module-step": [ { "@type-id": "custom.BulkPriceUpdate", "@supports-parallel-execution": false, "@supports-site-context": true, "@supports-organization-context": false, "description": "Updates product prices from CSV file in chunks", "module": "plugin_examplecartridge/cartridge/scripts/jobs/bulkPriceUpdate.js", "before-step-function": "beforeStep", "total-count-function": "getTotalCount", "before-chunk-function": "beforeChunk", "read-function": "read", "process-function": "process", "write-function": "write", "after-chunk-function": "afterChunk", "after-step-function": "afterStep", "chunk-size": 200, "transactional": false, "parameters": { "parameter": [ { "@name": "inputFileName", "@type": "string", "@required": true, "description": "Name of CSV file in IMPEX folder" }, { "@name": "hasHeader", "@type": "boolean", "@required": false, "description": "Whether CSV file has header row" } ] }, "status-codes": { "status": [ { "@code": "OK", "description": "Processing completed successfully" }, { "@code": "ERROR", "description": "An error occurred during processing" } ] } } ] } } ``` #### Chunk-Specific Attributes | Attribute | Required | Description | Default Function | |-----------|----------|-------------|------------------| | `before-step-function` | Optional | Setup function before processing starts | None | | `total-count-function` | Optional | Returns total items for progress tracking | None | | `before-chunk-function` | Optional | Setup before each chunk | None | | `read-function` | Optional | Reads next item from data source | `read` | | `process-function` | Optional | Processes individual items | `process` | | `write-function` | Optional | Writes processed chunk | `write` | | `after-chunk-function` | Optional | Cleanup after each chunk | None | | `after-step-function` | Optional | Final cleanup after all chunks | None | | `chunk-size` | ✅ **Required** | Number of items per chunk | Must be numeric > 0 | ### Parameter Configuration Parameters allow Business Manager users to configure job execution. Each parameter supports these attributes: ```json { "@name": "parameterName", "@type": "string|boolean|number|password", "@required": true, "description": "User-friendly description shown in Business Manager" } ``` #### Supported Parameter Types - `string`: Text input - `boolean`: Checkbox (true/false) - `number`: Numeric input - `password`: Masked text input (use for sensitive data) ### Status Code Configuration Custom status codes enable sophisticated flow control in Business Manager: ```json { "status-codes": { "status": [ { "@code": "OK", "description": "Standard success" }, { "@code": "CUSTOM_STATUS", "description": "Custom workflow trigger" }, { "@code": "ERROR", "description": "Processing failed" } ] } } ``` **Flow Control Usage:** - Use custom status codes to branch job flows - Configure different follow-up steps based on status - Enable conditional processing in complex workflows ### Pipeline Step Configuration For legacy pipeline-based steps (not recommended for new development): ```json { "step-types": { "pipeline-step": [ { "@type-id": "custom.LegacyPipelineStep", "@supports-site-context": true, "@supports-organization-context": false, "description": "Legacy pipeline step", "pipeline": "cartridge/pipelines/jobs/LegacyPipeline.xml", "start-node": "Start" } ] } } ``` **Note**: Pipeline steps are legacy - use script-module-step or chunk-script-module-step for new development. ### Resource Management #### 1. Proper Resource Locking Configure resource locks in Business Manager to prevent conflicts: - Lock catalogs when modifying products - Lock inventory lists during inventory updates - Lock price books during price modifications #### 2. Optimal Scheduling Strategies ```javascript // Stagger job start times to distribute load // Good: 01:17, 02:23, 03:08 // Bad: 01:00, 02:00, 03:00 (creates load spikes) // Keep job load factor below 0.20 // Monitor total job execution time per day ``` #### 3. Environment-Specific Considerations ```javascript exports.beforeStep = function (parameters, stepExecution) { var logger = Logger.getLogger('jobs', 'ReportingJob'); var System = require('dw/system/System'); if (System.getInstanceType() === System.DEVELOPMENT_SYSTEM) { logger.debug('Running in development mode'); // Enable verbose logging for development } else if (System.getInstanceType() === System.STAGING_SYSTEM) { logger.debug('Running in staging mode'); // Moderate logging for staging } else if (System.getInstanceType() === System.PRODUCTION_SYSTEM) { logger.info('Running in production mode'); // Minimal logging for performance } }; ``` ## Advanced Patterns and Integration ### External Service Integration ```javascript var ServiceRegistry = require('dw/svc/ServiceRegistry'); exports.process = function (item, parameters, stepExecution) { var logger = Logger.getLogger('jobs', 'ReportingJob'); try { var service = ServiceRegistry.get('MyExternalService'); var result = service.call({ productID: item.productID, action: 'update' }); if (result.isOk()) { return { productID: item.productID, status: 'SUCCESS', externalID: result.object.id }; } else { logger.warn('Service call failed for {0}: {1}', item.productID, result.getErrorMessage()); return { productID: item.productID, status: 'ERROR', message: result.getErrorMessage() }; } } catch (e) { logger.error('Service error for {0}: {1}', item.productID, e.message); return { productID: item.productID, status: 'ERROR', message: e.message }; } }; ``` ### Idempotent Job Design ```javascript // Design jobs to be safely re-runnable exports.beforeStep = function (parameters, stepExecution) { var logger = Logger.getLogger('jobs', 'ReportingJob'); // Check for previous successful run var statusFile = new File(File.IMPEX + 'status/' + parameters.jobRunID + '.complete'); if (statusFile.exists()) { logger.info('Job already completed successfully. Skipping.'); return new Status(Status.OK, 'ALREADY_COMPLETED'); } // Create processing flag var processingFile = new File(File.IMPEX + 'status/' + parameters.jobRunID + '.processing'); var writer = new FileWriter(processingFile); writer.writeLine(new Date().toISOString()); writer.close(); }; exports.afterStep = function (success, parameters, stepExecution) { if (success) { // Mark job as completed var statusFile = new File(File.IMPEX + 'status/' + parameters.jobRunID + '.complete'); var writer = new FileWriter(statusFile); writer.writeLine(new Date().toISOString()); writer.close(); // Clean up processing flag var processingFile = new File(File.IMPEX + 'status/' + parameters.jobRunID + '.processing'); processingFile.remove(); } }; ``` ## Quick Reference ### Choosing the Right Job Model **Use Task-Oriented When:** - Processing single files or making single API calls - Quick database updates affecting known small datasets - Simple configuration or setup tasks - Progress tracking is not important **Use Chunk-Oriented When:** - Processing large datasets (>1000 items) - Iterating over products, orders, customers, or file rows - Progress monitoring is required - Failure resilience is critical - Transaction control is important ### Essential Performance Guidelines 1. **Always use streaming APIs** for file processing 2. **Close all SeekableIterators** to prevent memory leaks 3. **Keep chunk sizes between 100-500** for most operations 4. **Commit transactions per chunk** for resilience 5. **Avoid accumulating objects** in global scope 6. **Log appropriately** - info for milestones, debug for development only 7. **Validate inputs** and handle errors gracefully 8. **Design for idempotency** to enable safe re-runs ### Common Troubleshooting Steps 1. **OutOfMemoryError**: Check streaming APIs, iterator closure, chunk size 2. **ScriptingTimeoutError**: Consider chunk-oriented model, review algorithm efficiency 3. **Transaction timeouts**: Reduce chunk size, commit per chunk 4. **Job hangs**: Check resource locks, review for infinite loops 5. **Poor performance**: Use Code Profiler, review API usage patterns 6. **Job Not Visible in Business Manager**: If custom jobs don't appear in Administration > Operations > Jobs after deployment: - **Check Available Versions**: Use MCP `get_code_versions` tool to see all code versions on the instance - **Activate Different Version**: Use MCP `activate_code_version` tool to switch code versions - **Alternative Manual Method**: Switch code versions in Business Manager (`Administration > Site Development > Code Deployment > Activate`) Remember: **The Job Framework is critical infrastructure**. Always prioritize stability, performance, and maintainability over quick implementation. ``` -------------------------------------------------------------------------------- /docs/best-practices/sfra_models.md: -------------------------------------------------------------------------------- ```markdown # Salesforce B2C Commerce SFRA Models: Best Practices & Implementation Guide This guide provides comprehensive best practices for creating, extending, and customizing models within the Salesforce B2C Commerce Storefront Reference Architecture (SFRA). Models are the JSON object layer that converts B2C Commerce Script API objects into pure JSON designed for storefront rendering. **IMPORTANT**: Before developing SFRA models, consult the **Performance and Stability Best Practices** guide from this MCP server. Review database optimization guidelines and index-friendly API usage to ensure your models follow SFCC performance standards. ## 🚀 Enhanced SFRA Model Documentation Access This MCP server provides comprehensive access to SFRA model documentation through enhanced tools that can help you understand existing models and develop better custom ones: ### **Essential SFRA Model Documentation Tools** 1. **`get_sfra_categories`** - Discover all available SFRA documentation categories including models 2. **`get_sfra_documents_by_category`** - Explore model documents by functional area: - `product` - Product models including product-full, product-tile, product-bundle - `order` - Cart, billing, shipping, payment, totals models - `customer` - Account, address models - `pricing` - Price models for different pricing scenarios - `store` - Store and location models 3. **`get_sfra_document`** - Get detailed information about specific SFRA models 4. **`search_sfra_documentation`** - Find specific model functionality across all documentation ### **Recommended Model Documentation Workflow** Before creating or extending models, use these tools to understand existing model patterns: ```javascript // 1. Explore all available model categories get_sfra_categories() // 2. Examine product models for complex data structures get_sfra_documents_by_category("product") get_sfra_document("product-full") // Comprehensive product model // 3. Study cart and order models for business logic patterns get_sfra_documents_by_category("order") get_sfra_document("cart") // Complex cart model with totals and line items // 4. Review customer models for profile management get_sfra_documents_by_category("customer") get_sfra_document("account") // Customer account model // 5. Search for specific model functionality search_sfra_documentation("decorator") search_sfra_documentation("model") ``` ## Core Model Concepts ### What Are SFRA Models? SFRA models serve as a JSON transformation layer that: - **Convert** SFCC Script API objects into pure JSON objects - **Apply** business logic specific to the storefront - **Structure** data for optimal template rendering - **Provide** a consistent interface between controllers and views Models are passed to templates via the `viewData` variable (accessible as `pdict` in templates). ### Model Design Principles 1. **Pure JSON Output**: Models should return only JSON-serializable data 2. **Single Responsibility**: Each model should handle one specific domain 3. **Composability**: Models should be easily combinable and extensible 4. **Performance**: Minimize API calls and database queries within models 5. **Testability**: Models should be unit-testable with mock data ## Default SFRA Model Project Structure Understanding the default SFRA model structure is essential for building consistent and maintainable e-commerce applications. The base SFRA cartridge (`app_storefront_base`) provides a comprehensive model library that follows established patterns and conventions. ### Directory Organization ``` cartridge/models/ ├── account.js # Customer account model ├── address.js # Address transformation model ├── billing.js # Billing information model ├── cart.js # Shopping cart model with totals ├── categories.js # Category navigation model ├── content.js # Content asset model ├── locale.js # Locale and currency model ├── order.js # Order model for order history ├── payment.js # Payment instrument model ├── productLineItems.js # Collection of product line items ├── shipping.js # Shipping address and methods ├── store.js # Individual store model ├── stores.js # Store locator collection model ├── totals.js # Order/cart totals calculation model ├── price/ # Price-related models │ ├── default.js # Standard price model │ ├── range.js # Price range model (min-max) │ └── tiered.js # Tiered pricing model ├── product/ # Product-related models │ ├── bonusProduct.js # Bonus/promotional product model │ ├── fullProduct.js # Complete product details model │ ├── productAttributes.js # Product attributes model │ ├── productBundle.js # Product bundle model │ ├── productImages.js # Product images model │ ├── productSet.js # Product set model │ ├── productTile.js # Product tile/listing model │ └── decorators/ # Product decorator functions │ ├── index.js # Decorator registry │ ├── attributes.js # Product attributes decorator │ ├── availability.js # Stock availability decorator │ ├── base.js # Core product information │ ├── bonusUnitPrice.js # Bonus pricing decorator │ ├── bundleReadyToOrder.js # Bundle ordering readiness │ ├── bundledProducts.js # Bundle component products │ ├── currentUrl.js # Product URL decorator │ ├── description.js # Product description decorator │ ├── images.js # Product images decorator │ ├── online.js # Online availability decorator │ ├── options.js # Product options decorator │ ├── pageMetaData.js # SEO metadata decorator │ ├── price.js # Price calculation decorator │ ├── promotions.js # Promotional pricing decorator │ ├── quantity.js # Quantity handling decorator │ ├── quantitySelector.js # Quantity selector decorator │ ├── ratings.js # Product ratings decorator │ ├── raw.js # Raw API object decorator │ ├── readyToOrder.js # Order readiness decorator │ ├── searchPrice.js # Search result pricing │ ├── searchVariationAttributes.js # Search variation handling │ ├── setIndividualProducts.js # Product set individuals │ ├── setProductsCollection.js # Product set collection │ ├── setReadyToOrder.js # Set ordering readiness │ ├── sizeChart.js # Size chart decorator │ ├── template.js # Template selection decorator │ └── variationAttributes.js # Variation attributes decorator ├── productLineItem/ # Product line item models │ ├── bonusOrderLineItem.js # Bonus order line item │ ├── bonusProductLineItem.js # Bonus product line item │ ├── bundleLineItem.js # Bundle line item model │ ├── bundleOrderLineItem.js # Bundle order line item │ ├── orderLineItem.js # Order line item model │ ├── productLineItem.js # Cart line item model │ └── decorators/ # Line item decorators │ ├── index.js # Decorator registry │ ├── appliedPromotions.js # Applied promotion decorator │ ├── bonusProductLineItem.js # Bonus product decorator │ ├── bonusProductLineItemUUID.js # Bonus UUID decorator │ ├── bonusUnitPrice.js # Bonus pricing decorator │ ├── bundledProductLineItems.js # Bundle items decorator │ ├── discountBonusLineItems.js # Discount bonus decorator │ ├── gift.js # Gift message decorator │ ├── options.js # Line item options decorator │ ├── orderable.js # Orderability decorator │ ├── preOrderUUID.js # Pre-order UUID decorator │ ├── priceTotal.js # Price total decorator │ ├── quantity.js # Quantity decorator │ ├── quantityOptions.js # Quantity options decorator │ ├── renderedPromotions.js # Rendered promotions decorator │ ├── shipment.js # Shipment decorator │ └── uuid.js # UUID decorator ├── search/ # Search-related models │ ├── contentSearch.js # Content search results model │ ├── productSearch.js # Product search results model │ ├── productSortOptions.js # Sort options model │ ├── attributeRefinementValue/ # Search refinement models │ └── suggestions/ # Search suggestion models └── shipping/ # Shipping-related models └── shippingMethod.js # Shipping method model ``` ### Key Model Categories #### **1. Core Commerce Models** - **`cart.js`** - Central shopping cart model with totals, line items, and promotions - **`order.js`** - Order model for order history and confirmation pages - **`account.js`** - Customer account information and preferences - **`payment.js`** - Payment instruments and billing information #### **2. Product Models** - **`product/fullProduct.js`** - Complete product details for PDP - **`product/productTile.js`** - Lightweight product info for listings - **`product/productBundle.js`** - Bundle product handling - **`product/decorators/`** - Modular product enhancement functions #### **3. Line Item Models** - **`productLineItem/productLineItem.js`** - Cart line items - **`productLineItem/orderLineItem.js`** - Order line items - **`productLineItem/decorators/`** - Line item enhancement functions #### **4. Pricing Models** - **`price/default.js`** - Standard pricing model - **`price/range.js`** - Price range for variable pricing - **`price/tiered.js`** - Volume-based pricing tiers - **`totals.js`** - Cart/order total calculations #### **5. Location & Shipping Models** - **`store.js`** & **`stores.js`** - Store locator functionality - **`shipping.js`** - Shipping address and method selection - **`address.js`** - Address transformation and validation #### **6. Search Models** - **`search/productSearch.js`** - Search results and refinements - **`search/contentSearch.js`** - Content search functionality - **`search/productSortOptions.js`** - Sorting options for search ### Model Inheritance Patterns The SFRA model structure follows several key patterns: #### **Decorator Pattern** (Product Models) ```javascript // product/decorators/index.js - Central decorator registry module.exports = { base: require('*/cartridge/models/product/decorators/base'), price: require('*/cartridge/models/product/decorators/price'), images: require('*/cartridge/models/product/decorators/images'), // ... other decorators }; // Usage in fullProduct.js var decorators = require('*/cartridge/models/product/decorators/index'); function fullProduct(product, apiProduct, options) { decorators.base(product, apiProduct, options.productType); decorators.price(product, apiProduct, options.promotions); decorators.images(product, apiProduct, options); return product; } ``` #### **Composition Pattern** (Complex Models) ```javascript // cart.js - Composes multiple sub-models var TotalsModel = require('*/cartridge/models/totals'); var ProductLineItemsModel = require('*/cartridge/models/productLineItems'); function Cart(basket) { this.totals = new TotalsModel(basket); this.items = new ProductLineItemsModel(basket.productLineItems); this.shipments = createShipmentModels(basket.shipments); } ``` #### **Factory Pattern** (Conditional Models) ```javascript // Different models based on product type function createProductModel(apiProduct, type) { switch (type) { case 'full': return require('*/cartridge/models/product/fullProduct'); case 'tile': return require('*/cartridge/models/product/productTile'); case 'bundle': return require('*/cartridge/models/product/productBundle'); } } ``` ### Best Practices for Structure 1. **Follow the Directory Convention**: Maintain the same directory structure in custom cartridges 2. **Use Decorator Registries**: Create `index.js` files to register and export decorators 3. **Separate Concerns**: Keep pricing, images, and availability in separate decorators 4. **Maintain Model Hierarchy**: Extend base models rather than replacing entirely 5. **Consistent Naming**: Follow SFRA naming conventions for predictable imports This structure provides a solid foundation for building scalable, maintainable e-commerce models that integrate seamlessly with the SFRA framework. ## Model Architecture Patterns ### 1. Simple Data Models Basic models that transform API objects into JSON structures: ```javascript 'use strict'; /** * Address Model - Transforms dw.customer.OrderAddress into JSON * @param {dw.customer.OrderAddress} addressObject - Address from SFCC API * @constructor */ function Address(addressObject) { if (addressObject) { this.address = { firstName: addressObject.firstName, lastName: addressObject.lastName, address1: addressObject.address1, address2: addressObject.address2, city: addressObject.city, postalCode: addressObject.postalCode, stateCode: addressObject.stateCode, countryCode: addressObject.countryCode.value, phone: addressObject.phone }; } } module.exports = Address; ``` ### 2. Decorator Pattern Models Complex models using the decorator pattern for modular composition: ```javascript 'use strict'; var decorators = require('*/cartridge/models/product/decorators/index'); /** * Full Product Model using Decorator Pattern * @param {dw.catalog.Product} apiProduct - Product from SFCC API * @param {Object} options - Configuration options * @returns {Object} - Decorated product model */ function fullProduct(product, apiProduct, options) { // Base product information decorators.base(product, apiProduct, options.productType); // Price information with promotions decorators.price(product, apiProduct, options.promotions, false, options.optionModel); // Images with different sizes if (options.variationModel) { decorators.images(product, options.variationModel, { types: ['large', 'small'], quantity: 'all' }); } else { decorators.images(product, apiProduct, { types: ['large', 'small'], quantity: 'all' }); } // Additional decorators for complete product information decorators.quantity(product, apiProduct, options.quantity); decorators.variationAttributes(product, options.variationModel, { attributes: '*', endPoint: 'Variation' }); decorators.description(product, apiProduct); decorators.ratings(product); decorators.promotions(product, options.promotions); decorators.attributes(product, apiProduct.attributeModel); decorators.availability(product, options.quantity, apiProduct.minOrderQuantity.value, apiProduct.availabilityModel); decorators.options(product, options.optionModel, options.variables, options.quantity); return product; } module.exports = fullProduct; ``` **⚠️ CRITICAL DECORATOR WARNING** **NEVER** create decorators that return a new object or use `Object.assign()` to clone the product. This breaks the SFRA model pattern and causes unpredictable behavior: ```javascript // ❌ WRONG: Never do this in a decorator module.exports = function randomString(product) { var decoratedProduct = Object.assign({}, product); decoratedProduct.randomString = Math.random().toString(36).substring(2, 15); return decoratedProduct; // This breaks the model! }; ``` **Why this breaks models:** - Decorators should **mutate** the original product object, not create copies - Returning a new object breaks the reference chain between decorators - Subsequent decorators will operate on the wrong object - The final model will be incomplete or corrupted ```javascript // ✅ CORRECT: Mutate the original object module.exports = function randomString(product) { product.randomString = Math.random().toString(36).substring(2, 15); // No return statement - decorators modify in place }; ``` **Decorator Best Practices:** 1. **Mutate, don't clone** - Always modify the passed product object directly 2. **No return values** - Decorators should not return anything 3. **Side effects only** - Decorators should only add/modify properties 4. **Reference preservation** - Keep the original object reference intact ### 3. Factory Pattern Models Models that create different types based on input: ```javascript 'use strict'; var ProductFactory = require('*/cartridge/scripts/factories/product'); var fullProduct = require('*/cartridge/models/product/fullProduct'); var productTile = require('*/cartridge/models/product/productTile'); /** * Product Model Factory * @param {dw.catalog.Product} apiProduct - Product from API * @param {Object} options - Configuration options * @param {string} type - Model type ('full', 'tile', 'bundle') * @returns {Object} - Appropriate product model */ function create(apiProduct, options, type) { var product = {}; switch (type) { case 'full': return fullProduct(product, apiProduct, options); case 'tile': return productTile(product, apiProduct, options); case 'bundle': return bundleProduct(product, apiProduct, options); default: return fullProduct(product, apiProduct, options); } } module.exports = { get: create }; ``` ## Model Customization Strategies ### 1. Extending Base Models Override base models by creating a model with the same name in your custom cartridge: ```javascript 'use strict'; // Import the base model var base = module.superModule; /** * Extended Account Model with additional customer data * @param {dw.customer.Customer} currentCustomer - Current customer * @param {Object} addressBook - Customer's address book * @param {Object} orderHistory - Customer's order history * @constructor */ function Account(currentCustomer, addressBook, orderHistory) { // Call base constructor base.call(this, currentCustomer, addressBook, orderHistory); // Add custom properties this.loyaltyProgram = getLoyaltyProgram(currentCustomer); this.preferences = getCustomerPreferences(currentCustomer); this.communicationPreferences = getCommunicationPreferences(currentCustomer); } /** * Get customer loyalty program information * @param {dw.customer.Customer} customer - Customer object * @returns {Object} Loyalty program data */ function getLoyaltyProgram(customer) { if (customer && customer.profile && customer.profile.custom.loyaltyNumber) { return { number: customer.profile.custom.loyaltyNumber, tier: customer.profile.custom.loyaltyTier || 'Bronze', points: customer.profile.custom.loyaltyPoints || 0 }; } return null; } /** * Get customer preferences * @param {dw.customer.Customer} customer - Customer object * @returns {Object} Customer preferences */ function getCustomerPreferences(customer) { if (customer && customer.profile) { return { language: customer.profile.custom.preferredLanguage || 'en', currency: customer.profile.custom.preferredCurrency || 'USD', newsletter: customer.profile.custom.newsletterSubscription || false }; } return {}; } module.exports = Account; ``` ### 2. Creating New Decorators Add new functionality through custom decorators: ```javascript 'use strict'; /** * Social Media Decorator - Adds social sharing data to product * @param {Object} product - Product model to decorate * @param {dw.catalog.Product} apiProduct - Product from API */ module.exports = function socialMedia(product, apiProduct) { var URLUtils = require('dw/web/URLUtils'); var Site = require('dw/system/Site'); var socialData = {}; // Facebook sharing socialData.facebook = { url: URLUtils.https('Product-Show', 'pid', apiProduct.ID).toString(), title: apiProduct.name, description: apiProduct.shortDescription ? apiProduct.shortDescription.markup : '', image: getProductImage(apiProduct, 'large') }; // Twitter sharing socialData.twitter = { url: URLUtils.https('Product-Show', 'pid', apiProduct.ID).toString(), text: apiProduct.name + ' - ' + Site.getCurrent().getName(), hashtags: getProductHashtags(apiProduct) }; // Pinterest sharing socialData.pinterest = { url: URLUtils.https('Product-Show', 'pid', apiProduct.ID).toString(), description: apiProduct.name, media: getProductImage(apiProduct, 'large') }; Object.defineProperty(product, 'social', { enumerable: true, value: socialData }); }; /** * Get product image for social sharing * @param {dw.catalog.Product} apiProduct - Product from API * @param {string} size - Image size * @returns {string} Image URL */ function getProductImage(apiProduct, size) { var images = apiProduct.getImages(size); return images.length > 0 ? images[0].getURL().toString() : ''; } /** * Generate hashtags from product attributes * @param {dw.catalog.Product} apiProduct - Product from API * @returns {string} Comma-separated hashtags */ function getProductHashtags(apiProduct) { var hashtags = []; if (apiProduct.brand) { hashtags.push(apiProduct.brand.replace(/\s+/g, '')); } if (apiProduct.primaryCategory) { hashtags.push(apiProduct.primaryCategory.displayName.replace(/\s+/g, '')); } return hashtags.join(','); } ``` ### 3. Adding Custom Properties to Existing Models Use controller logic to extend models without modifying base files: ```javascript 'use strict'; var server = require('server'); var ProductMgr = require('dw/catalog/ProductMgr'); var ProductFactory = require('*/cartridge/scripts/factories/product'); server.get('Show', function (req, res, next) { var productId = req.querystring.pid; var product = ProductMgr.getProduct(productId); if (!product) { res.setStatusCode(404); res.render('error/notFound'); return next(); } // Create base product model var productModel = ProductFactory.get({ product: product, variationModel: product.variationModel, options: req.querystring, promotions: null, quantity: parseInt(req.querystring.quantity, 10) || 1, variables: null, apiProduct: product, productType: product.productType }); // Add custom properties productModel.customAttributes = getCustomAttributes(product); productModel.relatedProducts = getRelatedProducts(product); productModel.reviews = getProductReviews(product); productModel.availability = getStoreAvailability(product); res.render('product/productDetails', { product: productModel }); next(); }); /** * Get custom attributes for product * @param {dw.catalog.Product} product - Product object * @returns {Object} Custom attributes */ function getCustomAttributes(product) { var customAttributes = {}; if (product.custom) { customAttributes.sustainabilityRating = product.custom.sustainabilityRating; customAttributes.madeIn = product.custom.madeIn; customAttributes.careInstructions = product.custom.careInstructions; } return customAttributes; } module.exports = server.exports(); ``` ## Advanced Model Patterns ### 1. Composite Models Combine multiple models into complex structures: ```javascript 'use strict'; var CartModel = require('*/cartridge/models/cart'); var AccountModel = require('*/cartridge/models/account'); var AddressModel = require('*/cartridge/models/address'); /** * Checkout Model - Composite model for checkout page * @param {dw.order.Basket} basket - Current basket * @param {dw.customer.Customer} customer - Current customer * @param {Object} options - Additional options * @constructor */ function Checkout(basket, customer, options) { // Cart information this.cart = new CartModel(basket); // Customer information if (customer && customer.authenticated) { this.customer = new AccountModel(customer); this.addresses = getCustomerAddresses(customer); this.paymentInstruments = getCustomerPaymentInstruments(customer); } else { this.customer = null; this.addresses = []; this.paymentInstruments = []; } // Checkout-specific data this.forms = getCheckoutForms(); this.shipping = getShippingOptions(basket); this.payment = getPaymentOptions(); this.order = getOrderSummary(basket); } /** * Get customer addresses as models * @param {dw.customer.Customer} customer - Customer object * @returns {Array} Array of address models */ function getCustomerAddresses(customer) { var addresses = []; var addressBook = customer.addressBook; if (addressBook && addressBook.addresses) { for (var i = 0; i < addressBook.addresses.length; i++) { addresses.push(new AddressModel(addressBook.addresses[i])); } } return addresses; } module.exports = Checkout; ``` ### 2. Cached Models Implement caching for expensive model operations: ```javascript 'use strict'; var CacheMgr = require('dw/system/CacheMgr'); var ProductMgr = require('dw/catalog/ProductMgr'); var Logger = require('dw/system/Logger'); var cache = CacheMgr.getCache('ProductRecommendations'); var logger = Logger.getLogger('models', 'ProductRecommendations'); /** * Product Recommendations Model with Caching * @param {dw.catalog.Product} product - Product for recommendations * @param {number} maxRecommendations - Maximum number of recommendations * @constructor */ function ProductRecommendations(product, maxRecommendations) { this.recommendations = getRecommendations(product, maxRecommendations || 4); } /** * Get product recommendations with caching * @param {dw.catalog.Product} product - Product object * @param {number} maxRecommendations - Maximum recommendations * @returns {Array} Array of recommended products */ function getRecommendations(product, maxRecommendations) { var cacheKey = 'recommendations_' + product.ID + '_' + maxRecommendations; var cachedRecommendations = cache.get(cacheKey); if (cachedRecommendations) { logger.debug('Using cached recommendations for product: {0}', product.ID); return cachedRecommendations; } logger.debug('Generating new recommendations for product: {0}', product.ID); var recommendations = []; var recommendedProducts = product.getRecommendations(); if (recommendedProducts) { var ProductFactory = require('*/cartridge/scripts/factories/product'); var iterator = recommendedProducts.iterator(); while (iterator.hasNext() && recommendations.length < maxRecommendations) { var recommendedProduct = iterator.next(); if (recommendedProduct.online) { var productModel = ProductFactory.get({ product: recommendedProduct, variationModel: recommendedProduct.variationModel, options: {}, promotions: null, quantity: 1, variables: null, apiProduct: recommendedProduct, productType: recommendedProduct.productType }); recommendations.push(productModel); } } } // Cache for 1 hour cache.put(cacheKey, recommendations, 3600); return recommendations; } module.exports = ProductRecommendations; ``` ## Model Testing Strategies ### Unit Testing Models ```javascript 'use strict'; var assert = require('chai').assert; var AccountModel = require('../../../cartridges/app_custom/cartridge/models/account'); describe('Account Model', function() { var mockCustomer; var mockProfile; var mockAddressBook; beforeEach(function() { mockProfile = { firstName: 'John', lastName: 'Doe', email: '[email protected]', custom: { loyaltyNumber: 'LP123456', loyaltyTier: 'Gold', loyaltyPoints: 1500 } }; mockCustomer = { authenticated: true, profile: mockProfile }; mockAddressBook = { addresses: [] }; }); describe('Constructor', function() { it('should create account with loyalty program data', function() { var account = new AccountModel(mockCustomer, mockAddressBook, {}); assert.isNotNull(account.loyaltyProgram); assert.equal(account.loyaltyProgram.number, 'LP123456'); assert.equal(account.loyaltyProgram.tier, 'Gold'); assert.equal(account.loyaltyProgram.points, 1500); }); it('should handle customer without loyalty program', function() { delete mockCustomer.profile.custom; var account = new AccountModel(mockCustomer, mockAddressBook, {}); assert.isNull(account.loyaltyProgram); }); }); }); ``` ## Performance Best Practices ### 1. Minimize API Calls ```javascript // ❌ Bad: Multiple API calls in loop function getBadProductData(productIds) { var products = []; for (var i = 0; i < productIds.length; i++) { var product = ProductMgr.getProduct(productIds[i]); // API call per product products.push(transformProduct(product)); } return products; } // ✅ Good: Single API call with batch processing function getGoodProductData(productIds) { var ProductSearchModel = require('dw/catalog/ProductSearchModel'); var searchModel = new ProductSearchModel(); searchModel.setProductIDs(productIds); searchModel.search(); var products = []; var productHits = searchModel.getProductSearchHits(); while (productHits.hasNext()) { var productHit = productHits.next(); products.push(transformProduct(productHit.product)); } return products; } ``` ### 2. Lazy Loading Properties ```javascript 'use strict'; /** * Product Model with Lazy Loading */ function Product(apiProduct) { this.id = apiProduct.ID; this.name = apiProduct.name; // Lazy load expensive properties Object.defineProperty(this, 'recommendations', { get: function() { if (!this._recommendations) { this._recommendations = getProductRecommendations(apiProduct); } return this._recommendations; }, enumerable: true }); } ``` ### 3. Efficient Data Transformation ```javascript 'use strict'; var collections = require('*/cartridge/scripts/util/collections'); /** * Transform collection efficiently using collections.map */ function transformProductLineItems(lineItems) { return collections.map(lineItems, function(lineItem) { return { productID: lineItem.productID, productName: lineItem.productName, price: lineItem.price.value, quantity: lineItem.quantity.value }; }); } /** * Filter and transform in single pass */ function getAvailableProducts(products) { return collections.map( collections.select(products, function(product) { return product.online && product.availabilityModel.available; }), function(product) { return transformProductTile(product); } ); } ``` ## Security Considerations ### 1. Data Sanitization ```javascript 'use strict'; var StringUtils = require('dw/util/StringUtils'); /** * Sanitize user-generated content in models */ function sanitizeCustomerData(customer) { return { firstName: StringUtils.encodeString(customer.profile.firstName, StringUtils.ENCODE_TYPE_HTML), lastName: StringUtils.encodeString(customer.profile.lastName, StringUtils.ENCODE_TYPE_HTML), email: customer.profile.email // Email validation handled elsewhere }; } /** * Remove sensitive data from model output */ function sanitizePaymentInstrument(paymentInstrument) { return { UUID: paymentInstrument.UUID, maskedCreditCardNumber: paymentInstrument.maskedCreditCardNumber, creditCardType: paymentInstrument.creditCardType, // Never expose: creditCardNumber, securityCode expirationMonth: paymentInstrument.expirationMonth, expirationYear: paymentInstrument.expirationYear }; } ``` ### 2. Access Control in Models ```javascript 'use strict'; /** * Customer Model with Privacy Controls */ function Customer(apiCustomer, viewingCustomer) { // Public information always available this.customerNumber = apiCustomer.customerNo; // Private information only for authenticated customer if (viewingCustomer && viewingCustomer.UUID === apiCustomer.UUID) { this.profile = getCustomerProfile(apiCustomer); this.addresses = getCustomerAddresses(apiCustomer); this.orders = getCustomerOrders(apiCustomer); } // Admin-only information if (viewingCustomer && hasAdminPermission(viewingCustomer)) { this.internalNotes = apiCustomer.custom.internalNotes; this.riskScore = apiCustomer.custom.riskScore; } } ``` ## Model Documentation Standards ### JSDoc Documentation ```javascript 'use strict'; /** * Enhanced Product Model * * @namespace ProductModel * @description Provides a comprehensive JSON representation of SFCC Product objects * with additional business logic for storefront display */ /** * Create enhanced product model * * @memberof ProductModel * @param {dw.catalog.Product} apiProduct - SFCC Product API object * @param {Object} options - Configuration options for model creation * @param {dw.catalog.ProductVariationModel} options.variationModel - Product variation model * @param {number} options.quantity - Selected quantity for pricing calculations * @param {dw.util.Collection<dw.campaign.Promotion>} options.promotions - Active promotions * @param {Object} options.variables - Additional variables from request * * @returns {Object} Enhanced product model with following structure: * @returns {string} returns.id - Product ID * @returns {string} returns.productName - Localized product name * @returns {string} returns.productType - Type: 'master', 'variant', 'simple', 'bundle', 'set' * @returns {Object} returns.price - Price information with sales/list prices * @returns {Array<Object>} returns.images - Product images in various sizes * @returns {Object} returns.availability - Availability and inventory information * @returns {Array<Object>} returns.variationAttributes - Available variation attributes * @returns {string} returns.longDescription - Full product description * @returns {Object} returns.custom - Custom product attributes * * @throws {Error} Throws error if apiProduct is null or undefined * * @since 1.0.0 * @version 2.1.0 - Added sustainability attributes */ function createEnhancedProduct(apiProduct, options) { // Implementation here } ``` ## Common Model Patterns & Anti-Patterns ### ✅ Good Patterns ```javascript // 1. Consistent constructor pattern function ModelName(apiObject, options) { this.property = transformProperty(apiObject.property); } // 2. Use Object.defineProperty for computed properties Object.defineProperty(this, 'computedProperty', { enumerable: true, value: calculateValue() }); // 3. Separate transformation logic into functions function transformPrice(apiPrice) { return { value: apiPrice.value, currency: apiPrice.currencyCode, formatted: StringUtils.formatMoney(apiPrice) }; } // 4. Use factory pattern for complex models var ProductFactory = { get: function(options) { return createProductModel(options); } }; ``` ### ❌ Anti-Patterns to Avoid ```javascript // 1. Don't put business logic in templates // ❌ Bad: Logic in ISML template <isif condition="${pdict.product.price.value > 100 && pdict.customer.tier === 'gold'}"> <!-- discount logic --> </isif> // ✅ Good: Logic in model function Product(apiProduct, customer) { this.showGoldDiscount = apiProduct.price.value > 100 && customer.tier === 'gold'; } // 2. Don't make API calls in model getters // ❌ Bad: API call in getter Object.defineProperty(this, 'recommendations', { get: function() { return apiProduct.getRecommendations(); // Expensive API call } }); // ✅ Good: Pre-compute in constructor or use caching function Product(apiProduct) { this.recommendations = getRecommendationsWithCache(apiProduct); } // 3. Don't expose API objects directly // ❌ Bad: Exposing API object this.product = apiProduct; // Exposes entire API object // ✅ Good: Extract needed properties this.id = apiProduct.ID; this.name = apiProduct.name; ``` ## Integration with Controllers ### Model Usage in Controllers ```javascript 'use strict'; var server = require('server'); var BasketMgr = require('dw/order/BasketMgr'); var CartModel = require('*/cartridge/models/cart'); var AccountModel = require('*/cartridge/models/account'); server.post('AddToCart', function (req, res, next) { var currentBasket = BasketMgr.getCurrentBasket(); var customer = req.currentCustomer; // Perform cart operations var result = addProductToCart(req.form.pid, req.form.quantity); if (result.success) { // Create models for response var cartModel = new CartModel(currentBasket); // Add customer-specific data if authenticated if (customer.authenticated) { var accountModel = new AccountModel(customer.raw); cartModel.customer = accountModel; } // Return JSON response with models res.json({ success: true, cart: cartModel, message: Resource.msg('cart.add.success', 'cart', null) }); } else { res.json({ success: false, error: result.error }); } next(); }); ``` This comprehensive guide covers all aspects of SFRA model development, from basic patterns to advanced techniques. Use the SFRA documentation tools available through this MCP server to explore existing models and understand their implementations before creating your own customizations. ``` -------------------------------------------------------------------------------- /docs/dw_customer/CustomerMgr.md: -------------------------------------------------------------------------------- ```markdown ## Package: dw.customer # Class CustomerMgr ## Inheritance Hierarchy - Object - dw.customer.CustomerMgr ## Description Provides helper methods for managing customers and customer profiles. Note: this class allows access to sensitive information through operations that retrieve the Profile object. Pay attention to appropriate legal and regulatory requirements related to this data. ## Properties ### customerGroups **Type:** Collection (Read Only) The customer groups of the current site. ### passwordConstraints **Type:** CustomerPasswordConstraints (Read Only) An instance of CustomerPasswordConstraints for the customer list assigned to the current site. ### registeredCustomerCount **Type:** Number (Read Only) The number of registered customers in the system. This number can be used for reporting purposes. ### siteCustomerList **Type:** CustomerList (Read Only) The customer list of the current site. ## Constructor Summary ## Method Summary ### authenticateCustomer **Signature:** `static authenticateCustomer(login : String, password : String) : AuthenticationStatus` This method authenticates a customer using the supplied login and password. ### createCustomer **Signature:** `static createCustomer(login : String, password : String) : Customer` Creates a new Customer using the supplied login, password. ### createCustomer **Signature:** `static createCustomer(login : String, password : String, customerNo : String) : Customer` Creates a new Customer using the supplied login, password, and a customerNo. ### createExternallyAuthenticatedCustomer **Signature:** `static createExternallyAuthenticatedCustomer(authenticationProviderId : String, externalId : String) : Customer` Given an authentication provider Id and an external Id: creates a Customer record in the system if one does not exist already for the same 'authenticationProviderId' and 'externalId' pair. ### describeProfileType **Signature:** `static describeProfileType() : ObjectTypeDefinition` Returns the meta data for profiles. ### getCustomerByCustomerNumber **Signature:** `static getCustomerByCustomerNumber(customerNumber : String) : Customer` Returns the customer with the specified customer number. ### getCustomerByLogin **Signature:** `static getCustomerByLogin(login : String) : Customer` Returns the customer for the specified login name. ### getCustomerByToken **Signature:** `static getCustomerByToken(token : String) : Customer` Returns the customer associated with the specified password reset token. ### getCustomerGroup **Signature:** `static getCustomerGroup(id : String) : CustomerGroup` Returns the customer group with the specified ID or null if group does not exists. ### getCustomerGroups **Signature:** `static getCustomerGroups() : Collection` Returns the customer groups of the current site. ### getCustomerList **Signature:** `static getCustomerList(id : String) : CustomerList` Returns the customer list identified by the specified ID. ### getExternallyAuthenticatedCustomerProfile **Signature:** `static getExternallyAuthenticatedCustomerProfile(authenticationProviderId : String, externalId : String) : Profile` Given an authentication provider Id and external Id returns the Customer Profile in our system. ### getPasswordConstraints **Signature:** `static getPasswordConstraints() : CustomerPasswordConstraints` Returns an instance of CustomerPasswordConstraints for the customer list assigned to the current site. ### getProfile **Signature:** `static getProfile(customerNumber : String) : Profile` Returns the profile with the specified customer number. ### getRegisteredCustomerCount **Signature:** `static getRegisteredCustomerCount() : Number` Returns the number of registered customers in the system. ### getSiteCustomerList **Signature:** `static getSiteCustomerList() : CustomerList` Returns the customer list of the current site. ### isAcceptablePassword **Signature:** `static isAcceptablePassword(password : String) : boolean` Checks if the given password matches the password constraints (for example password length) of the current site's assigned customerlist. ### isPasswordExpired **Signature:** `static isPasswordExpired(login : String) : boolean` Checks if the password for the given customer is expired ### loginCustomer **Signature:** `static loginCustomer(login : String, password : String, rememberMe : boolean) : Customer` This method authenticates the current session using the supplied login and password. ### loginCustomer **Signature:** `static loginCustomer(authStatus : AuthenticationStatus, rememberMe : boolean) : Customer` This method logs in the authenticated customer (from a previous authenticateCustomer() call). ### loginExternallyAuthenticatedCustomer **Signature:** `static loginExternallyAuthenticatedCustomer(authenticationProviderId : String, externalId : String, rememberMe : boolean) : Customer` Logs in externally authenticated customer if it has already been created in the system and the profile is not disabled or locked ### logoutCustomer **Signature:** `static logoutCustomer(rememberMe : boolean) : Customer` Logs out the customer currently logged into the storefront. ### processProfiles **Signature:** `static processProfiles(processFunction : Function, queryString : String, args : Object...) : void` Executes a user-definable function on a set of customer profiles. ### queryProfile **Signature:** `static queryProfile(queryString : String, args : Object...) : Profile` Searches for a single profile instance. ### queryProfiles **Signature:** `static queryProfiles(queryString : String, sortString : String, args : Object...) : SeekableIterator` Searches for profile instances. ### queryProfiles **Signature:** `static queryProfiles(queryAttributes : Map, sortString : String) : SeekableIterator` Searches for profile instances. ### removeCustomer **Signature:** `static removeCustomer(customer : Customer) : void` Logs out the supplied customer and deletes the customer record. ### removeCustomerTrackingData **Signature:** `static removeCustomerTrackingData(customer : Customer) : void` Removes (asynchronously) tracking data for this customer (from external systems or data stores). ### searchProfile **Signature:** `static searchProfile(queryString : String, args : Object...) : Profile` Searches for a single profile instance. ### searchProfiles **Signature:** `static searchProfiles(queryString : String, sortString : String, args : Object...) : SeekableIterator` Searches for profile instances. ### searchProfiles **Signature:** `static searchProfiles(queryAttributes : Map, sortString : String) : SeekableIterator` Searches for profile instances. ## Method Detail ## Method Details ### authenticateCustomer **Signature:** `static authenticateCustomer(login : String, password : String) : AuthenticationStatus` **Description:** This method authenticates a customer using the supplied login and password. It will not log in the customer into the current session, but returns only a status indicating success or failure (with different error codes for the failure cases). Upon successful authentication (status code 'AUTH_OK') the status object also holds the authenticated customer. To continue the login process, call the loginCustomer(AuthenticationStatus, boolean) method. This method verifies that the password for the customer is not expired. If it is expired the authentication will fail, with a status code of ERROR_PASSWORD_EXPIRED. This allows the storefront to require the customer to change the password, and then the login can proceed. **Parameters:** - `login`: Login name, must not be null. - `password`: Password, must not be null. **Returns:** the status of the authentication process --- ### createCustomer **Signature:** `static createCustomer(login : String, password : String) : Customer` **Description:** Creates a new Customer using the supplied login, password. The system automatically assigns a customer number based on the customer sequence numbers configured for the site or organization. The number is guaranteed to be unique, but is not guaranteed to be sequential. It can be higher or lower than a previously created number. As a result, sorting customers by customer number is not guaranteed to sort them in their order of creation. The method throws an exception if any of the following conditions are encountered: A Customer with the supplied Login already exists The Login is not acceptable. The Password is not acceptable. The system cannot create the Customer. A valid login name is between 1 and 256 characters in length (not counting leading or trailing whitespace), and may contain only the following characters: alphanumeric (Unicode letters or decimal digits) space period dash underscore @ Note: a storefront can be customized to provide further constraints on characters in a login name, but it cannot remove any constraints described above. If customers are created using this Script API call then any updated to the customer records should be done through Script API calls as well. The customer records created with Script API call should not be updated with OCAPI calls as the email validation is handled differently in these calls and may result in InvalidEmailException. **Parameters:** - `login`: The unique login name associated with the new customer and its profile, must not be null. If login is already in use, an exception will be thrown. - `password`: Customer plain customer password, which is encrypted before it is stored at the profile, must not be null. **Returns:** customer The new customer object. --- ### createCustomer **Signature:** `static createCustomer(login : String, password : String, customerNo : String) : Customer` **Description:** Creates a new Customer using the supplied login, password, and a customerNo. If the customerNo is not specified, the system automatically assigns a customer number based on the customer sequence numbers configured for the site or organization. An automatically assigned number is guaranteed to be unique, but is not guaranteed to be sequential. It can be higher or lower than a previously created number. As a result, sorting customers by customer number is not guaranteed to sort them in their order of creation. The method throws an exception if any of the following conditions are encountered: A Customer with the supplied Login already exists A Customer with the explicitly provided or calculated customer number already exists. The Login is not acceptable. The Password is not acceptable. The system cannot create the Customer. A valid login name is between 1 and 256 characters in length (not counting leading or trailing whitespace), and may contain only the following characters: alphanumeric (Unicode letters or decimal digits) space period dash underscore @ Note: a storefront can be customized to provide further constraints on characters in a login name, but it cannot remove any constraints described above. A valid CustomerNo is between 1 and 100 characters in length (not counting leading or trailing whitespace). Commerce Cloud Digital recommends that a CustomerNo only contain characters valid for URLs. If customers are created using this Script API call then any updated to the customer records should be done through Script API calls as well. The customer records created with Script API call should not be updated with OCAPI calls as the email validation is handled differently in these calls and may result in InvalidEmailException. **Parameters:** - `login`: The unique login name associated with the new customer and its profile, must not be null. If login is already in use, an exception will be thrown. - `password`: Customer plain customer password, which is encrypted before it is stored at the profile, must not be null. - `customerNo`: The unique customerNo can be null, the system will then automatically assign a new value. If provided explicitly, the system will make sure that no other customer uses the same value and will throw an exception otherwise. **Returns:** customer The new customer object. --- ### createExternallyAuthenticatedCustomer **Signature:** `static createExternallyAuthenticatedCustomer(authenticationProviderId : String, externalId : String) : Customer` **Description:** Given an authentication provider Id and an external Id: creates a Customer record in the system if one does not exist already for the same 'authenticationProviderId' and 'externalId' pair. If one already exists - it is returned. **Parameters:** - `authenticationProviderId`: the Id of the authentication provider as configured in Commerce Cloud Digital. - `externalId`: the Id of the customer at the authentication provider. Each authentication provider generates these in a different way, they are unique within their system **Returns:** On success: the created customer. On failure - null --- ### describeProfileType **Signature:** `static describeProfileType() : ObjectTypeDefinition` **Description:** Returns the meta data for profiles. **Returns:** the meta data for profiles. --- ### getCustomerByCustomerNumber **Signature:** `static getCustomerByCustomerNumber(customerNumber : String) : Customer` **Description:** Returns the customer with the specified customer number. If no customer with this customer number exists, null is returned. **Parameters:** - `customerNumber`: the customer number associated with the customer, must not be null. **Returns:** The customer if found, null otherwise --- ### getCustomerByLogin **Signature:** `static getCustomerByLogin(login : String) : Customer` **Description:** Returns the customer for the specified login name. If no customer with this login name exists, null is returned. **Parameters:** - `login`: the unique login name associated with the customer, must not be null. **Returns:** The customer if found, null otherwise --- ### getCustomerByToken **Signature:** `static getCustomerByToken(token : String) : Customer` **Description:** Returns the customer associated with the specified password reset token. A valid token is one that is associated with a customer record and is not expired. Such a token can be generated by Credentials.createResetPasswordToken(). If the passed token is valid, the associated customer is returned. Otherwise null is returned **Parameters:** - `token`: password reset token **Returns:** The customer associated with the token. Null if the token is invalid. --- ### getCustomerGroup **Signature:** `static getCustomerGroup(id : String) : CustomerGroup` **Description:** Returns the customer group with the specified ID or null if group does not exists. **Parameters:** - `id`: the customer group identifier. **Returns:** Customer group for ID or null --- ### getCustomerGroups **Signature:** `static getCustomerGroups() : Collection` **Description:** Returns the customer groups of the current site. **Returns:** Customer groups of current site. --- ### getCustomerList **Signature:** `static getCustomerList(id : String) : CustomerList` **Description:** Returns the customer list identified by the specified ID. Returns null if no customer list with the specified id exists. Note: Typically the ID of an automatically created customer list is equal to the ID of the site. **Parameters:** - `id`: The ID of the customer list. **Returns:** The CustomerList, or null if not found. --- ### getExternallyAuthenticatedCustomerProfile **Signature:** `static getExternallyAuthenticatedCustomerProfile(authenticationProviderId : String, externalId : String) : Profile` **Description:** Given an authentication provider Id and external Id returns the Customer Profile in our system. **Parameters:** - `authenticationProviderId`: the Id of the authentication provider as configured in Commerce Cloud Digital. - `externalId`: the Id of the customer at the authentication provider. Each authentication provider generates these in a different way, they are unique within their system **Returns:** The Profile of the customer if found, null otherwise --- ### getPasswordConstraints **Signature:** `static getPasswordConstraints() : CustomerPasswordConstraints` **Description:** Returns an instance of CustomerPasswordConstraints for the customer list assigned to the current site. **Returns:** customer password constraints for current site --- ### getProfile **Signature:** `static getProfile(customerNumber : String) : Profile` **Description:** Returns the profile with the specified customer number. **Parameters:** - `customerNumber`: the customer number of the customer of the to be retrieved profile **Returns:** Profile for specified customer number --- ### getRegisteredCustomerCount **Signature:** `static getRegisteredCustomerCount() : Number` **Description:** Returns the number of registered customers in the system. This number can be used for reporting purposes. **Returns:** the number of registered customers in the system. --- ### getSiteCustomerList **Signature:** `static getSiteCustomerList() : CustomerList` **Description:** Returns the customer list of the current site. **Returns:** The customer list assigned to the current site. --- ### isAcceptablePassword **Signature:** `static isAcceptablePassword(password : String) : boolean` **Description:** Checks if the given password matches the password constraints (for example password length) of the current site's assigned customerlist. **Parameters:** - `password`: the to be checked password **Returns:** true if the given password matches all required criteria --- ### isPasswordExpired **Signature:** `static isPasswordExpired(login : String) : boolean` **Description:** Checks if the password for the given customer is expired **Parameters:** - `login`: the login for the customer to be checked **Returns:** true if the password is expired --- ### loginCustomer **Signature:** `static loginCustomer(login : String, password : String, rememberMe : boolean) : Customer` **Description:** This method authenticates the current session using the supplied login and password. If a different customer is currently authenticated in the session, then this customer is "logged out" and her/his privacy and form data are deleted. If the authentication with the given credentials fails, then null is returned and no changes to the session are made. The authentication will be sucessful even when the password of the customer is already expired (according to the customer list settings). If the input value "RememberMe" is set to true, this method stores a cookie on the customer's machine which will be used to identify the customer when the next session is initiated. The cookie is set to expire in 180 days (i.e. 6 months). Note that a customer who is remembered is not automatically authenticated and will have to explicitly log in to access any personal information. **Deprecated:** use authenticateCustomer(login, password) and loginCustomer(authStatus, rememberMe) instead since they correctly check for expired passwords **Parameters:** - `login`: Login name, must not be null. - `password`: Password, must not be null. - `rememberMe`: Boolean value indicating if the customer wants to be remembered on the current computer. If a value of true is supplied a cookie identifying the customer is stored upon successful login. If a value of false, or a null value, is supplied, then no cookie is stored and any existing cookie is removed. **Returns:** Customer successfully authenticated customer. Null if the authentication with the given credentials fails. --- ### loginCustomer **Signature:** `static loginCustomer(authStatus : AuthenticationStatus, rememberMe : boolean) : Customer` **Description:** This method logs in the authenticated customer (from a previous authenticateCustomer() call). If a different customer is currently authenticated in the session, then this customer is "logged out" and all privacy-relevant data and all form data are deleted. If the previous authentication was not successful, then null is returned and no changes to the session are made. If the input value "RememberMe" is set to true, this method stores a cookie on the customer's machine which will be used to identify the customer when the next session is initiated. The cookie is set to expire in 180 days (i.e. 6 months). Note that a customer who is remembered is not automatically authenticated and will have to explicitly log in to access any personal information. **Parameters:** - `authStatus`: the authentication status from the previous authenticateCustomer call - `rememberMe`: Boolean value indicating if the customer wants to be remembered on the current computer. If a value of true is supplied a cookie identifying the customer is stored upon successful login. If a value of false, or a null value, is supplied, then no cookie is stored and any existing cookie is removed. **Returns:** Customer successfully authenticated customer. Null if the authentication status was not indicating success of the authentication. --- ### loginExternallyAuthenticatedCustomer **Signature:** `static loginExternallyAuthenticatedCustomer(authenticationProviderId : String, externalId : String, rememberMe : boolean) : Customer` **Description:** Logs in externally authenticated customer if it has already been created in the system and the profile is not disabled or locked **Parameters:** - `authenticationProviderId`: the Id of the authentication provider as configured in Commerce Cloud Digital. - `externalId`: the Id of the customer at the authentication provider. - `rememberMe`: whether to drop the remember me cookie **Returns:** Customer if found in the system and not disabled or locked. getExternallyAuthenticatedCustomerProfile(String, String) --- ### logoutCustomer **Signature:** `static logoutCustomer(rememberMe : boolean) : Customer` **Description:** Logs out the customer currently logged into the storefront. The boolean value "RememberMe" indicates, if the customer would like to be remembered on the current browser. If a value of true is supplied, the customer authentication state is set to "not logged in" and additionally the following session data is removed: the customer session private data, the form status data, dictionary information of interaction continue nodes, basket reference information, the secure token cookie. If the value is set to false or null, the complete session dictionary is cleaned up. The customer and anonymous cookie are removed and a new session cookie is set. **Parameters:** - `rememberMe`: Boolean value indicating if the customer wants to be remembered on the current browser. If a value of true is supplied, the customer authentication state is set to "not logged in" and additionally the following session data is removed: the customer session private data, the form status data, dictionary information of interaction continue nodes, basket reference information, the secure token cookie. If the value is set to false or null, the complete session dictionary is cleaned up. The customer and anonymous cookie are removed and a new session cookie is set. **Returns:** the new customer identity after logout. If rememberMe is true, null is returned. --- ### processProfiles **Signature:** `static processProfiles(processFunction : Function, queryString : String, args : Object...) : void` **Description:** Executes a user-definable function on a set of customer profiles. This method is intended to be used in batch processes and jobs, since it allows efficient processing of large result sets (which might take a while to process). First, a search with the given parameters is executed. Then the given function is executed once for each profile of the search result. The profile is handed over as the only parameter to this function. The search can be configured using a simple query language, which provides most common filter and operator functionality. For a description of this query language, see the queryProfile(String, Object...) method. The callback function will be supplied with a single argument of type 'Profile'. When the callback function defines additional arguments, they will be undefined when the function is called. When the callback function doesn't define any arguments at all, it will be called anyway (no error will happen, but the function won't get a profile as parameter). Error during execution of the callback function will be logged, and execution will continue with the next element from the result set. This method can be used as in this example (which counts the number of men): var count=0; function callback(profile: Profile) { count++; dw.system.Logger.debug("customer found: "+profile.customerNo) } CustomerMgr.processProfiles(callback, "gender=1"); dw.system.Logger.debug("found "+count+" men in customer list"); **Parameters:** - `processFunction`: the function to execute for each profile - `queryString`: the query string to use when searching for a profile. - `args`: the query string arguments. --- ### queryProfile **Signature:** `static queryProfile(queryString : String, args : Object...) : Profile` **Description:** Searches for a single profile instance. The search can be configured using a simple query language, which provides most common filter and operator functionality. The identifier for an attribute to use in a query condition is always the ID of the attribute as defined in the type definition. For custom defined attributes the prefix custom is required in the search term (e.g. custom.color = {1}), while for system attributes no prefix is used (e.g. name = {4}). Supported attribute value types with sample expression values: String 'String', 'Str*', 'Strin?' Integer 1, 3E4 Number 1.0, 3.99E5 Date yyyy-MM-dd e.g. 2007-05-31 (Default TimeZone = UTC) DateTime yyyy-MM-dd'T'hh:mm:ss+Z e.g. 2007-05-31T00:00+Z (Z TimeZone = UTC) or 2007-05-31T00:00:00 Boolean true, false Email '[email protected]', '*@demandware.com' Set of String 'String', 'Str*', 'Strin?' Set of Integer 1, 3E4 Set of Number 1.0, 3.99E5 Enum of String 'String', 'Str*', 'Strin?' Enum of Integer 1, 3E4 The following types of attributes are not queryable: Image HTML Text Quantity Password Note, that some system attributes are not queryable by default regardless of the actual value type code. The following operators are supported in a condition: = Equals - All types; supports NULL value (thumbnail = NULL) != Not equals - All types; supports NULL value (thumbnail != NULL) < Less than - Integer, Number and Date types only > Greater than - Integer, Number and Date types only <= Less or equals than - Integer, Number and Date types only >= Greater or equals than - Integer, Number and Date types only LIKE Like - String types and Email only; use if leading or trailing wildcards will be used to support substring search(custom.country LIKE 'US*') ILIKE Caseindependent Like - String types and Email only, use to support case insensitive query (custom.country ILIKE 'usa'), does also support wildcards for substring matching Conditions can be combined using logical expressions 'AND', 'OR' and 'NOT' and nested using parenthesis e.g. gender = {1} AND (age >= {2} OR (NOT profession LIKE {3})). The query language provides a placeholder syntax to pass objects as additional search parameters. Each passed object is related to a placeholder in the query string. The placeholder must be an Integer that is surrounded by braces. The first Integer value must be '0', the second '1' and so on, e.g. querySystemObjects("sample", "age = {0} or creationDate >= {1}", 18, date) If there is more than one object matching the specified query criteria, the result is not deterministic. In order to retrieve a single object from a sorted result set it is recommended to use the following code: queryProfiles("", "custom.myAttr asc", null).first(). The method first() returns only the next element and closes the iterator. This method is deprecated and will be removed in a future release. One of the following methods should be used instead: searchProfile(String, Object...), searchProfiles(Map, String) and searchProfiles(String, String, Object...) to search for customers and processProfiles(Function, String, Object...) to search and process customers in jobs. **Deprecated:** use searchProfile(String, Object...) instead. **Parameters:** - `queryString`: the query string to use when searching for a profile. - `args`: the query string arguments. **Returns:** the profile which was found when executing the queryString. --- ### queryProfiles **Signature:** `static queryProfiles(queryString : String, sortString : String, args : Object...) : SeekableIterator` **Description:** Searches for profile instances. The search can be configured using a simple query language, which provides most common filter and operator functionality. For a description of this query language, see the queryProfile(String, Object...) method. This method is deprecated and will be removed in a future release. One of the following methods should be used instead: searchProfile(String, Object...), searchProfiles(Map, String) and searchProfiles(String, String, Object...) to search for customers and processProfiles(Function, String, Object...) to search and process customers in jobs. **Deprecated:** use searchProfiles(String, String, Object...) instead. **Parameters:** - `queryString`: the actual query. - `sortString`: an optional sorting or null if no sorting is necessary. - `args`: optional parameters for the query string. **Returns:** SeekableIterator containing the result set of the query. --- ### queryProfiles **Signature:** `static queryProfiles(queryAttributes : Map, sortString : String) : SeekableIterator` **Description:** Searches for profile instances. The search can be configured with a map, which key-value pairs are converted into a query expression. The key-value pairs are turned into a sequence of '=' or 'like' conditions, which are combined with AND statements. Example: A map with the key/value pairs: 'name'/'tom*', 'age'/66 will be converted as follows: "name like 'tom*' and age = 66" The identifier for an attribute to use in a query condition is always the ID of the attribute as defined in the type definition. For custom defined attributes the prefix custom is required in the search term (e.g. custom.color = {1}), while for system attributes no prefix is used (e.g. name = {4}). Supported attribute value types with sample expression values: String 'String', 'Str*', 'Strin?' Integer 1, 3E4 Number 1.0, 3.99E5 Date yyyy-MM-dd e.g. 2007-05-31 (Default TimeZone = UTC) DateTime yyyy-MM-dd'T'hh:mm:ss+Z e.g. 2007-05-31T00:00+Z (Z TimeZone = UTC) or 2007-05-31T00:00:00 Boolean true, false Email '[email protected]', '*@demandware.com' Set of String 'String', 'Str*', 'Strin?' Set of Integer 1, 3E4 Set of Number 1.0, 3.99E5 Enum of String 'String', 'Str*', 'Strin?' Enum of Integer 1, 3E4 The following types of attributes are not queryable: Image HTML Text Quantity Password Note, that some system attributes are not queryable by default regardless of the actual value type code. The sorting parameter is optional and may contain a comma separated list of attribute names to sort by. Each sort attribute name may be followed by an optional sort direction specifier ('asc' | 'desc'). Default sorting directions is ascending, if no direction was specified. Example: age desc, name Please note that specifying a localized custom attribute as the sorting attribute is currently not supported. It is strongly recommended to call close() on the returned SeekableIterator if not all of its elements are being retrieved. This will ensure the proper cleanup of system resources. See SeekableIterator.close() This method is deprecated and will be removed in a future release. One of the following methods should be used instead: searchProfile(String, Object...), searchProfiles(Map, String) and searchProfiles(String, String, Object...) to search for customers and processProfiles(Function, String, Object...) to search and process customers in jobs. **Deprecated:** use searchProfiles(Map, String) instead. **Parameters:** - `queryAttributes`: key-value pairs that define the query. - `sortString`: an optional sorting or null if no sorting is necessary. **Returns:** SeekableIterator containing the result set of the query. --- ### removeCustomer **Signature:** `static removeCustomer(customer : Customer) : void` **Description:** Logs out the supplied customer and deletes the customer record. The customer must be a registered customer and the customer must currently be logged in. The customer must be logged in for security reasons to ensure that only the customer itself can remove itself from the system. While logout the customers session is reset to an anonymous session and, if present, the "Remember me" cookie of the customer is removed. Deleting the customer record includes the customer credentials, profile, address-book with all addresses, customer payment instruments, product lists and memberships in customer groups. Orders placed by this customer won't be deleted. If the supplied customer is not a registered customer or is not logged in, the API throws an exception **Parameters:** - `customer`: The customer to remove, must not be null. --- ### removeCustomerTrackingData **Signature:** `static removeCustomerTrackingData(customer : Customer) : void` **Description:** Removes (asynchronously) tracking data for this customer (from external systems or data stores). This will not remove the customer from the database, nor will it prevent tracking to start again in the future for this customer. The customer is identified by login / email /customerNo / cookie when its a registered customer, and by cookie when its an anonymous customer. **Parameters:** - `customer`: the customer --- ### searchProfile **Signature:** `static searchProfile(queryString : String, args : Object...) : Profile` **Description:** Searches for a single profile instance. The search can be configured using a simple query language, which provides most common filter and operator functionality. The identifier for an attribute to use in a query condition is always the ID of the attribute as defined in the type definition. For custom defined attributes the prefix custom is required in the search term (e.g. custom.color = {1}), while for system attributes no prefix is used (e.g. name = {4}). Supported attribute value types with sample expression values: String 'String', 'Str*', 'Strin?' Integer 1, 3E4 Number 1.0, 3.99E5 Date yyyy-MM-dd e.g. 2007-05-31 (Default TimeZone = UTC) DateTime yyyy-MM-dd'T'hh:mm:ss+Z e.g. 2007-05-31T00:00+Z (Z TimeZone = UTC) or 2007-05-31T00:00:00 Boolean true, false Email '[email protected]', '*@demandware.com' Set of String 'String', 'Str*', 'Strin?' Set of Integer 1, 3E4 Set of Number 1.0, 3.99E5 Enum of String 'String', 'Str*', 'Strin?' Enum of Integer 1, 3E4 The following types of attributes are not queryable: Image HTML Text Quantity Password Note, that some system attributes are not queryable by default regardless of the actual value type code. The following operators are supported in a condition: = Equals - All types; supports NULL value (thumbnail = NULL) != Not equals - All types; supports NULL value (thumbnail != NULL) < Less than - Integer, Number and Date types only > Greater than - Integer, Number and Date types only <= Less or equals than - Integer, Number and Date types only >= Greater or equals than - Integer, Number and Date types only LIKE Like - String types and Email only; use if leading or trailing wildcards will be used to support substring search(custom.country LIKE 'US*') ILIKE Caseindependent Like - String types and Email only, use to support case insensitive query (custom.country ILIKE 'usa'), does also support wildcards for substring matching Conditions can be combined using logical expressions 'AND', 'OR' and 'NOT' and nested using parenthesis e.g. gender = {1} AND (age >= {2} OR (NOT profession LIKE {3})). The query language provides a placeholder syntax to pass objects as additional search parameters. Each passed object is related to a placeholder in the query string. The placeholder must be an Integer that is surrounded by braces. The first Integer value must be '0', the second '1' and so on, e.g. querySystemObjects("sample", "age = {0} or creationDate >= {1}", 18, date) If there is more than one object matching the specified query criteria, the result is not deterministic. In order to retrieve a single object from a sorted result set it is recommended to use the following code: queryProfiles("", "custom.myAttr asc", null).first(). The method first() returns only the next element and closes the iterator. If the customer search API is configured to use the new Search Service, the following differences apply: newly created customers might not be found immediately via the search service, and changes to existing customers might also not be in effect immediately (there is a slight delay in updating the index) wildcards will be filtered from the query (*, %, +) and replaced by spaces LIKE and ILIKE queries will be executed as fulltext queries (working on whole words), not as substring searches LIKE queries will always be case insensitive using logical operators may change the execution of LIKE/ILIKE clauses to exact string comparison, depending on how they are combined using logical operators may result in degraded performance, depending on how they are combined **Parameters:** - `queryString`: the query string to use when searching for a profile. - `args`: the query string arguments. **Returns:** the profile which was found when executing the queryString. --- ### searchProfiles **Signature:** `static searchProfiles(queryString : String, sortString : String, args : Object...) : SeekableIterator` **Description:** Searches for profile instances. The search can be configured using a simple query language, which provides most common filter and operator functionality. For a description of this query language, see the searchProfile(String, Object...) method. If the customer search API is configured to use the new Search Service, the following differences apply: newly created customers might not be found immediately via the search service, and changes to existing customers might also not be in effect immediately (there is a slight delay in updating the index) the search will return only the first 1000 hits from the search result **Parameters:** - `queryString`: the actual query. - `sortString`: an optional sorting or null if no sorting is necessary. - `args`: optional parameters for the query string. **Returns:** SeekableIterator containing the result set of the query. --- ### searchProfiles **Signature:** `static searchProfiles(queryAttributes : Map, sortString : String) : SeekableIterator` **Description:** Searches for profile instances. The search can be configured with a map, which key-value pairs are converted into a query expression. The key-value pairs are turned into a sequence of '=' or 'like' conditions, which are combined with AND statements. Example: A map with the key/value pairs: 'name'/'tom*', 'age'/66 will be converted as follows: "name like 'tom*' and age = 66" The identifier for an attribute to use in a query condition is always the ID of the attribute as defined in the type definition. For custom defined attributes the prefix custom is required in the search term (e.g. custom.color = {1}), while for system attributes no prefix is used (e.g. name = {4}). Supported attribute value types with sample expression values: String 'String', 'Str*', 'Strin?' Integer 1, 3E4 Number 1.0, 3.99E5 Date yyyy-MM-dd e.g. 2007-05-31 (Default TimeZone = UTC) DateTime yyyy-MM-dd'T'hh:mm:ss+Z e.g. 2007-05-31T00:00+Z (Z TimeZone = UTC) or 2007-05-31T00:00:00 Boolean true, false Email '[email protected]', '*@demandware.com' Set of String 'String', 'Str*', 'Strin?' Set of Integer 1, 3E4 Set of Number 1.0, 3.99E5 Enum of String 'String', 'Str*', 'Strin?' Enum of Integer 1, 3E4 The following types of attributes are not queryable: Image HTML Text Quantity Password Note, that some system attributes are not queryable by default regardless of the actual value type code. The sorting parameter is optional and may contain a comma separated list of attribute names to sort by. Each sort attribute name may be followed by an optional sort direction specifier ('asc' | 'desc'). Default sorting directions is ascending, if no direction was specified. Example: age desc, name Please note that specifying a localized custom attribute as the sorting attribute is currently not supported. It is strongly recommended to call close() on the returned SeekableIterator if not all of its elements are being retrieved. This will ensure the proper cleanup of system resources. SeekableIterator.close() If the customer search API is configured to use the new Search Service, the following differences apply: newly created customers might not be found immediately via the search service, and changes to existing customers might also not be in effect immediately (there is a slight delay in updating the index) wildcards will be filtered from the query (*, %, +) and replaced by spaces LIKE and ILIKE queries will be executed as fulltext queries (working on whole words), not as substring searches LIKE queries will always be case insensitive using logical operators may change the execution of LIKE/ILIKE clauses to exact string comparison, depending on how they are combined using logical operators may result in degraded performance, depending on how they are combined the search will return only the first 1000 hits from the search result **Parameters:** - `queryAttributes`: key-value pairs that define the query. - `sortString`: an optional sorting or null if no sorting is necessary. **Returns:** SeekableIterator containing the result set of the query. --- ```