This is page 20 of 43. Use http://codebase.md/taurgis/sfcc-dev-mcp?lines=false&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/dw_order/ShippingOrder.md: -------------------------------------------------------------------------------- ```markdown ## Package: dw.order # Class ShippingOrder ## Inheritance Hierarchy - Object - dw.object.Extensible - dw.order.AbstractItemCtnr - dw.order.ShippingOrder ## Description A shipping order is used to specify items that should be shipped, and is typically exported to, and updated by a back-office warehouse management system. An Order can have n shipping orders expressing how the order is to be shipped. The creation, export and update of shipping orders is largely handled by custom logic in scripts by implementing ShippingOrderHooks. Use method Order.createShippingOrder() for creation and add items using createShippingOrderItem(OrderItem, Quantity) - each item is related to an order item which in turn represents a product- or shipping- line item in the order. A shipping order has a status calculated from its item status, one of CONFIRMED - shipping order not yet exported, with 0 items, or all items in status CONFIRMED. WAREHOUSE - shipping order exported, with all items in status WAREHOUSE. SHIPPED - exported shipping order has been updated, with 1-n items in status SHIPPED and 0-n CANCELLED. CANCELLED - exported shipping order has been updated, with all items in status CANCELLED. The following status transitions are supported. Every status transition is documented by the addition of an order note such as 'Shipping order 123456 status changed to WAREHOUSE.': From To When Use CONFIRMED WAREHOUSE Shipping order exported Call setStatusWarehouse() - note this is the only way to set the items to status WAREHOUSE WAREHOUSE SHIPPED One or more items have been SHIPPED Call ShippingOrderItem.setStatus(String) using ShippingOrderItem.STATUS_SHIPPED WAREHOUSE CANCELLED All items have been CANCELLED Call ShippingOrderItem.setStatus(String) using ShippingOrderItem.STATUS_CANCELLED Order post-processing APIs (gillian) are now inactive by default and will throw an exception if accessed. Activation needs preliminary approval by Product Management. Please contact support in this case. Existing customers using these APIs are not affected by this change and can use the APIs until further notice. ## Constants ### ORDERBY_ITEMID **Type:** Object Sorting by item id. Use with method getItems() as an argument to method FilteringCollection.sort(Object). ### ORDERBY_ITEMPOSITION **Type:** Object Sorting by the position of the related oder item. Use with method getItems() as an argument to method FilteringCollection.sort(Object). ### ORDERBY_UNSORTED **Type:** Object Unsorted , as it is. Use with method getItems() as an argument to method FilteringCollection.sort(Object). ### QUALIFIER_PRODUCTITEMS **Type:** Object Selects the product items. Use with method getItems() as an argument to method FilteringCollection.select(Object). ### QUALIFIER_SERVICEITEMS **Type:** Object Selects for the service items. Use with method getItems() as an argument to method FilteringCollection.select(Object). ### STATUS_CANCELLED **Type:** String = "CANCELLED" Constant for Shipping Order Status CANCELLED ### STATUS_CONFIRMED **Type:** String = "CONFIRMED" Constant for Shipping Order Status CONFIRMED ### STATUS_SHIPPED **Type:** String = "SHIPPED" Constant for Shipping Order Status SHIPPED ### STATUS_WAREHOUSE **Type:** String = "WAREHOUSE" Constant for Shipping Order Status WAREHOUSE ## Properties ### invoice **Type:** Invoice (Read Only) Returns null or the previously created Invoice. ### invoiceNumber **Type:** String (Read Only) Returns null or the invoice-number. ### items **Type:** FilteringCollection (Read Only) A filtering collection of the shipping order items belonging to the shipping order. This FilteringCollection could be sorted / filtered using: FilteringCollection.sort(Object) with ORDERBY_ITEMID FilteringCollection.sort(Object) with ORDERBY_ITEMPOSITION FilteringCollection.sort(Object) with ORDERBY_UNSORTED FilteringCollection.select(Object) with QUALIFIER_PRODUCTITEMS FilteringCollection.select(Object) with QUALIFIER_SERVICEITEMS ### shipDate **Type:** Date Gets the shipping date. Returns null if this shipping order is not yet shipped. ### shippingAddress **Type:** OrderAddress The shipping address (optional, can be null). Note: the shipping address is not copied into the ShippingOrder but is a link to a OrderAddress held in the Order. ### shippingMethod **Type:** ShippingMethod (Read Only) The shipping method of the shipping order. Can be null. ### shippingOrderNumber **Type:** String (Read Only) Gets the shipping order number. ### status **Type:** EnumValue (Read Only) Gets the status of this shipping order. The status is read-only and calculated from the item status. See class documentation for more details. The possible values are STATUS_CONFIRMED, STATUS_WAREHOUSE, STATUS_SHIPPED, STATUS_CANCELLED. ### trackingInfos **Type:** Collection (Read Only) Gets all tracking informations for this shipping order. ## Constructor Summary ## Method Summary ### addTrackingInfo **Signature:** `addTrackingInfo(trackingInfoID : String) : TrackingInfo` Adds a tracking info to this shipping order with the given ID. ### createInvoice **Signature:** `createInvoice() : Invoice` Creates a new Invoice based on this ShippingOrder. ### createInvoice **Signature:** `createInvoice(invoiceNumber : String) : Invoice` Creates a new Invoice based on this ShippingOrder. ### createShippingOrderItem **Signature:** `createShippingOrderItem(orderItem : OrderItem, quantity : Quantity) : ShippingOrderItem` Create a ShippingOrderItem in the shipping order with the number shippingOrderNumber. ### createShippingOrderItem **Signature:** `createShippingOrderItem(orderItem : OrderItem, quantity : Quantity, splitIfPartial : boolean) : ShippingOrderItem` Create a ShippingOrderItem in the shipping order with the number shippingOrderNumber. ### getInvoice **Signature:** `getInvoice() : Invoice` Returns null or the previously created Invoice. ### getInvoiceNumber **Signature:** `getInvoiceNumber() : String` Returns null or the invoice-number. ### getItems **Signature:** `getItems() : FilteringCollection` A filtering collection of the shipping order items belonging to the shipping order. ### getShipDate **Signature:** `getShipDate() : Date` Gets the shipping date. ### getShippingAddress **Signature:** `getShippingAddress() : OrderAddress` Returns the shipping address (optional, can be null). ### getShippingMethod **Signature:** `getShippingMethod() : ShippingMethod` Returns the shipping method of the shipping order. ### getShippingOrderNumber **Signature:** `getShippingOrderNumber() : String` Gets the shipping order number. ### getStatus **Signature:** `getStatus() : EnumValue` Gets the status of this shipping order. ### getTrackingInfo **Signature:** `getTrackingInfo(trackingInfoID : String) : TrackingInfo` Gets a tracking info for this shipping order. ### getTrackingInfos **Signature:** `getTrackingInfos() : Collection` Gets all tracking informations for this shipping order. ### setShipDate **Signature:** `setShipDate(date : Date) : void` Sets the shipping date. ### setShippingAddress **Signature:** `setShippingAddress(address : OrderAddress) : void` Set a shipping address for the shipping order. ### setShippingMethodID **Signature:** `setShippingMethodID(shippingMethodID : String) : void` Set the id of shipping method. ### setStatusWarehouse **Signature:** `setStatusWarehouse() : void` Set a CONFIRMED shipping order (all items in status CONFIRMED) to status WAREHOUSE (all items in status WAREHOUSE). Note - this method is the only way to transition a shipping order from CONFIRMED to WAREHOUSE. ## Method Detail ## Method Details ### addTrackingInfo **Signature:** `addTrackingInfo(trackingInfoID : String) : TrackingInfo` **Description:** Adds a tracking info to this shipping order with the given ID. **Parameters:** - `trackingInfoID`: the tracking info id **Returns:** the new tracking info **See Also:** TrackingInfo --- ### createInvoice **Signature:** `createInvoice() : Invoice` **Description:** Creates a new Invoice based on this ShippingOrder. The shipping-order-number will be used as the invoice-number. The Invoice can then be accessed using getInvoice() or getInvoiceNumber() can be used. The method must not be called more than once for a ShippingOrder, nor may 2 Invoices exist with the same invoice-number. The new Invoice is a debit-invoice with a status Invoice.STATUS_NOT_PAID, and will be passed to the capture payment-hook in a separate database transaction for processing. **Returns:** new invoice --- ### createInvoice **Signature:** `createInvoice(invoiceNumber : String) : Invoice` **Description:** Creates a new Invoice based on this ShippingOrder. The invoice-number must be specified as an argument.The Invoice can then be accessed using getInvoice() or getInvoiceNumber() can be used. The method must not be called more than once for a ShippingOrder, nor may 2 Invoices exist with the same invoice-number. The new Invoice is a debit-invoice with a status Invoice.STATUS_NOT_PAID, and will be passed to the capture payment-hook in a separate database transaction for processing. **Parameters:** - `invoiceNumber`: the invoice-number to use **Returns:** new invoice --- ### createShippingOrderItem **Signature:** `createShippingOrderItem(orderItem : OrderItem, quantity : Quantity) : ShippingOrderItem` **Description:** Create a ShippingOrderItem in the shipping order with the number shippingOrderNumber. The quantity of the new item can be optionally specified. A quantity of null indicates the new item should be based on the entire order item and is recommended for ShippingLineItems. If a quantity is specified for a ProductLineItem which is less than ProductLineItem.getQuantity() the ProductLineItem will be split, creating a new ProductLineItem. The new ShippingOrderItem will be associated with the new ProductLineItem, which will receive the specified quantity. See also createShippingOrderItem(OrderItem, Quantity, Boolean). **Parameters:** - `orderItem`: the order item for which the shipping order item is to be created - `quantity`: the quantity for which the shipping order item will be created **Returns:** the created shipping order item --- ### createShippingOrderItem **Signature:** `createShippingOrderItem(orderItem : OrderItem, quantity : Quantity, splitIfPartial : boolean) : ShippingOrderItem` **Description:** Create a ShippingOrderItem in the shipping order with the number shippingOrderNumber. The quantity of the new item can be optionally specified. A quantity of null indicates the new item should be based on the entire order item and is recommended for ShippingLineItems. If the specified quantity is less than ProductLineItem.getQuantity() the ProductLineItem will be split or not depending on splitIfPartial parameter. When split is true, the method is equivalent to createShippingOrderItem(OrderItem, Quantity). **Parameters:** - `orderItem`: the order item for which the shipping order item is to be created - `quantity`: the quantity for which the shipping order item will be created, not null - `splitIfPartial`: the flag whether ProductLineItem should be split when requested quantity is less than ProductLineItem's quantity **Returns:** the created shipping order item --- ### getInvoice **Signature:** `getInvoice() : Invoice` **Description:** Returns null or the previously created Invoice. **Returns:** null or the previously created invoice. **See Also:** createInvoice(String) --- ### getInvoiceNumber **Signature:** `getInvoiceNumber() : String` **Description:** Returns null or the invoice-number. **Returns:** null or the previously created invoice number. **See Also:** createInvoice(String) --- ### getItems **Signature:** `getItems() : FilteringCollection` **Description:** A filtering collection of the shipping order items belonging to the shipping order. This FilteringCollection could be sorted / filtered using: FilteringCollection.sort(Object) with ORDERBY_ITEMID FilteringCollection.sort(Object) with ORDERBY_ITEMPOSITION FilteringCollection.sort(Object) with ORDERBY_UNSORTED FilteringCollection.select(Object) with QUALIFIER_PRODUCTITEMS FilteringCollection.select(Object) with QUALIFIER_SERVICEITEMS **Returns:** the filtering collection of the shipping items. **See Also:** createShippingOrderItem(OrderItem, Quantity) ShippingOrderItem --- ### getShipDate **Signature:** `getShipDate() : Date` **Description:** Gets the shipping date. Returns null if this shipping order is not yet shipped. **Returns:** the shipping date or null --- ### getShippingAddress **Signature:** `getShippingAddress() : OrderAddress` **Description:** Returns the shipping address (optional, can be null). Note: the shipping address is not copied into the ShippingOrder but is a link to a OrderAddress held in the Order. **Returns:** the shipping address or null --- ### getShippingMethod **Signature:** `getShippingMethod() : ShippingMethod` **Description:** Returns the shipping method of the shipping order. Can be null. **Returns:** the shipping method or null --- ### getShippingOrderNumber **Signature:** `getShippingOrderNumber() : String` **Description:** Gets the shipping order number. **Returns:** the shipping order number --- ### getStatus **Signature:** `getStatus() : EnumValue` **Description:** Gets the status of this shipping order. The status is read-only and calculated from the item status. See class documentation for more details. The possible values are STATUS_CONFIRMED, STATUS_WAREHOUSE, STATUS_SHIPPED, STATUS_CANCELLED. **Returns:** the status --- ### getTrackingInfo **Signature:** `getTrackingInfo(trackingInfoID : String) : TrackingInfo` **Description:** Gets a tracking info for this shipping order. **Parameters:** - `trackingInfoID`: the tracking info id **Returns:** the tracking info or null **See Also:** TrackingInfo --- ### getTrackingInfos **Signature:** `getTrackingInfos() : Collection` **Description:** Gets all tracking informations for this shipping order. **Returns:** all tracking informations for this shipping order **See Also:** TrackingInfo --- ### setShipDate **Signature:** `setShipDate(date : Date) : void` **Description:** Sets the shipping date. **Parameters:** - `date`: the ship date --- ### setShippingAddress **Signature:** `setShippingAddress(address : OrderAddress) : void` **Description:** Set a shipping address for the shipping order. **Parameters:** - `address`: the shipping address **See Also:** getShippingAddress() --- ### setShippingMethodID **Signature:** `setShippingMethodID(shippingMethodID : String) : void` **Description:** Set the id of shipping method. **Parameters:** - `shippingMethodID`: the id of the shipping method **See Also:** ShippingMethod.getID() --- ### setStatusWarehouse **Signature:** `setStatusWarehouse() : void` **Description:** Set a CONFIRMED shipping order (all items in status CONFIRMED) to status WAREHOUSE (all items in status WAREHOUSE). Note - this method is the only way to transition a shipping order from CONFIRMED to WAREHOUSE. **Throws:** IllegalArgumentException - if the shipping order is in a status other than CONFIRMED. --- ``` -------------------------------------------------------------------------------- /tests/mcp/yaml/activate-code-version.full-mode.test.mcp.yml: -------------------------------------------------------------------------------- ```yaml # ================================================================================== # SFCC MCP Server - activate_code_version Tool YAML Tests (Full Mode) # Tests code version activation functionality with SFCC credentials # This tool provides code version management for deployment troubleshooting # # Test Coverage: # - Tool availability and definition validation in full mode # - Parameter validation (required codeVersionId, empty/null handling) # - Error responses for non-existent and invalid code versions # - Response structure validation for both success and error cases # - Performance requirements (under 2000ms for OCAPI calls) # - Edge cases (long IDs, special characters) # - Consistency across different invalid inputs # - SFCC fault information parsing # # Quick Test Commands: # aegis "tests/mcp/yaml/activate-code-version.full-mode.test.mcp.yml" --config "aegis.config.with-dw.json" --verbose # aegis query activate_code_version 'codeVersionId:test-version-001' --config "aegis.config.with-dw.json" # ================================================================================== description: "activate_code_version tool full mode tests - Core functionality validation" tests: # ================================================================================== # TOOL AVAILABILITY TESTS # ================================================================================== - it: "should list activate_code_version tool in full mode" request: jsonrpc: "2.0" id: "tool-availability-full" method: "tools/list" params: {} expect: response: jsonrpc: "2.0" id: "tool-availability-full" result: tools: match:arrayElements: match:partial: name: "match:type:string" description: "match:type:string" match:extractField: "tools.*.name" value: "match:arrayContains:activate_code_version" stderr: "toBeEmpty" - it: "should have correct tool definition in full mode" request: jsonrpc: "2.0" id: "tool-definition-full" method: "tools/list" params: {} expect: response: jsonrpc: "2.0" id: "tool-definition-full" result: tools: "match:arrayContains:name:activate_code_version" stderr: "toBeEmpty" - it: "should have proper tool schema definition" request: jsonrpc: "2.0" id: "tool-schema-validation" method: "tools/list" params: {} expect: response: jsonrpc: "2.0" id: "tool-schema-validation" result: tools: "match:arrayContains:name:activate_code_version" stderr: "toBeEmpty" # ================================================================================== # SETUP - RESET TO KNOWN STATE # ================================================================================== - it: "should reset to known state before testing" request: jsonrpc: "2.0" id: "reset-state" method: "tools/call" params: name: "activate_code_version" arguments: codeVersionId: "reset_version" expect: response: jsonrpc: "2.0" id: "reset-state" result: content: match:arrayElements: type: "text" text: "match:type:string" isError: false stderr: "toBeEmpty" # ================================================================================== # SUCCESSFUL ACTIVATION TESTS # ================================================================================== - it: "should successfully activate first test code version" request: jsonrpc: "2.0" id: "activate-success-1" method: "tools/call" params: name: "activate_code_version" arguments: codeVersionId: "test_activation" expect: response: jsonrpc: "2.0" id: "activate-success-1" result: content: match:arrayElements: type: "text" text: "match:contains:\"id\": \"test_activation\"" isError: false stderr: "toBeEmpty" performance: maxResponseTime: "2000ms" - it: "should successfully activate second test code version" request: jsonrpc: "2.0" id: "activate-success-2" method: "tools/call" params: name: "activate_code_version" arguments: codeVersionId: "simple_id" expect: response: jsonrpc: "2.0" id: "activate-success-2" result: content: match:arrayElements: type: "text" text: "match:contains:\"id\": \"simple_id\"" isError: false stderr: "toBeEmpty" performance: maxResponseTime: "2000ms" - it: "should successfully activate third test code version with dashes" request: jsonrpc: "2.0" id: "activate-success-3" method: "tools/call" params: name: "activate_code_version" arguments: codeVersionId: "version-with-dashes" expect: response: jsonrpc: "2.0" id: "activate-success-3" result: content: match:arrayElements: type: "text" text: "match:contains:\"id\": \"version-with-dashes\"" isError: false stderr: "toBeEmpty" performance: maxResponseTime: "2000ms" # ================================================================================== # RE-ACTIVATION FAILURE TEST # ================================================================================== - it: "should fail when attempting to re-activate currently active version" request: jsonrpc: "2.0" id: "reactivate-failure" method: "tools/call" params: name: "activate_code_version" arguments: codeVersionId: "version-with-dashes" expect: response: jsonrpc: "2.0" id: "reactivate-failure" result: content: match:arrayElements: type: "text" text: "match:contains:already active" isError: true stderr: "toBeEmpty" performance: maxResponseTime: "2000ms" # ================================================================================== # RESET TO KNOWN STATE (Final) # ================================================================================== - it: "should reset back to reset_version to clean up test state" request: jsonrpc: "2.0" id: "final-reset" method: "tools/call" params: name: "activate_code_version" arguments: codeVersionId: "reset_version" expect: response: jsonrpc: "2.0" id: "final-reset" result: content: match:arrayElements: type: "text" text: "match:contains:\"id\": \"reset_version\"" isError: false stderr: "toBeEmpty" performance: maxResponseTime: "2000ms" # ================================================================================== # PARAMETER VALIDATION TESTS # ================================================================================== - it: "should reject missing required parameter" request: jsonrpc: "2.0" id: "missing-param" method: "tools/call" params: name: "activate_code_version" arguments: {} expect: response: jsonrpc: "2.0" id: "missing-param" result: content: match:arrayElements: type: "text" text: "match:contains:codeVersionId must be a non-empty string" isError: true stderr: "toBeEmpty" performance: maxResponseTime: "1000ms" - it: "should reject empty codeVersionId" request: jsonrpc: "2.0" id: "empty-param" method: "tools/call" params: name: "activate_code_version" arguments: codeVersionId: "" expect: response: jsonrpc: "2.0" id: "empty-param" result: content: match:arrayElements: type: "text" text: "match:contains:codeVersionId must be a non-empty string" isError: true stderr: "toBeEmpty" performance: maxResponseTime: "1000ms" - it: "should reject null codeVersionId" request: jsonrpc: "2.0" id: "null-param" method: "tools/call" params: name: "activate_code_version" arguments: codeVersionId: null expect: response: jsonrpc: "2.0" id: "null-param" result: content: match:arrayElements: type: "text" text: "match:contains:codeVersionId must be a non-empty string" isError: true stderr: "toBeEmpty" performance: maxResponseTime: "1000ms" # ================================================================================== # BASIC FUNCTIONALITY TESTS (Using Non-Existent Code Version) # ================================================================================== - it: "should handle non-existent code version gracefully" request: jsonrpc: "2.0" id: "non-existent-version" method: "tools/call" params: name: "activate_code_version" arguments: codeVersionId: "test-version-nonexistent" expect: response: jsonrpc: "2.0" id: "non-existent-version" result: content: match:arrayElements: type: "text" text: "match:regex:Error[\\s\\S]*404[\\s\\S]*not found[\\s\\S]*" isError: true stderr: "toBeEmpty" performance: maxResponseTime: "2000ms" - it: "should provide meaningful error for invalid code version ID" request: jsonrpc: "2.0" id: "invalid-version-id" method: "tools/call" params: name: "activate_code_version" arguments: codeVersionId: "invalid@#$%^&*()" expect: response: jsonrpc: "2.0" id: "invalid-version-id" result: content: match:arrayElements: type: "text" text: "match:regex:Error[\\s\\S]*(404|not found|invalid)[\\s\\S]*" isError: true stderr: "toBeEmpty" performance: maxResponseTime: "2000ms" # ================================================================================== # RESPONSE STRUCTURE VALIDATION # ================================================================================== - it: "should return proper error response structure" request: jsonrpc: "2.0" id: "error-structure" method: "tools/call" params: name: "activate_code_version" arguments: codeVersionId: "test-nonexistent" expect: response: jsonrpc: "2.0" id: "error-structure" result: content: match:arrayElements: match:partial: type: "text" text: "match:type:string" isError: true stderr: "toBeEmpty" - it: "should include SFCC fault information in error response" request: jsonrpc: "2.0" id: "sfcc-fault-info" method: "tools/call" params: name: "activate_code_version" arguments: codeVersionId: "test-fault-info" expect: response: jsonrpc: "2.0" id: "sfcc-fault-info" result: content: match:arrayElements: type: "text" text: "match:regex:Error[\\s\\S]*(fault|InvalidParameterException|404)[\\s\\S]*" isError: true stderr: "toBeEmpty" # ================================================================================== # PERFORMANCE TESTS # ================================================================================== - it: "should meet performance requirements for error responses" request: jsonrpc: "2.0" id: "performance-error" method: "tools/call" params: name: "activate_code_version" arguments: codeVersionId: "test-performance" expect: response: jsonrpc: "2.0" id: "performance-error" result: content: match:arrayElements: type: "text" text: "match:contains:Error" isError: true stderr: "toBeEmpty" performance: maxResponseTime: "2000ms" # ================================================================================== # EDGE CASE TESTS # ================================================================================== - it: "should handle very long code version ID" request: jsonrpc: "2.0" id: "long-version-id" method: "tools/call" params: name: "activate_code_version" arguments: codeVersionId: "test-very-long-code-version-id-that-exceeds-normal-length-expectations-and-might-cause-issues" expect: response: jsonrpc: "2.0" id: "long-version-id" result: content: match:arrayElements: type: "text" text: "match:regex:Error[\\s\\S]*(404|not found)[\\s\\S]*" isError: true stderr: "toBeEmpty" performance: maxResponseTime: "2000ms" - it: "should handle special characters in code version ID" request: jsonrpc: "2.0" id: "special-chars" method: "tools/call" params: name: "activate_code_version" arguments: codeVersionId: "test-version-with-spaces and-chars" expect: response: jsonrpc: "2.0" id: "special-chars" result: content: match:arrayElements: type: "text" text: "match:regex:Error[\\s\\S]*(404|not found)[\\s\\S]*" isError: true stderr: "toBeEmpty" performance: maxResponseTime: "2000ms" # ================================================================================== # CONSISTENCY TESTS # ================================================================================== - it: "should return consistent error format across different invalid inputs" request: jsonrpc: "2.0" id: "consistent-errors-1" method: "tools/call" params: name: "activate_code_version" arguments: codeVersionId: "test-consistency-1" expect: response: jsonrpc: "2.0" id: "consistent-errors-1" result: content: match:arrayElements: match:partial: type: "text" isError: true stderr: "toBeEmpty" - it: "should return consistent error format for different invalid version" request: jsonrpc: "2.0" id: "consistent-errors-2" method: "tools/call" params: name: "activate_code_version" arguments: codeVersionId: "test-consistency-2" expect: response: jsonrpc: "2.0" id: "consistent-errors-2" result: content: match:arrayElements: match:partial: type: "text" isError: true stderr: "toBeEmpty" ``` -------------------------------------------------------------------------------- /src/clients/cartridge-generation-client.ts: -------------------------------------------------------------------------------- ```typescript /** * SFCC Cartridge Generation Client * * This client handles the generation of SFCC cartridge directory structures * with all necessary files and configurations, replacing the outdated sgmf-scripts * with a modern, integrated approach. */ import { Logger } from '../utils/logger.js'; import { IFileSystemService, IPathService } from '../services/index.js'; interface CartridgeGenerationOptions { cartridgeName: string; targetPath?: string; fullProjectSetup?: boolean; } interface CartridgeTemplates { packageJson: (cartridgeName: string) => object; dwJson: () => object; webpackConfig: (cartridgeName: string) => string; dotProject: (cartridgeName: string) => string; projectProperties: (cartridgeName: string) => string; eslintrc: () => object; stylelintrc: () => object; eslintignore: () => string; gitignore: () => string; } export class CartridgeGenerationClient { private logger: Logger; private templates: CartridgeTemplates; private fileSystem: IFileSystemService; private pathService: IPathService; constructor(fileSystem: IFileSystemService, pathService: IPathService) { this.logger = Logger.getChildLogger('CartridgeGenerationClient'); this.fileSystem = fileSystem; this.pathService = pathService; this.templates = this.initializeTemplates(); } /** * Normalize the target path by removing /cartridges or /cartridges/ from the end * The cartridge creation always happens from the root folder */ private normalizeTargetPath(targetPath: string): string { // Remove trailing slashes first let normalized = targetPath.replace(/\/+$/, ''); // Remove /cartridges from the end if present if (normalized.endsWith('/cartridges')) { normalized = normalized.slice(0, -11); // Remove '/cartridges' (11 characters) } this.logger.debug(`Normalized target path from '${targetPath}' to '${normalized}'`); return normalized; } /** * Generate a complete cartridge structure */ async generateCartridgeStructure(options: CartridgeGenerationOptions): Promise<{ success: boolean; message: string; createdFiles: string[]; createdDirectories: string[]; skippedFiles: string[]; }> { const { cartridgeName, targetPath, fullProjectSetup = true } = options; const createdFiles: string[] = []; const createdDirectories: string[] = []; const skippedFiles: string[] = []; try { this.logger.info(`Starting cartridge generation for: ${cartridgeName}`); // Determine the working directory and normalize path let workingDir = targetPath ?? process.cwd(); workingDir = this.normalizeTargetPath(workingDir); if (fullProjectSetup) { // Full project setup - create everything directly in the working directory this.logger.info(`Creating full project setup directly in: ${workingDir}`); // Ensure the working directory exists await this.ensureDirectory(workingDir); if (!createdDirectories.includes(workingDir)) { createdDirectories.push(workingDir); } // Create root files directly in working directory await this.createRootFiles(workingDir, cartridgeName, createdFiles, skippedFiles); // Create cartridge structure directly in working directory await this.createCartridgeStructure(workingDir, cartridgeName, createdFiles, createdDirectories, skippedFiles); return { success: true, message: `Successfully created full project setup for cartridge '${cartridgeName}' in '${workingDir}'`, createdFiles, createdDirectories, skippedFiles, }; } else { // Cartridge-only setup - add to existing project const cartridgesDir = this.pathService.join(workingDir, 'cartridges'); // Ensure cartridges directory exists await this.ensureDirectory(cartridgesDir); if (!createdDirectories.includes(cartridgesDir)) { createdDirectories.push(cartridgesDir); } // Create cartridge structure await this.createCartridgeStructure(workingDir, cartridgeName, createdFiles, createdDirectories, skippedFiles); return { success: true, message: `Successfully created cartridge '${cartridgeName}' in existing project at '${workingDir}'`, createdFiles, createdDirectories, skippedFiles, }; } } catch (error) { this.logger.error('Error generating cartridge structure:', error); return { success: false, message: `Failed to generate cartridge structure: ${error instanceof Error ? error.message : 'Unknown error'}`, createdFiles, createdDirectories, skippedFiles, }; } } /** * Create root project files (package.json, webpack, etc.) */ private async createRootFiles( projectDir: string, cartridgeName: string, createdFiles: string[], skippedFiles: string[], ): Promise<void> { // Create package.json const packageJsonPath = this.pathService.join(projectDir, 'package.json'); await this.safeWriteFile( packageJsonPath, JSON.stringify(this.templates.packageJson(cartridgeName), null, 2), createdFiles, skippedFiles, ); // Create dw.json const dwJsonPath = this.pathService.join(projectDir, 'dw.json'); await this.safeWriteFile( dwJsonPath, JSON.stringify(this.templates.dwJson(), null, 2), createdFiles, skippedFiles, ); // Create webpack.config.js const webpackPath = this.pathService.join(projectDir, 'webpack.config.js'); await this.safeWriteFile( webpackPath, this.templates.webpackConfig(cartridgeName), createdFiles, skippedFiles, ); // Create .eslintrc.json const eslintrcPath = this.pathService.join(projectDir, '.eslintrc.json'); await this.safeWriteFile( eslintrcPath, JSON.stringify(this.templates.eslintrc(), null, 2), createdFiles, skippedFiles, ); // Create .stylelintrc.json const stylelintrcPath = this.pathService.join(projectDir, '.stylelintrc.json'); await this.safeWriteFile( stylelintrcPath, JSON.stringify(this.templates.stylelintrc(), null, 2), createdFiles, skippedFiles, ); // Create .eslintignore const eslintignorePath = this.pathService.join(projectDir, '.eslintignore'); await this.safeWriteFile( eslintignorePath, this.templates.eslintignore(), createdFiles, skippedFiles, ); // Create .gitignore const gitignorePath = this.pathService.join(projectDir, '.gitignore'); await this.safeWriteFile( gitignorePath, this.templates.gitignore(), createdFiles, skippedFiles, ); } /** * Create the cartridge directory structure */ private async createCartridgeStructure( baseDir: string, cartridgeName: string, createdFiles: string[], createdDirectories: string[], skippedFiles: string[], ): Promise<void> { // Create cartridges directory const cartridgesDir = this.pathService.join(baseDir, 'cartridges'); await this.ensureDirectory(cartridgesDir); createdDirectories.push(cartridgesDir); // Create specific cartridge directory const cartridgeDir = this.pathService.join(cartridgesDir, cartridgeName); await this.ensureDirectory(cartridgeDir); createdDirectories.push(cartridgeDir); // Create .project file const projectPath = this.pathService.join(cartridgeDir, '.project'); await this.safeWriteFile( projectPath, this.templates.dotProject(cartridgeName), createdFiles, skippedFiles, ); // Create cartridge subdirectory const cartridgeSubDir = this.pathService.join(cartridgeDir, 'cartridge'); await this.ensureDirectory(cartridgeSubDir); createdDirectories.push(cartridgeSubDir); // Create cartridge properties file const propertiesPath = this.pathService.join(cartridgeSubDir, `${cartridgeName}.properties`); await this.safeWriteFile( propertiesPath, this.templates.projectProperties(cartridgeName), createdFiles, skippedFiles, ); // Create directory structure const directories = [ 'controllers', 'models', 'templates', 'templates/default', 'templates/resources', 'client', 'client/default', 'client/default/js', 'client/default/scss', ]; for (const dir of directories) { const fullPath = this.pathService.join(cartridgeSubDir, dir); await this.ensureDirectory(fullPath); createdDirectories.push(fullPath); } } /** * Ensure a directory exists, create if it doesn't */ private async ensureDirectory(dirPath: string): Promise<void> { try { await this.fileSystem.access(dirPath); } catch { await this.fileSystem.mkdir(dirPath, { recursive: true }); this.logger.info(`Created directory: ${dirPath}`); } } /** * Safely write a file, skipping if it already exists */ private async safeWriteFile( filePath: string, content: string, createdFiles: string[], skippedFiles: string[], ): Promise<void> { try { await this.fileSystem.access(filePath); // File exists, skip it skippedFiles.push(filePath); this.logger.info(`Skipped existing file: ${filePath}`); } catch { // File doesn't exist, create it await this.fileSystem.writeFile(filePath, content); createdFiles.push(filePath); this.logger.info(`Created file: ${filePath}`); } } /** * Initialize all file templates */ private initializeTemplates(): CartridgeTemplates { return { packageJson: (cartridgeName: string) => ({ name: cartridgeName, version: '0.0.1', description: 'New overlay cartridge', main: 'index.js', scripts: { 'lint': 'npm run lint:css && npm run lint:js', 'lint:css': 'sgmf-scripts --lint css', 'lint:js': 'sgmf-scripts --lint js', 'lint:fix': 'eslint cartridges --fix', upload: 'sgmf-scripts --upload -- ', uploadCartridge: `sgmf-scripts --uploadCartridge ${cartridgeName}`, 'compile:js': 'sgmf-scripts --compile js', 'compile:scss': 'sgmf-scripts --compile css', }, devDependencies: { autoprefixer: '^10.4.14', bestzip: '^2.2.1', 'css-loader': '^6.0.0', 'css-minimizer-webpack-plugin': '^5.0.1', eslint: '^8.56.0', 'eslint-config-airbnb-base': '^15.0.0', 'eslint-config-prettier': '^9.1.0', 'eslint-plugin-import': '^2.29.0', 'mini-css-extract-plugin': '^2.7.6', 'postcss-loader': '^7.0.0', sass: '^1.69.7', 'sass-loader': '^13.3.2', 'sgmf-scripts': '^3.0.0', shx: '^0.3.4', stylelint: '^15.4.0', 'stylelint-config-standard-scss': '^11.0.0', 'webpack-remove-empty-scripts': '^1.0.4', }, browserslist: [ 'last 2 versions', 'ie >= 10', ], }), dwJson: () => ({ hostname: '', username: '', password: '', 'code-version': '', }), webpackConfig: (cartridgeName: string) => `'use strict'; var path = require('path'); var MiniCssExtractPlugin = require('mini-css-extract-plugin'); var CssMinimizerPlugin = require('css-minimizer-webpack-plugin'); var sgmfScripts = require('sgmf-scripts'); var RemoveEmptyScriptsPlugin = require('webpack-remove-empty-scripts'); module.exports = [{ mode: 'development', name: 'js', entry: sgmfScripts.createJsPath(), output: { path: path.resolve('./cartridges/${cartridgeName}/cartridge/static'), filename: '[name].js' } }, { mode: 'none', name: 'scss', entry: sgmfScripts.createScssPath(), output: { path: path.resolve('./cartridges/${cartridgeName}/cartridge/static') }, module: { rules: [{ test: /\\.scss$/, use: [{ loader: MiniCssExtractPlugin.loader, options: { esModule: false } }, { loader: 'css-loader', options: { url: false } }, { loader: 'postcss-loader', options: { postcssOptions: { plugins: [require('autoprefixer')] } } }, { loader: 'sass-loader', options: { implementation: require('sass'), sassOptions: { includePaths: [ path.resolve(path.resolve(process.cwd(), '../storefront-reference-architecture/node_modules/')), path.resolve(process.cwd(), '../storefront-reference-architecture/node_modules/flag-icons/sass') ] } } }] }] }, plugins: [ new RemoveEmptyScriptsPlugin(), new MiniCssExtractPlugin({ filename: '[name].css', chunkFilename: '[name].css' }) ], optimization: { minimizer: ['...', new CssMinimizerPlugin()] } }];`, dotProject: (cartridgeName: string) => `<?xml version="1.0" encoding="UTF-8"?> <projectDescription> <name>${cartridgeName}</name> <comment></comment> <projects> </projects> <buildSpec> <buildCommand> <name>com.demandware.studio.core.beehiveElementBuilder</name> <arguments> </arguments> </buildCommand> </buildSpec> <natures> <nature>com.demandware.studio.core.beehiveNature</nature> </natures> </projectDescription>`, projectProperties: (cartridgeName: string) => `## cartridge.properties for cartridge ${cartridgeName} #demandware.cartridges.${cartridgeName}.multipleLanguageStorefront=true`, eslintrc: () => ({ root: true, extends: 'airbnb-base/legacy', globals: { session: 'readonly', request: 'readonly', }, rules: { 'import/no-unresolved': 'off', indent: ['error', 4, { SwitchCase: 1, VariableDeclarator: 1 }], 'func-names': 'off', 'require-jsdoc': 'error', 'valid-jsdoc': ['error', { preferType: { Boolean: 'boolean', Number: 'number', object: 'Object', String: 'string', }, requireReturn: false, }], 'vars-on-top': 'off', 'global-require': 'off', 'no-shadow': ['error', { allow: ['err', 'callback'] }], 'max-len': 'off', 'no-plusplus': 'off', }, }), stylelintrc: () => ({ extends: 'stylelint-config-standard-scss', plugins: [ 'stylelint-scss', ], }), eslintignore: () => `node_modules/ cartridges/**/cartridge/static/ coverage/ doc/ bin/ codecept.conf.js`, gitignore: () => `node_modules/ cartridges/*/cartridge/static/ .DS_Store *.log npm-debug.log* yarn-debug.log* yarn-error.log* coverage/ .nyc_output/ .env dw.json`, }; } } ``` -------------------------------------------------------------------------------- /docs/dw_io/File.md: -------------------------------------------------------------------------------- ```markdown ## Package: dw.io # Class File ## Inheritance Hierarchy - Object - dw.io.File ## Description Represents a file resource accessible from scripting. As with java.io.File, a File is essentially an "abstract pathname" which may or may not denote an actual file on the file system. Methods createNewFile, mkdir, mkdirs, and remove are provided to actually manipulate physical files. File access is limited to certain virtual directories. These directories are a subset of those accessible through WebDAV. As a result of this restriction, pathnames must be one of the following forms: /TEMP(/...) /IMPEX(/...) /REALMDATA(/...) /CATALOGS/[Catalog Name](/...) /LIBRARIES/[Library Name](/...) Note, that these paths are analogous to the WebDAV URIs used to access the same directories. The files are stored in a shared file system where multiple processes could access the same file. The programmer has to make sure no more than one process writes to a file at a given time. This class provides other useful methods for listing the children of a directory and for working with zip files. Note: when this class is used with sensitive data, be careful in persisting sensitive information. For performance reasons no more than 100,000 files (regular files and directories) should be stored in a directory. ## Constants ### CATALOGS **Type:** String = "CATALOGS" Catalogs root directory. ### CUSTOMER_SNAPSHOTS **Type:** String = "CUSTOMERSNAPSHOTS" Customer snapshots root directory. ### CUSTOMERPI **Type:** String = "CUSTOMERPI" Customer Payment Instrument root directory. ### DYNAMIC **Type:** String = "DYNAMIC" Reserved for future use. ### IMPEX **Type:** String = "IMPEX" Import/export root directory. ### LIBRARIES **Type:** String = "LIBRARIES" Libraries root directory. ### REALMDATA **Type:** String = "REALMDATA" RealmData root directory. ### SEPARATOR **Type:** String = "/" The UNIX style '/' path separator, which must be used for files paths. ### STATIC **Type:** String = "STATIC" Static content root directory. ### TEMP **Type:** String = "TEMP" Temp root directory. ## Properties ### directory **Type:** boolean (Read Only) Indicates that this file is a directory. ### file **Type:** boolean (Read Only) Indicates if this file is a file. ### fullPath **Type:** String (Read Only) Return the full file path denoted by this File. This value will be the same regardless of which constructor was used to create this File. ### name **Type:** String (Read Only) The name of the file or directory denoted by this object. This is just the last name in the pathname's name sequence. If the pathname's name sequence is empty, then the empty string is returned. ### path **Type:** String (Read Only) The portion of the path relative to the root directory. ### rootDirectoryType **Type:** String (Read Only) The root directory type, e.g. "IMPEX" represented by this File. ## Constructor Summary File(absPath : String) Creates a File from the given absolute file path in the file namespace. File(rootDir : File, relPath : String) Creates a File given a root directory and a relative path. ## Method Summary ### copyTo **Signature:** `copyTo(file : File) : File` Copy a file. ### createNewFile **Signature:** `createNewFile() : boolean` Create file. ### exists **Signature:** `exists() : boolean` Indicates if the file exists. ### getFullPath **Signature:** `getFullPath() : String` Return the full file path denoted by this File. ### getName **Signature:** `getName() : String` Returns the name of the file or directory denoted by this object. ### getPath **Signature:** `getPath() : String` Returns the portion of the path relative to the root directory. ### getRootDirectory **Signature:** `static getRootDirectory(rootDir : String, args : String...) : File` Returns a File representing a directory for the specified root directory type. ### getRootDirectoryType **Signature:** `getRootDirectoryType() : String` Returns the root directory type, e.g. ### gunzip **Signature:** `gunzip(root : File) : void` Assumes this instance is a gzip file. ### gzip **Signature:** `gzip(outputZipFile : File) : void` GZip this instance into a new gzip file. ### isDirectory **Signature:** `isDirectory() : boolean` Indicates that this file is a directory. ### isFile **Signature:** `isFile() : boolean` Indicates if this file is a file. ### lastModified **Signature:** `lastModified() : Number` Return the time, in milliseconds, that this file was last modified. ### length **Signature:** `length() : Number` Return the length of the file in bytes. ### list **Signature:** `list() : String[]` Returns an array of strings naming the files and directories in the directory denoted by this object. ### listFiles **Signature:** `listFiles() : List` Returns an array of File objects in the directory denoted by this File. ### listFiles **Signature:** `listFiles(filter : Function) : List` Returns an array of File objects denoting the files and directories in the directory denoted by this object that satisfy the specified filter. ### md5 **Signature:** `md5() : String` Returns an MD5 hash of the content of the file of this instance. ### mkdir **Signature:** `mkdir() : boolean` Creates a directory. ### mkdirs **Signature:** `mkdirs() : boolean` Creates a directory, including, its parent directories, as needed. ### remove **Signature:** `remove() : boolean` Deletes the file or directory denoted by this object. ### renameTo **Signature:** `renameTo(file : File) : boolean` Rename file. ### unzip **Signature:** `unzip(root : File) : void` Assumes this instance is a zip file. ### zip **Signature:** `zip(outputZipFile : File) : void` Zip this instance into a new zip file. ## Constructor Detail ## Method Detail ## Method Details ### copyTo **Signature:** `copyTo(file : File) : File` **Description:** Copy a file. Directories cannot be copied. This method cannot be used from storefront requests. **Parameters:** - `file`: the File object to copy to **Returns:** a reference to the copied file. **Throws:** IOException - if there is an interruption during file copy. FileAlreadyExistsException - if the file to copy to already exists UnsupportedOperationException - if invoked from a storefront request --- ### createNewFile **Signature:** `createNewFile() : boolean` **Description:** Create file. **Returns:** boolean, true - if file has been created, false - file already exists **Throws:** - Exception --- ### exists **Signature:** `exists() : boolean` **Description:** Indicates if the file exists. **Returns:** true if file exists, false otherwise. --- ### getFullPath **Signature:** `getFullPath() : String` **Description:** Return the full file path denoted by this File. This value will be the same regardless of which constructor was used to create this File. **Returns:** the full file path. --- ### getName **Signature:** `getName() : String` **Description:** Returns the name of the file or directory denoted by this object. This is just the last name in the pathname's name sequence. If the pathname's name sequence is empty, then the empty string is returned. **Returns:** The name of the file or directory denoted by this object. --- ### getPath **Signature:** `getPath() : String` **Description:** Returns the portion of the path relative to the root directory. **Deprecated:** Use getFullPath() to access the full path. This method does not return the correct path for files in the CATALOGS or LIBRARIES virtual directories. **Returns:** the relative file path, possibly blank but not null. --- ### getRootDirectory **Signature:** `static getRootDirectory(rootDir : String, args : String...) : File` **Description:** Returns a File representing a directory for the specified root directory type. If the root directory type is CATALOGS or LIBRARIES, then an additional argument representing the specific catalog or library must be provided. Otherwise, no additional arguments are needed. **Parameters:** - `rootDir`: root directory type (see the constants defined in this class) - `args`: root directory specific arguments **Returns:** File object representing the directory --- ### getRootDirectoryType **Signature:** `getRootDirectoryType() : String` **Description:** Returns the root directory type, e.g. "IMPEX" represented by this File. **Returns:** root directory type --- ### gunzip **Signature:** `gunzip(root : File) : void` **Description:** Assumes this instance is a gzip file. Unzipping it will explode the contents in the directory passed in (root). **Parameters:** - `root`: a File indicating root. root must be a directory. **Throws:** Exception - if the zip files contents can't be exploded. --- ### gzip **Signature:** `gzip(outputZipFile : File) : void` **Description:** GZip this instance into a new gzip file. If you're zipping a file, then a single entry, the instance, is included in the output gzip file. Note that a new File is created. GZipping directories is not supported. This file is never modified. **Parameters:** - `outputZipFile`: the zip file created. **Throws:** IOException - if the zip file can't be created. --- ### isDirectory **Signature:** `isDirectory() : boolean` **Description:** Indicates that this file is a directory. **Returns:** true if the file is a directory, false otherwise. --- ### isFile **Signature:** `isFile() : boolean` **Description:** Indicates if this file is a file. **Returns:** true if the file is a file, false otherwise. --- ### lastModified **Signature:** `lastModified() : Number` **Description:** Return the time, in milliseconds, that this file was last modified. **Returns:** the time, in milliseconds, that this file was last modified. --- ### length **Signature:** `length() : Number` **Description:** Return the length of the file in bytes. **Returns:** the file length in bytes. --- ### list **Signature:** `list() : String[]` **Description:** Returns an array of strings naming the files and directories in the directory denoted by this object. If this object does not denote a directory, then this method returns null. Otherwise an array of strings is returned, one for each file or directory in the directory. Names denoting the directory itself and the directory's parent directory are not included in the result. Each string is a file name rather than a complete path. There is no guarantee that the name strings in the resulting array will appear in any specific order; they are not, in particular, guaranteed to appear in alphabetical order. **Returns:** An array of strings naming the files and directories in the directory denoted by this File. The array will be empty if the directory is empty. Returns null if this File does not denote a directory. --- ### listFiles **Signature:** `listFiles() : List` **Description:** Returns an array of File objects in the directory denoted by this File. If this File does not denote a directory, then this method returns null. Otherwise an array of File objects is returned, one for each file or directory in the directory. Files denoting the directory itself and the directory's parent directory are not included in the result. There is no guarantee that the files in the resulting array will appear in any specific order; they are not, in particular, guaranteed to appear in alphabetical order. Example usage: // Assume "foo" is an accessible directory. var this_directory : dw.io.File = new File("foo"); // Find all files in directory foo, one level "down". // listFiles() will not traverse subdirectories. var folder : dw.util.List = this_directory.listFiles(); var first_element : dw.io.File = folder[0]; function modification_comparison(lhs : File, rhs : File) { return lhs.lastModified() < rhs.lastModified(); } function lexigraphic_comparison(lhs: File, rhs : File) { return lhs.getName() < rhs.getName(); } var time_ordered_folder : dw.util.ArrayList = folder.sort(modification_comparison); var alphabetic_folder : dw.util.ArrayList = folder.sort(lexigraphic_comparison); **Returns:** a list of File objects or null if this is not a directory. --- ### listFiles **Signature:** `listFiles(filter : Function) : List` **Description:** Returns an array of File objects denoting the files and directories in the directory denoted by this object that satisfy the specified filter. The behavior of this method is the same as that of the listFiles() method, except that the files in the returned array must satisfy the filter. The filter is a Javascript function which accepts one argument, a File, and returns true or false depending on whether the file meets the filter conditions. If the given filter is null then all files are accepted. Otherwise, a file satisfies the filter if and only if the filter returns true. Example usage: // Assume "foo" is an accessible directory. var this_directory : dw.io.File = new File("foo"); function longer_than_3(candidate : dw.io.File) { return candidate.getName().length > 3; } // Find all files in directory foo, one level "down", // such that the filename is longer than 3 characters. var folder_long_names : dw.util.List = this_directory.listFiles(longer_than_3); **Parameters:** - `filter`: a Javascript function which accepts a File argument and returns true or false. **Returns:** list of File objects or null if this is not a directory --- ### md5 **Signature:** `md5() : String` **Description:** Returns an MD5 hash of the content of the file of this instance. **Returns:** The MD5 hash of the file's content. **Throws:** Exception - if the file could not be read or is a directory. --- ### mkdir **Signature:** `mkdir() : boolean` **Description:** Creates a directory. **Returns:** true if file creation succeeded, false otherwise. --- ### mkdirs **Signature:** `mkdirs() : boolean` **Description:** Creates a directory, including, its parent directories, as needed. **Returns:** true if file creation succeeded, false otherwise. --- ### remove **Signature:** `remove() : boolean` **Description:** Deletes the file or directory denoted by this object. If this File represents a directory, then the directory must be empty in order to be deleted. **Returns:** true if file deletion succeeded, false otherwise --- ### renameTo **Signature:** `renameTo(file : File) : boolean` **Description:** Rename file. **Parameters:** - `file`: the File object to rename to **Returns:** boolean, true - if file rename succeeded, false - failed --- ### unzip **Signature:** `unzip(root : File) : void` **Description:** Assumes this instance is a zip file. Unzipping it will explode the contents in the directory passed in (root). **Parameters:** - `root`: a File indicating root. root must be a directory. **Throws:** Exception - if the zip files contents can't be exploded. --- ### zip **Signature:** `zip(outputZipFile : File) : void` **Description:** Zip this instance into a new zip file. If you're zipping a directory, the directory itself and all its children files to any level (any number of subdirectories) are included in the zip file. The directory will be the only entry in the archive (single root). If you're zipping a file, then a single entry, the instance, is included in the output zip file. Note that a new File is created. This file is never modified. **Parameters:** - `outputZipFile`: the zip file created. **Throws:** IOException - if the zip file can't be created. --- ``` -------------------------------------------------------------------------------- /docs/dw_system/Response.md: -------------------------------------------------------------------------------- ```markdown ## Package: dw.system # Class Response ## Inheritance Hierarchy - Object - dw.system.Response ## Description Represents an HTTP response in Commerce Cloud Digital. An instance of this class is implicitly available within Digital script under the variable "response". The Response object can be used to set cookies and specific HTTP headers, for directly accessing the output stream or for sending redirects. ## Constants ### ACCESS_CONTROL_ALLOW_CREDENTIALS **Type:** String = "Access-Control-Allow-Credentials" An allowed header name constant for Access-Control-Allow-Credentials ### ACCESS_CONTROL_ALLOW_HEADERS **Type:** String = "Access-Control-Allow-Headers" An allowed header name constant for Access-Control-Allow-Headers ### ACCESS_CONTROL_ALLOW_METHODS **Type:** String = "Access-Control-Allow-Methods" An allowed header name constant for Access-Control-Allow-Methods ### ACCESS_CONTROL_ALLOW_ORIGIN **Type:** String = "Access-Control-Allow-Origin" An allowed header name constant for Access-Control-Allow-Origin ### ACCESS_CONTROL_EXPOSE_HEADERS **Type:** String = "Access-Control-Expose-Headers" An allowed header name constant for Access-Control-Expose-Headers ### ALLOW **Type:** String = "Allow" An allowed header name constant for Allow ### CONTENT_DISPOSITION **Type:** String = "Content-Disposition" An allowed header name constant for Content-Disposition ### CONTENT_LANGUAGE **Type:** String = "Content-Language" An allowed header name constant for Content-Language ### CONTENT_LOCATION **Type:** String = "Content-Location" An allowed header name constant for Content-Location ### CONTENT_MD5 **Type:** String = "Content-MD5" An allowed header name constant for Content-MD5 ### CONTENT_SECURITY_POLICY **Type:** String = "Content-Security-Policy" An allowed header name constant for Content-Security-Policy. Note: The Commerce Cloud platform can override this header for tools like the Storefront Toolkit. ### CONTENT_SECURITY_POLICY_REPORT_ONLY **Type:** String = "Content-Security-Policy-Report-Only" An allowed header name constant for Content-Security-Policy-Report-Only. You can set this response header only for storefront requests. Report recipient can't be a B2C Commerce system. ### CONTENT_TYPE **Type:** String = "Content-Type" An allowed header name constant for Content-Type ### CROSS_ORIGIN_EMBEDDER_POLICY **Type:** String = "Cross-Origin-Embedder-Policy" An allowed header name constant for Cross-Origin-Embedder-Policy ### CROSS_ORIGIN_EMBEDDER_POLICY_REPORT_ONLY **Type:** String = "Cross-Origin-Embedder-Policy-Report-Only" An allowed header name constant for Cross-Origin-Embedder-Policy-Report-Only. You can set this response header only for storefront requests. Report recipient can't be a B2C Commerce system. ### CROSS_ORIGIN_OPENER_POLICY **Type:** String = "Cross-Origin-Opener-Policy" An allowed header name constant for Cross-Origin-Opener-Policy ### CROSS_ORIGIN_OPENER_POLICY_REPORT_ONLY **Type:** String = "Cross-Origin-Opener-Policy-Report-Only" An allowed header name constant for Cross-Origin-Opener-Policy-Report-Only. You can set this response header only for storefront requests. Report recipient can't be a B2C Commerce system. ### CROSS_ORIGIN_RESOURCE_POLICY **Type:** String = "Cross-Origin-Resource-Policy" An allowed header name constant for Cross-Origin-Resource-Policy ### LINK **Type:** String = "Link" An allowed header name constant for Link ### LOCATION **Type:** String = "Location" An allowed header name constant for Location ### PERMISSIONS_POLICY **Type:** String = "Permissions-Policy" An allowed header name constant for Permissions-Policy ### PLATFORM_FOR_PRIVACY_PREFERENCES_PROJECT **Type:** String = "P3P" An allowed header name constant for Platform for Privacy Preferences Project ### REFERRER_POLICY **Type:** String = "Referrer-Policy" An allowed header name constant for Referrer-Policy ### REFRESH **Type:** String = "Refresh" An allowed header name constant for Refresh ### RETRY_AFTER **Type:** String = "Retry-After" An allowed header name constant for Retry-After ### SERVICE_WORKER_ALLOWED **Type:** String = "service-worker-allowed" An allowed header name constant for service-worker-allowed ### VARY **Type:** String = "Vary" An allowed header name constant for Vary ### X_CONTENT_TYPE_OPTIONS **Type:** String = "X-Content-Type-Options" An allowed header name constant for X-Content-Type-Options ### X_FRAME_OPTIONS **Type:** String = "X-FRAME-OPTIONS" An allowed header name constant for X-FRAME-OPTIONS. Note: The Commerce Cloud platform can override this header for tools like the Storefront Toolkit. ### X_FRAME_OPTIONS_ALLOW_FROM **Type:** String = "ALLOW-FROM" An allowed value ALLOW-FROM for X-FRAME-OPTIONS ### X_FRAME_OPTIONS_DENY_VALUE **Type:** String = "DENY" An allowed value DENY for X-FRAME-OPTIONS ### X_FRAME_OPTIONS_SAMEORIGIN_VALUE **Type:** String = "SAMEORIGIN" An allowed value SAME-ORIGIN value for X-FRAME-OPTIONS ### X_ROBOTS_TAG **Type:** String = "X-Robots-Tag" An allowed header name constant for X-Robots-Tag ### X_XSS_PROTECTION **Type:** String = "X-XSS-Protection" An allowed header name constant for X-XSS-Protection ## Properties ### writer **Type:** PrintWriter (Read Only) A print writer which can be used to print content directly to the response. ## Constructor Summary ## Method Summary ### addHttpCookie **Signature:** `addHttpCookie(cookie : Cookie) : void` Adds the specified cookie to the outgoing response. ### addHttpHeader **Signature:** `addHttpHeader(name : String, value : String) : void` Adds a response header with the given name and value. ### containsHttpHeader **Signature:** `containsHttpHeader(name : String) : boolean` Checks whether the response message header has a field with the specified name. ### getWriter **Signature:** `getWriter() : PrintWriter` Returns a print writer which can be used to print content directly to the response. ### redirect **Signature:** `redirect(url : URL) : void` Sends a temporary redirect response (HTTP status 302) to the client for the specified redirect location URL. ### redirect **Signature:** `redirect(url : URL, status : Number) : void` Sends a redirect response with the given status to the client for the specified redirect location URL. ### redirect **Signature:** `redirect(location : String) : void` Sends a temporary redirect response (HTTP status 302) to the client for the specified redirect location URL. ### redirect **Signature:** `redirect(location : String, status : Number) : void` Sends a redirect response with the given status to the client for the specified redirect location URL. ### redirect **Signature:** `redirect(redirect : URLRedirect) : void` Sends a redirect response with the given status to the client for the specified redirect location URL. ### setBuffered **Signature:** `setBuffered(buffered : boolean) : void` Sets whether the output should be buffered or streamed directly to the client. ### setContentType **Signature:** `setContentType(contentType : String) : void` Sets the content type for this response. ### setExpires **Signature:** `setExpires(expires : Number) : void` Sets the cache expiration time for the response. ### setExpires **Signature:** `setExpires(expires : Date) : void` Convenience method for setExpires(Number) which takes a Date object. ### setHttpHeader **Signature:** `setHttpHeader(name : String, value : String) : void` Adds a response header with the given name and value. ### setStatus **Signature:** `setStatus(status : Number) : void` Sets the HTTP response code. ### setVaryBy **Signature:** `setVaryBy(varyBy : String) : void` Marks the response as personalized with the given variant identifier. ## Method Detail ## Method Details ### addHttpCookie **Signature:** `addHttpCookie(cookie : Cookie) : void` **Description:** Adds the specified cookie to the outgoing response. This method can be called multiple times to set more than one cookie. If a cookie with the same cookie name, domain and path is set multiple times for the same response, only the last set cookie with this name is sent to the client. This method can be used to set, update or delete cookies at the client. If the cookie doesn't exist at the client, it is set initially. If a cookie with the same name, domain and path already exists at the client, it is updated. A cookie can be deleted at the client by submitting a cookie with the maxAge attribute set to 0 (see Cookie.setMaxAge() for more information). Example, how a cookie can be deleted at the client: var cookie : Cookie = new Cookie("SomeName", "Simple Value"); cookie.setMaxAge(0); response.addHttpCookie(cookie); You can't set a cookie's SameSite attribute using the API. The server sets SameSite to None if either the developer sets the cookie's Secure flag or the global security preference Enforce HTTPS is enabled, in which case the Secure flag is also set. Otherwise, the server doesn't set the SameSite attribute and the browser uses its own default SameSite setting. The SameSite attribute is not sent with a cookie if the server detects that the client doesn't correctly interpret the attribute. **Parameters:** - `cookie`: a Cookie object --- ### addHttpHeader **Signature:** `addHttpHeader(name : String, value : String) : void` **Description:** Adds a response header with the given name and value. This method allows response headers to have multiple values. For public headers, only the names listed in the "Constants" section are allowed. Custom header names must begin with the prefix "X-SF-CC-" and can contain only alphanumeric characters, dash, and underscore. **Parameters:** - `name`: the name to use for the response header. - `value`: the value to use. --- ### containsHttpHeader **Signature:** `containsHttpHeader(name : String) : boolean` **Description:** Checks whether the response message header has a field with the specified name. **Parameters:** - `name`: the name to use. --- ### getWriter **Signature:** `getWriter() : PrintWriter` **Description:** Returns a print writer which can be used to print content directly to the response. --- ### redirect **Signature:** `redirect(url : URL) : void` **Description:** Sends a temporary redirect response (HTTP status 302) to the client for the specified redirect location URL. **Parameters:** - `url`: the URL object for the target location, must be not null --- ### redirect **Signature:** `redirect(url : URL, status : Number) : void` **Description:** Sends a redirect response with the given status to the client for the specified redirect location URL. **Parameters:** - `url`: the URL object with the redirect location, must be not null - `status`: the status code for this redirect, must be 301, 302 or 307 --- ### redirect **Signature:** `redirect(location : String) : void` **Description:** Sends a temporary redirect response (HTTP status 302) to the client for the specified redirect location URL. The target location must be a relative or an absolute URL. **Parameters:** - `location`: the target location as a string, must be not empty --- ### redirect **Signature:** `redirect(location : String, status : Number) : void` **Description:** Sends a redirect response with the given status to the client for the specified redirect location URL. **Parameters:** - `location`: the redirect location, must be not empty - `status`: the status code for this redirect, must be 301, 302 or 307 --- ### redirect **Signature:** `redirect(redirect : URLRedirect) : void` **Description:** Sends a redirect response with the given status to the client for the specified redirect location URL. **Parameters:** - `redirect`: the URLRedirect object with the location and status, must be not null --- ### setBuffered **Signature:** `setBuffered(buffered : boolean) : void` **Description:** Sets whether the output should be buffered or streamed directly to the client. By default, buffering is enabled. The mode can only be changed before anything has been written to the response. Switching buffering off and using streaming mode is recommended for sending large responses. **Parameters:** - `buffered`: if true, buffering is used, if false the response will be streamed --- ### setContentType **Signature:** `setContentType(contentType : String) : void` **Description:** Sets the content type for this response. This method may only be called before any output is written to the response. **Parameters:** - `contentType`: the MIME type of the content, like "text/html", "application/json" etc. --- ### setExpires **Signature:** `setExpires(expires : Number) : void` **Description:** Sets the cache expiration time for the response. The response will only be cached if caching was not disabled previously. By default, responses are not cached. This method can be called multiple times during request processing. If caching is enabled, the lowest expiration time, resulting from the invocations of the method becomes the cache expiration time. This is only used for HTTP requests. Streamed responses cannot be cached. This method is an alternative for setting the cache time using the <iscache> tag in ISML templates. **Parameters:** - `expires`: the expiration time in milliseconds since January 1, 1970, 00:00:00 GMT --- ### setExpires **Signature:** `setExpires(expires : Date) : void` **Description:** Convenience method for setExpires(Number) which takes a Date object. **Parameters:** - `expires`: a Date object. --- ### setHttpHeader **Signature:** `setHttpHeader(name : String, value : String) : void` **Description:** Adds a response header with the given name and value. If one or more value(s) have already been set, the new value overwrites the previous one. The containsHttpHeader(String) method can be used to test for the presence of a header before setting its value. For public headers, only the names listed in the "Constants" section are allowed. Custom header names must begin with the prefix "X-SF-CC-" and can contain only alphanumeric characters, dash, and underscore. **Parameters:** - `name`: the name to use for the response header. - `value`: the value to use. --- ### setStatus **Signature:** `setStatus(status : Number) : void` **Description:** Sets the HTTP response code. **Parameters:** - `status`: a standard-conform HTTP status code, for example 200 for "OK" --- ### setVaryBy **Signature:** `setVaryBy(varyBy : String) : void` **Description:** Marks the response as personalized with the given variant identifier. Commerce Cloud Digital identifies unique pages based on a combination of pricebook, promotion, sorting rule and A/B test segments, caches the different variants of the page, and then delivers the correct version to the user. If a page is personalized by means other than pricebook, promotion, sorting rule and A/B test, the page must not be cached, because the wrong variants of the page would be delivered to the user. For performance reasons, a page should only be marked as personalized if it really is. Otherwise, the performance can unnecessarily degrade. This method has the same effect as using <iscache varyby="price_promotion" /> tag in an ISML template. Once the vary-by value was set, either using this method or by the <iscache> tag in a template, the entire response is treated as personalized. **Parameters:** - `varyBy`: the variation criteria, currently only "price_promotion" is supported, any other value has no effect --- ``` -------------------------------------------------------------------------------- /docs/best-practices/ocapi_hooks.md: -------------------------------------------------------------------------------- ```markdown # Quick Guide: Salesforce B2C Commerce OCAPI Hooks This guide provides best practices and examples for implementing OCAPI hooks in Salesforce B2C Commerce Cloud. **IMPORTANT**: Before implementing OCAPI hooks, consult the **Performance and Stability Best Practices** guide from this MCP server. Pay special attention to the OCAPI-specific performance requirements and hook development guidelines to ensure optimal performance and avoid database-intensive operations. ## 1. Core Concepts OCAPI hooks are server-side extension points that allow you to inject custom B2C Commerce Script logic into the lifecycle of an OCAPI request. They are used to augment, validate, or modify the behavior of existing API endpoints. ### Hook Types & Execution Order There are three main hook types, executed in a specific order for state-changing requests (POST, PATCH, etc.): #### `before<HTTP_Method>` Executes before core platform logic. - **Use Case**: Input validation, data preprocessing, custom authorization checks. - **Context**: Runs within the database transaction. Can modify the incoming request document. #### `after<HTTP_Method>` Executes after core platform logic succeeds but before the response is generated. - **Use Case**: Business logic side effects, such as calling an external ERP/OMS, triggering basket recalculation, or saving data to custom objects. - **Context**: Runs within the same database transaction. Operates on persistent Script API objects (e.g., `dw.order.Basket`). #### `modify<HTTP_Method>Response` Executes last, after the platform generates the JSON response. - **Use Case**: Final formatting of the JSON response. Add, remove, or reformat attributes (especially `c_` custom attributes) before sending to the client. - **Context**: NOT transactional. Attempting to modify persistent data (e.g., `basket.setCustomerEmail()`) will throw an `ORMTransactionException`. | Hook Type | Transactional? | Can Modify Persistent Data? | Primary Purpose | |-----------|---------------|----------------------------|-----------------| | before | Yes | Yes | Validation & Preprocessing | | after | Yes | Yes | Business Logic & Side Effects | | modifyResponse | No | No | Formatting the JSON Response | ## 2. Registration Hooks are registered in a custom cartridge via two files. ### `package.json` (Cartridge Root) This file points to your hooks configuration file. ```json { "name": "my-hooks-cartridge", "hooks": "./cartridge/hooks.json" } ``` ### `hooks.json` (e.g., `/cartridge/hooks.json`) This file maps the official hook extension point name to your script file. ```json { "hooks": [ { "name": "dw.ocapi.shop.customer.address.beforePATCH", "script": "./customer/addressValidation.js" }, { "name": "dw.ocapi.shop.customer.modifyGETResponse", "script": "./customer/enrichCustomerResponse.js" }, { "name": "dw.ocapi.shop.order.afterPOST", "script": "./order/notifyOms.js" } ] } ``` ### Recommended Cartridge Structure Organize hook scripts by the resource they modify for better maintainability. ``` /cartridge └──hooks.json └──/scripts └──/hooks ├──/customer │ ├── addressValidation.js │ └── enrichCustomerResponse.js └──/order └── notifyOms.js ``` ## 3. Core Implementation Patterns ### Script Structure (CommonJS) Hook scripts are CommonJS modules. The function name must be exported and match the hook's method name (e.g., `afterPOST`). ```javascript 'use strict'; var Status = require('dw/system/Status'); /** * @param {dw.customer.Customer} customer - The customer object. * @param {Object} customerResponse - The response document to be modified. * @returns {dw.system.Status} - A status object. */ exports.modifyGETResponse = function (customer, customerResponse) { // Your logic here return new Status(Status.OK); }; ``` ### Signaling Success vs. Failure (`dw.system.Status`) Use the Status object to control the execution flow. - **Success**: `return new Status(Status.OK);` or `return void` (for Shop API hooks, void is often preferred to allow other hooks in the cartridge path to run). - **Controlled Failure**: `return new Status(Status.ERROR, 'ERROR_CODE', 'Descriptive message.');` This halts execution, rolls back the transaction, and returns an HTTP 400 error with a fault document containing your code and message. ### Data Integrity (`dw.system.Transaction`) All modifications to persistent data in `before` or `after` hooks must be wrapped in a transaction. ```javascript var Transaction = require('dw/system/Transaction'); Transaction.wrap(function () { customer.getProfile().custom.lastAddressChange = new Date(); }); ``` ## 4. Code Examples ### Example 1: Custom Validation (before hook) Reject an address update if the US postal code format is invalid. **Hook**: `dw.ocapi.shop.customer.address.beforePATCH` **Script**: `cartridge/scripts/hooks/customer/addressValidation.js` ```javascript 'use strict'; var Status = require('dw/system/Status'); exports.beforePATCH = function (customer, addressName, customerAddress) { var countryCode = customerAddress.country_code; var postalCode = customerAddress.postal_code; if (countryCode === 'US' && postalCode) { var postalCodeRegex = /^\d{5}(-\d{4})?$/; if (!postalCodeRegex.test(postalCode)) { // Reject the request with a specific error return new Status(Status.ERROR, 'INVALID_POSTAL_CODE', 'The postal code format is invalid for the United States.'); } } return new Status(Status.OK); }; ``` ### Example 2: Enriching a Response (modifyResponse hook) Add a custom flag `c_isPreferredCustomer` to the customer GET response. **Hook**: `dw.ocapi.shop.customer.modifyGETResponse` **Script**: `cartridge/scripts/hooks/customer/enrichCustomerResponse.js` ```javascript 'use strict'; var Status = require('dw/system/Status'); exports.modifyGETResponse = function (customer, customerResponse) { // Logic to determine if the customer is preferred var isPreferred = customer.isMemberOfCustomerGroup('Preferred'); // Add a custom attribute directly to the response document. // This does NOT save anything to the database. customerResponse.c_isPreferredCustomer = isPreferred; return new Status(Status.OK); }; ``` ### Example 3: External Service Integration (after hook) Notify an external Order Management System (OMS) after an order is created. **Hook**: `dw.ocapi.shop.order.afterPOST` **Script**: `cartridge/scripts/hooks/order/notifyOms.js` ```javascript 'use strict'; var Status = require('dw/system/Status'); var LocalServiceRegistry = require('dw/svc/LocalServiceRegistry'); var Logger = require('dw/system/Logger').getLogger('OmsIntegrationHook'); exports.afterPOST = function (order) { try { var omsService = LocalServiceRegistry.createService('oms.http.service', { /* service config */ }); var payload = { orderNo: order.getOrderNo(), total: order.getTotalGrossPrice().getValue() }; var result = omsService.call({ payload: payload }); if (!result.isOk()) { // Log the error for monitoring, but don't return Status.ERROR. // The order is already created; returning an error here would be misleading to the client. Logger.error('Failed to notify OMS for order {0}. Error: {1}', order.getOrderNo(), result.getErrorMessage()); } } catch (e) { Logger.error('Exception notifying OMS for order {0}. Exception: {1}', order.getOrderNo(), e.toString()); } // Always return OK because the primary operation (order creation) was successful. return new Status(Status.OK); }; ``` ## 5. Key Best Practices ### Performance - **DON'T** perform expensive database lookups inside a hook (e.g., `ProductMgr.getProduct()`). - **DO** be aware of caching. Hooks on cacheable GET endpoints only run on a cache miss. - **DO** keep hook logic simple and efficient. ### Security - **DO** treat all client input as untrusted. Sanitize and validate data in before hooks. - **DO** re-authorize sensitive actions within the hook. For example, use `OrderMgr.getOrder(orderNumber, orderToken)` instead of just `OrderMgr.getOrder(orderNumber)`. ### Error Handling & Resilience - **DO** wrap all hook logic in `try/catch` blocks. - **DO** use `dw.system.Logger` with custom categories and include the `request.requestID` for easy tracing. - **BE AWARE** of the Hook Circuit Breaker. If a hook fails more than 50% of the time in its last 100 executions, it will be disabled for 60 seconds, returning HTTP 503. ### Testing - **DO** use the `dw-api-mock` library to unit test hook logic locally in a Node.js environment. - **DO** use API clients like Postman for integration testing on a sandbox. ## 6. Comprehensive Hook Reference ### Shop API Hooks | API Endpoint (Method & Path) | Available Hook Extension Points | |------------------------------|----------------------------------| | **Authentication** | | | `POST /customers/auth` | `dw.ocapi.shop.auth.beforePOST`, `dw.ocapi.shop.auth.afterPOST`, `dw.ocapi.shop.auth.modifyPOSTResponse` | | **Basket** | | | `POST /baskets` | `dw.ocapi.shop.basket.beforePOST_v2`, `dw.ocapi.shop.basket.afterPOST`, `dw.ocapi.shop.basket.modifyPOSTResponse`, `dw.ocapi.shop.basket.validateBasket` | | `GET /baskets/{basket_id}` | `dw.ocapi.shop.basket.beforeGET`, `dw.ocapi.shop.basket.modifyGETResponse`, `dw.ocapi.shop.basket.validateBasket` | | `PATCH /baskets/{basket_id}` | `dw.ocapi.shop.basket.beforePATCH`, `dw.ocapi.shop.basket.afterPATCH`, `dw.ocapi.shop.basket.modifyPATCHResponse`, `dw.ocapi.shop.basket.validateBasket` | | `DELETE /baskets/{basket_id}` | `dw.ocapi.shop.basket.beforeDELETE`, `dw.ocapi.shop.basket.afterDELETE` | | `PUT /baskets/{basket_id}/billing_address` | `dw.ocapi.shop.basket.billing_address.beforePUT`, `dw.ocapi.shop.basket.billing_address.afterPUT`, `dw.ocapi.shop.basket.billing_address.modifyPUTResponse` | | `POST /baskets/{basket_id}/coupons` | `dw.ocapi.shop.basket.coupon.beforePOST`, `dw.ocapi.shop.basket.coupon.afterPOST`, `dw.ocapi.shop.basket.coupon.modifyPOSTResponse` | | `DELETE /baskets/{basket_id}/coupons/{coupon_item_id}` | `dw.ocapi.shop.basket.coupon.beforeDELETE`, `dw.ocapi.shop.basket.coupon.afterDELETE`, `dw.ocapi.shop.basket.coupon.modifyDELETEResponse` | | `POST /baskets/{basket_id}/items` | `dw.ocapi.shop.basket.items.beforePOST`, `dw.ocapi.shop.basket.items.afterPOST`, `dw.ocapi.shop.basket.items.modifyPOSTResponse` | | `POST /baskets/{basket_id}/payment_instruments` | `dw.ocapi.shop.basket.payment_instrument.beforePOST`, `dw.ocapi.shop.basket.payment_instrument.afterPOST`, `dw.ocapi.shop.basket.payment_instrument.modifyPOSTResponse` | | **Customer** | | | `POST /customers` | `dw.ocapi.shop.customer.beforePOST`, `dw.ocapi.shop.customer.afterPOST`, `dw.ocapi.shop.customer.modifyPOSTResponse` | | `GET /customers/{customer_id}` | `dw.ocapi.shop.customer.beforeGET`, `dw.ocapi.shop.customer.modifyGETResponse` | | `PATCH /customers/{customer_id}` | `dw.ocapi.shop.customer.beforePATCH`, `dw.ocapi.shop.customer.afterPATCH`, `dw.ocapi.shop.customer.modifyPATCHResponse` | | `POST /customers/{customer_id}/addresses` | `dw.ocapi.shop.customer.addresses.beforePOST`, `dw.ocapi.shop.customer.addresses.afterPOST`, `dw.ocapi.shop.customer.address.modifyPOSTResponse` | | `PATCH /customers/{customer_id}/addresses/{address_name}` | `dw.ocapi.shop.customer.address.beforePATCH`, `dw.ocapi.shop.customer.address.afterPATCH`, `dw.ocapi.shop.customer.address.modifyPATCHResponse` | | `DELETE /customers/{customer_id}/addresses/{address_name}` | `dw.ocapi.shop.customer.address.beforeDELETE`, `dw.ocapi.shop.customer.address.afterDELETE` | | **Order** | | | `POST /orders` | `dw.ocapi.shop.order.beforePOST`, `dw.ocapi.shop.order.afterPOST`, `dw.ocapi.shop.order.modifyPOSTResponse` | | `GET /orders/{order_no}` | `dw.ocapi.shop.order.beforeGET`, `dw.ocapi.shop.order.modifyGETResponse` | | `PATCH /orders/{order_no}` | `dw.ocapi.shop.order.beforePATCH`, `dw.ocapi.shop.order.afterPATCH`, `dw.ocapi.shop.order.modifyPATCHResponse` | | `POST /orders/{order_no}/payment_instruments` | `dw.ocapi.shop.order.payment_instrument.beforePOST`, `dw.ocapi.shop.order.payment_instrument.afterPOST`, `dw.ocapi.shop.order.payment_instrument.modifyPOSTResponse` | | **Product & Catalog** | | | `GET /products/{id}` | `dw.ocapi.shop.product.beforeGET`, `dw.ocapi.shop.product.modifyGETResponse` | | `GET /product_search` | `dw.ocapi.shop.product_search.beforeGET`, `dw.ocapi.shop.product_search.modifyGETResponse` | | `GET /categories/{id}` | `dw.ocapi.shop.category.beforeGET`, `dw.ocapi.shop.category.modifyGETResponse` | | `GET /content/{id}` | `dw.ocapi.shop.content.beforeGET`, `dw.ocapi.shop.content.modifyGETResponse` | ### Data API Hooks | API Endpoint (Method & Path) | Available Hook Extension Points | |------------------------------|----------------------------------| | **Custom Object** | | | `PUT /custom_objects/{object_type}/{key}` | `dw.ocapi.data.object.beforePut`, `dw.ocapi.data.object.afterPut` | | `PATCH /custom_objects/{object_type}/{key}` | `dw.ocapi.data.object.beforePatch`, `dw.ocapi.data.object.afterPatch` | | `DELETE /custom_objects/{object_type}/{key}` | `dw.ocapi.data.object.beforeDelete`, `dw.ocapi.data.object.afterDelete` | | **Customer** | | | `POST /customer_lists/{list_id}/customers` | `dw.ocapi.data.customer_list.customers.beforePOST`, `dw.ocapi.data.customer_list.customers.afterPOST` | | `PATCH /customer_lists/{list_id}/customers/{customer_no}` | `dw.ocapi.data.customer_list.customer.beforePATCH`, `dw.ocapi.data.customer_list.customer.afterPATCH` | | `POST /customer_lists/{list_id}/customers/{customer_no}/addresses` | `dw.ocapi.data.customer_list.customer.addresses.beforePOST`, `dw.ocapi.data.customer_list.customer.addresses.afterPOST` | | **Content** | | | `PUT /libraries/{library_id}/content/{content_id}` | `dw.ocapi.data.content.content.beforeCreate`, `dw.ocapi.data.content.content.afterCreate` | | `PATCH /libraries/{library_id}/content/{content_id}` | `dw.ocapi.data.content.content.beforeUpdate`, `dw.ocapi.data.content.content.afterUpdate` | | `DELETE /libraries/{library_id}/content/{content_id}` | `dw.ocapi.data.content.content.beforeDelete`, `dw.ocapi.data.content.content.afterDelete` | | **User** | | | `PATCH /users/this/password` | `dw.ocapi.data.users.afterPATCH` | ## Troubleshooting Hook Registration **If OCAPI hooks are not executing after deployment:** 1. **Check Code Version**: If hooks don't execute after upload: - **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) 2. **Verify Hook Registration**: Check logs for hook registration confirmations after version activation 3. **Test Hook Execution**: Make OCAPI calls to endpoints that should trigger your hooks and verify they execute 4. **Verify API Settings**: Ensure OCAPI settings in Business Manager allow your endpoints and include proper hook configurations **Common Hook Issues:** - Hooks not triggering → Check code version activation and OCAPI settings - Hook scripts not found → Verify file paths match registration in hooks.json - Runtime errors in hooks → Check logs for specific error messages during hook execution - Data API hooks → Ensure proper authentication and permissions are configured ``` -------------------------------------------------------------------------------- /docs/dw_extensions.pinterest/PinterestProduct.md: -------------------------------------------------------------------------------- ```markdown ## Package: dw.extensions.pinterest # Class PinterestProduct ## Inheritance Hierarchy - Object - dw.extensions.pinterest.PinterestProduct ## Description Represents a row in the Pinterest catalog feed export. ## Constants ### AVAILABILITY_IN_STOCK **Type:** String = "in Indicates that the product is in stock. ### AVAILABILITY_OUT_OF_STOCK **Type:** String = "out Indicates that the product is not in stock. ### AVAILABILITY_PREORDER **Type:** String = "preorder" Indicates that the product is availabile in preorder. ### CONDITION_NEW **Type:** String = "new" Indicates that the product has never been used. ### CONDITION_REFURBISHED **Type:** String = "refurbished" Indicates that the product has been used but refurbished. ### CONDITION_USED **Type:** String = "used" Indicates that the product has been used. ## Properties ### availability **Type:** String The availability of the Pinterest product. Possible values are AVAILABILITY_IN_STOCK or AVAILABILITY_OUT_OF_STOCK. ### brand **Type:** String The Pinterest brand of the product. ### color **Type:** String The Pinterest color value label of the product. ### colorHex **Type:** String The Pinterest color hex value of the product. ### colorImage **Type:** URL The URL of the image to show in Pinterest for the product color (swatch). ### condition **Type:** String The condition of the Pinterest product. Possible values are CONDITION_NEW, CONDITION_REFURBISHED, or CONDITION_USED. ### description **Type:** String The Pinterest description of the product. ### googleProductCategory **Type:** String The category of this product in the Google category taxonomy. ### gtin **Type:** String The Pinterest GTIN of the product. ### ID **Type:** String (Read Only) The ID of the Pinterest product. This is the same as the ID of the Demandware product. ### imageLinks **Type:** List A list containing the URLs of the image to show in Pinterest for the product. ### itemGroupID **Type:** String The ID of the Pinterest item group for the product, that is, its master product. ### itemGroupLink **Type:** URL The URL of the Pinterest item group for the product, that is, the link to its master product in the Demandware storefront. ### link **Type:** URL The URL of the Demandware storefront link to the product. ### maxPrice **Type:** Money The maximum price to show in Pinterest for the product. ### minPrice **Type:** Money The minimum price to show in Pinterest for the product. ### price **Type:** Money The price to show in Pinterest for the product. ### productCategory **Type:** String The Pinterest category path of the product. ### returnPolicy **Type:** String The Pinterest return policy of the product. ### size **Type:** String The Pinterest size value label of the product. ### title **Type:** String The Pinterest title of the product. ## Constructor Summary ## Method Summary ### getAvailability **Signature:** `getAvailability() : String` Returns the availability of the Pinterest product. ### getBrand **Signature:** `getBrand() : String` Returns the Pinterest brand of the product. ### getColor **Signature:** `getColor() : String` Returns the Pinterest color value label of the product. ### getColorHex **Signature:** `getColorHex() : String` Returns the Pinterest color hex value of the product. ### getColorImage **Signature:** `getColorImage() : URL` Returns the URL of the image to show in Pinterest for the product color (swatch). ### getCondition **Signature:** `getCondition() : String` Returns the condition of the Pinterest product. ### getDescription **Signature:** `getDescription() : String` Returns the Pinterest description of the product. ### getGoogleProductCategory **Signature:** `getGoogleProductCategory() : String` Returns the category of this product in the Google category taxonomy. ### getGtin **Signature:** `getGtin() : String` Returns the Pinterest GTIN of the product. ### getID **Signature:** `getID() : String` Returns the ID of the Pinterest product. ### getImageLinks **Signature:** `getImageLinks() : List` Returns a list containing the URLs of the image to show in Pinterest for the product. ### getItemGroupID **Signature:** `getItemGroupID() : String` Returns the ID of the Pinterest item group for the product, that is, its master product. ### getItemGroupLink **Signature:** `getItemGroupLink() : URL` Returns the URL of the Pinterest item group for the product, that is, the link to its master product in the Demandware storefront. ### getLink **Signature:** `getLink() : URL` Returns the URL of the Demandware storefront link to the product. ### getMaxPrice **Signature:** `getMaxPrice() : Money` Returns the maximum price to show in Pinterest for the product. ### getMinPrice **Signature:** `getMinPrice() : Money` Returns the minimum price to show in Pinterest for the product. ### getPrice **Signature:** `getPrice() : Money` Returns the price to show in Pinterest for the product. ### getProductCategory **Signature:** `getProductCategory() : String` Returns the Pinterest category path of the product. ### getReturnPolicy **Signature:** `getReturnPolicy() : String` Returns the Pinterest return policy of the product. ### getSize **Signature:** `getSize() : String` Returns the Pinterest size value label of the product. ### getTitle **Signature:** `getTitle() : String` Returns the Pinterest title of the product. ### setAvailability **Signature:** `setAvailability(availability : String) : void` Sets the availability of the Pinterest product. ### setBrand **Signature:** `setBrand(brand : String) : void` Sets the Pinterest brand of the product. ### setColor **Signature:** `setColor(color : String) : void` Sets the Pinterest color value label of the product. ### setColorHex **Signature:** `setColorHex(colorHex : String) : void` Sets the Pinterest color hex value of the product. ### setColorImage **Signature:** `setColorImage(colorImage : URL) : void` Sets the URL of the image to show in Pinterest for the product color (swatch). ### setCondition **Signature:** `setCondition(condition : String) : void` Sets the condition of the Pinterest product. ### setDescription **Signature:** `setDescription(description : String) : void` Sets the Pinterest description of the product. ### setGoogleProductCategory **Signature:** `setGoogleProductCategory(googleProductCategory : String) : void` Sets the category of this product in the Google category taxonomy. ### setGtin **Signature:** `setGtin(gtin : String) : void` Sets the Pinterest GTIN of the product. ### setImageLinks **Signature:** `setImageLinks(imageLinks : List) : void` Sets the list of URLs of images to show in Pinterest for the product. ### setItemGroupID **Signature:** `setItemGroupID(itemGroupID : String) : void` Sets the ID of the Pinterest item group for the product, that is, its master product. ### setItemGroupLink **Signature:** `setItemGroupLink(itemGroupLink : URL) : void` Sets the URL of the Pinterest item group for the product, that is, the link to its master product in the Demandware storefront. ### setLink **Signature:** `setLink(link : URL) : void` Sets the URL of the Demandware storefront link to the product. ### setMaxPrice **Signature:** `setMaxPrice(maxPrice : Money) : void` Sets the maximum price to show in Pinterest for the product. ### setMinPrice **Signature:** `setMinPrice(minPrice : Money) : void` Sets the minimum price to show in Pinterest for the product. ### setPrice **Signature:** `setPrice(price : Money) : void` Sets the price to show in Pinterest for the product. ### setProductCategory **Signature:** `setProductCategory(productCategory : String) : void` Sets the Pinterest category path of the product. ### setReturnPolicy **Signature:** `setReturnPolicy(returnPolicy : String) : void` Sets the Pinterest return policy of the product. ### setSize **Signature:** `setSize(size : String) : void` Sets the Pinterest size value label of the product. ### setTitle **Signature:** `setTitle(title : String) : void` Sets the Pinterest title of the product. ## Method Detail ## Method Details ### getAvailability **Signature:** `getAvailability() : String` **Description:** Returns the availability of the Pinterest product. Possible values are AVAILABILITY_IN_STOCK or AVAILABILITY_OUT_OF_STOCK. --- ### getBrand **Signature:** `getBrand() : String` **Description:** Returns the Pinterest brand of the product. --- ### getColor **Signature:** `getColor() : String` **Description:** Returns the Pinterest color value label of the product. --- ### getColorHex **Signature:** `getColorHex() : String` **Description:** Returns the Pinterest color hex value of the product. --- ### getColorImage **Signature:** `getColorImage() : URL` **Description:** Returns the URL of the image to show in Pinterest for the product color (swatch). --- ### getCondition **Signature:** `getCondition() : String` **Description:** Returns the condition of the Pinterest product. Possible values are CONDITION_NEW, CONDITION_REFURBISHED, or CONDITION_USED. --- ### getDescription **Signature:** `getDescription() : String` **Description:** Returns the Pinterest description of the product. --- ### getGoogleProductCategory **Signature:** `getGoogleProductCategory() : String` **Description:** Returns the category of this product in the Google category taxonomy. --- ### getGtin **Signature:** `getGtin() : String` **Description:** Returns the Pinterest GTIN of the product. --- ### getID **Signature:** `getID() : String` **Description:** Returns the ID of the Pinterest product. This is the same as the ID of the Demandware product. **Returns:** product ID --- ### getImageLinks **Signature:** `getImageLinks() : List` **Description:** Returns a list containing the URLs of the image to show in Pinterest for the product. --- ### getItemGroupID **Signature:** `getItemGroupID() : String` **Description:** Returns the ID of the Pinterest item group for the product, that is, its master product. --- ### getItemGroupLink **Signature:** `getItemGroupLink() : URL` **Description:** Returns the URL of the Pinterest item group for the product, that is, the link to its master product in the Demandware storefront. --- ### getLink **Signature:** `getLink() : URL` **Description:** Returns the URL of the Demandware storefront link to the product. --- ### getMaxPrice **Signature:** `getMaxPrice() : Money` **Description:** Returns the maximum price to show in Pinterest for the product. --- ### getMinPrice **Signature:** `getMinPrice() : Money` **Description:** Returns the minimum price to show in Pinterest for the product. --- ### getPrice **Signature:** `getPrice() : Money` **Description:** Returns the price to show in Pinterest for the product. --- ### getProductCategory **Signature:** `getProductCategory() : String` **Description:** Returns the Pinterest category path of the product. --- ### getReturnPolicy **Signature:** `getReturnPolicy() : String` **Description:** Returns the Pinterest return policy of the product. --- ### getSize **Signature:** `getSize() : String` **Description:** Returns the Pinterest size value label of the product. --- ### getTitle **Signature:** `getTitle() : String` **Description:** Returns the Pinterest title of the product. --- ### setAvailability **Signature:** `setAvailability(availability : String) : void` **Description:** Sets the availability of the Pinterest product. Possible values are AVAILABILITY_IN_STOCK or AVAILABILITY_OUT_OF_STOCK. **Parameters:** - `availability`: the availability status to set for this product --- ### setBrand **Signature:** `setBrand(brand : String) : void` **Description:** Sets the Pinterest brand of the product. **Parameters:** - `brand`: Pinterest brand --- ### setColor **Signature:** `setColor(color : String) : void` **Description:** Sets the Pinterest color value label of the product. **Parameters:** - `color`: Pinterest color value label --- ### setColorHex **Signature:** `setColorHex(colorHex : String) : void` **Description:** Sets the Pinterest color hex value of the product. **Parameters:** - `colorHex`: Pinterest color hex value --- ### setColorImage **Signature:** `setColorImage(colorImage : URL) : void` **Description:** Sets the URL of the image to show in Pinterest for the product color (swatch). **Parameters:** - `colorImage`: link to Pinterest color image --- ### setCondition **Signature:** `setCondition(condition : String) : void` **Description:** Sets the condition of the Pinterest product. Possible values are CONDITION_NEW, CONDITION_REFURBISHED, or CONDITION_USED. **Parameters:** - `condition`: the condition status to set for this product --- ### setDescription **Signature:** `setDescription(description : String) : void` **Description:** Sets the Pinterest description of the product. **Parameters:** - `description`: Pinterest description --- ### setGoogleProductCategory **Signature:** `setGoogleProductCategory(googleProductCategory : String) : void` **Description:** Sets the category of this product in the Google category taxonomy. **Parameters:** - `googleProductCategory`: Google product category --- ### setGtin **Signature:** `setGtin(gtin : String) : void` **Description:** Sets the Pinterest GTIN of the product. **Parameters:** - `gtin`: Pinterest GTIN --- ### setImageLinks **Signature:** `setImageLinks(imageLinks : List) : void` **Description:** Sets the list of URLs of images to show in Pinterest for the product. **Parameters:** - `imageLinks`: links to the product images --- ### setItemGroupID **Signature:** `setItemGroupID(itemGroupID : String) : void` **Description:** Sets the ID of the Pinterest item group for the product, that is, its master product. **Parameters:** - `itemGroupID`: ID of Pinterest item group --- ### setItemGroupLink **Signature:** `setItemGroupLink(itemGroupLink : URL) : void` **Description:** Sets the URL of the Pinterest item group for the product, that is, the link to its master product in the Demandware storefront. **Parameters:** - `itemGroupLink`: link to the Pinterest item group --- ### setLink **Signature:** `setLink(link : URL) : void` **Description:** Sets the URL of the Demandware storefront link to the product. **Parameters:** - `link`: Demandware storefront link to the product --- ### setMaxPrice **Signature:** `setMaxPrice(maxPrice : Money) : void` **Description:** Sets the maximum price to show in Pinterest for the product. **Parameters:** - `maxPrice`: Pinterest maximum price --- ### setMinPrice **Signature:** `setMinPrice(minPrice : Money) : void` **Description:** Sets the minimum price to show in Pinterest for the product. **Parameters:** - `minPrice`: Pinterest minimum price --- ### setPrice **Signature:** `setPrice(price : Money) : void` **Description:** Sets the price to show in Pinterest for the product. **Parameters:** - `price`: Pinterest price --- ### setProductCategory **Signature:** `setProductCategory(productCategory : String) : void` **Description:** Sets the Pinterest category path of the product. **Parameters:** - `productCategory`: Pinterest category path --- ### setReturnPolicy **Signature:** `setReturnPolicy(returnPolicy : String) : void` **Description:** Sets the Pinterest return policy of the product. **Parameters:** - `returnPolicy`: Pinterest return policy --- ### setSize **Signature:** `setSize(size : String) : void` **Description:** Sets the Pinterest size value label of the product. **Parameters:** - `size`: Pinterest size value label --- ### setTitle **Signature:** `setTitle(title : String) : void` **Description:** Sets the Pinterest title of the product. **Parameters:** - `title`: Pinterest title --- ``` -------------------------------------------------------------------------------- /tests/mcp/yaml/search-system-object-attribute-definitions.full-mode.test.mcp.yml: -------------------------------------------------------------------------------- ```yaml # ================================================================================== # SFCC MCP Server - search_system_object_attribute_definitions Tool YAML Tests (Full Mode) # Streamlined smoke testing and declarative validation for core functionality # Complex business logic, edge cases, and workflows are covered in programmatic tests # # Quick Test Commands: # aegis "tests/mcp/yaml/search-system-object-attribute-definitions.full-mode.test.mcp.yml" --config "aegis.config.with-dw.json" --verbose # aegis query search_system_object_attribute_definitions '{"objectType": "Product", "searchRequest": {"query": {"match_all_query": {}}, "count": 5}}' --config "aegis.config.with-dw.json" # ================================================================================== description: "search_system_object_attribute_definitions tool smoke tests - Basic functionality validation" tests: # ================================================================================== # TOOL AVAILABILITY VALIDATION # ================================================================================== - it: "should have search_system_object_attribute_definitions tool available with proper schema" request: jsonrpc: "2.0" id: "tool-available" method: "tools/list" params: {} expect: response: jsonrpc: "2.0" id: "tool-available" result: tools: match:arrayElements: match:partial: name: "match:type:string" description: "match:type:string" match:extractField: "tools.*.name" value: "match:arrayContains:search_system_object_attribute_definitions" stderr: "toBeEmpty" # ================================================================================== # CORE FUNCTIONALITY VALIDATION - match_all_query # ================================================================================== - it: "should successfully search Product attributes with match_all_query and return valid structure" request: jsonrpc: "2.0" id: "match-all-success" method: "tools/call" params: name: "search_system_object_attribute_definitions" arguments: objectType: "Product" searchRequest: query: match_all_query: {} count: 5 expect: response: jsonrpc: "2.0" id: "match-all-success" result: content: - type: "text" text: "match:contains:object_attribute_definition_search_result" isError: false performance: maxResponseTime: "2000ms" stderr: "toBeEmpty" - it: "should return valid JSON structure with pagination in match_all_query response" request: jsonrpc: "2.0" id: "match-all-structure" method: "tools/call" params: name: "search_system_object_attribute_definitions" arguments: objectType: "Product" searchRequest: query: match_all_query: {} count: 3 expect: response: jsonrpc: "2.0" id: "match-all-structure" result: content: - type: "text" text: "match:regex:[\\s\\S]*\"_type\"[\\s\\S]*\"object_attribute_definition_search_result\"[\\s\\S]*" isError: false stderr: "toBeEmpty" - it: "should include query echo and pagination info in match_all_query response" request: jsonrpc: "2.0" id: "match-all-metadata" method: "tools/call" params: name: "search_system_object_attribute_definitions" arguments: objectType: "Product" searchRequest: query: match_all_query: {} count: 5 start: 0 expect: response: jsonrpc: "2.0" id: "match-all-metadata" result: content: - type: "text" text: "match:regex:[\\s\\S]*\"query\"[\\s\\S]*\"match_all_query\"[\\s\\S]*\"start\"[\\s\\S]*\"total\"[\\s\\S]*" isError: false stderr: "toBeEmpty" # ================================================================================== # CORE FUNCTIONALITY VALIDATION - text_query # ================================================================================== - it: "should successfully search with text_query and return matching results" request: jsonrpc: "2.0" id: "text-query-success" method: "tools/call" params: name: "search_system_object_attribute_definitions" arguments: objectType: "Product" searchRequest: query: text_query: fields: ["id", "display_name"] search_phrase: "UPC" count: 3 expect: response: jsonrpc: "2.0" id: "text-query-success" result: content: - type: "text" text: "match:contains:object_attribute_definition_search_result" isError: false performance: maxResponseTime: "2000ms" stderr: "toBeEmpty" - it: "should echo text_query parameters in response" request: jsonrpc: "2.0" id: "text-query-echo" method: "tools/call" params: name: "search_system_object_attribute_definitions" arguments: objectType: "Product" searchRequest: query: text_query: fields: ["id", "display_name"] search_phrase: "UPC" expect: response: jsonrpc: "2.0" id: "text-query-echo" result: content: - type: "text" text: "match:regex:[\\s\\S]*\"text_query\"[\\s\\S]*\"fields\"[\\s\\S]*\"search_phrase\"[\\s\\S]*\"UPC\"[\\s\\S]*" isError: false stderr: "toBeEmpty" # ================================================================================== # CORE FUNCTIONALITY VALIDATION - term_query # ================================================================================== - it: "should successfully search with term_query using exact matching" request: jsonrpc: "2.0" id: "term-query-success" method: "tools/call" params: name: "search_system_object_attribute_definitions" arguments: objectType: "Product" searchRequest: query: term_query: fields: ["value_type"] operator: "is" values: ["string"] count: 5 expect: response: jsonrpc: "2.0" id: "term-query-success" result: content: - type: "text" text: "match:contains:object_attribute_definition_search_result" isError: false performance: maxResponseTime: "2000ms" stderr: "toBeEmpty" - it: "should echo term_query parameters correctly" request: jsonrpc: "2.0" id: "term-query-echo" method: "tools/call" params: name: "search_system_object_attribute_definitions" arguments: objectType: "Product" searchRequest: query: term_query: fields: ["value_type"] operator: "is" values: ["string"] expect: response: jsonrpc: "2.0" id: "term-query-echo" result: content: - type: "text" text: "match:regex:[\\s\\S]*\"term_query\"[\\s\\S]*\"fields\"[\\s\\S]*\"operator\"[\\s\\S]*\"is\"[\\s\\S]*\"values\"[\\s\\S]*" isError: false stderr: "toBeEmpty" # ================================================================================== # PAGINATION AND SORTING VALIDATION # ================================================================================== - it: "should handle pagination parameters correctly" request: jsonrpc: "2.0" id: "pagination-test" method: "tools/call" params: name: "search_system_object_attribute_definitions" arguments: objectType: "Product" searchRequest: query: match_all_query: {} start: 5 count: 3 expect: response: jsonrpc: "2.0" id: "pagination-test" result: content: - type: "text" text: "match:regex:[\\s\\S]*\"start\"[\\s\\S]*5[\\s\\S]*\"count\"[\\s\\S]*3[\\s\\S]*" isError: false performance: maxResponseTime: "2000ms" stderr: "toBeEmpty" - it: "should handle sorting parameters correctly" request: jsonrpc: "2.0" id: "sorting-test" method: "tools/call" params: name: "search_system_object_attribute_definitions" arguments: objectType: "Product" searchRequest: query: match_all_query: {} sorts: - field: "id" sort_order: "asc" count: 5 expect: response: jsonrpc: "2.0" id: "sorting-test" result: content: - type: "text" text: "match:contains:object_attribute_definition_search_result" isError: false performance: maxResponseTime: "2000ms" stderr: "toBeEmpty" # ================================================================================== # DIFFERENT OBJECT TYPES VALIDATION # ================================================================================== - it: "should handle Customer object type correctly" request: jsonrpc: "2.0" id: "customer-test" method: "tools/call" params: name: "search_system_object_attribute_definitions" arguments: objectType: "Customer" searchRequest: query: match_all_query: {} count: 3 expect: response: jsonrpc: "2.0" id: "customer-test" result: content: - type: "text" text: "match:contains:object_attribute_definition_search_result" isError: false performance: maxResponseTime: "2000ms" stderr: "toBeEmpty" - it: "should handle Order object type correctly" request: jsonrpc: "2.0" id: "order-test" method: "tools/call" params: name: "search_system_object_attribute_definitions" arguments: objectType: "Order" searchRequest: query: match_all_query: {} count: 3 expect: response: jsonrpc: "2.0" id: "order-test" result: content: - type: "text" text: "match:contains:object_attribute_definition_search_result" isError: false performance: maxResponseTime: "2000ms" stderr: "toBeEmpty" # ================================================================================== # PARAMETER VALIDATION # ================================================================================== - it: "should handle minimal parameters gracefully (only objectType)" request: jsonrpc: "2.0" id: "minimal-params" method: "tools/call" params: name: "search_system_object_attribute_definitions" arguments: objectType: "Product" expect: response: jsonrpc: "2.0" id: "minimal-params" result: content: - type: "text" text: "match:contains:object_attribute_definition_search_result" isError: false performance: maxResponseTime: "2000ms" stderr: "toBeEmpty" - it: "should handle invalid object type gracefully (returns empty results)" request: jsonrpc: "2.0" id: "invalid-object-type" method: "tools/call" params: name: "search_system_object_attribute_definitions" arguments: objectType: "NonExistentType" searchRequest: query: match_all_query: {} expect: response: jsonrpc: "2.0" id: "invalid-object-type" result: content: - type: "text" text: "match:contains:\"total\": 0" isError: false performance: maxResponseTime: "2000ms" stderr: "toBeEmpty" # ================================================================================== # PERFORMANCE VALIDATION # ================================================================================== - it: "should complete match_all_query operations within reasonable time" request: jsonrpc: "2.0" id: "perf-match-all" method: "tools/call" params: name: "search_system_object_attribute_definitions" arguments: objectType: "Product" searchRequest: query: match_all_query: {} count: 10 expect: response: jsonrpc: "2.0" id: "perf-match-all" result: content: - type: "text" text: "match:contains:object_attribute_definition_search_result" isError: false performance: maxResponseTime: "1500ms" stderr: "toBeEmpty" - it: "should complete text_query operations within reasonable time" request: jsonrpc: "2.0" id: "perf-text-query" method: "tools/call" params: name: "search_system_object_attribute_definitions" arguments: objectType: "Product" searchRequest: query: text_query: fields: ["id", "display_name", "description"] search_phrase: "brand" count: 5 expect: response: jsonrpc: "2.0" id: "perf-text-query" result: content: - type: "text" text: "match:contains:object_attribute_definition_search_result" isError: false performance: maxResponseTime: "2000ms" stderr: "toBeEmpty" # ================================================================================== # RESPONSE STRUCTURE VALIDATION # ================================================================================== - it: "should return properly structured SFCC API response format" request: jsonrpc: "2.0" id: "structure-validation" method: "tools/call" params: name: "search_system_object_attribute_definitions" arguments: objectType: "Product" searchRequest: query: match_all_query: {} count: 2 expect: response: jsonrpc: "2.0" id: "structure-validation" result: content: - type: "text" text: "match:regex:[\\s\\S]*\"_v\"[\\s\\S]*\"_type\"[\\s\\S]*\"object_attribute_definition_search_result\"[\\s\\S]*\"hits\"[\\s\\S]*\"query\"[\\s\\S]*\"start\"[\\s\\S]*\"total\"[\\s\\S]*" isError: false stderr: "toBeEmpty" - it: "should include attribute definition details when available" request: jsonrpc: "2.0" id: "attribute-details" method: "tools/call" params: name: "search_system_object_attribute_definitions" arguments: objectType: "Product" searchRequest: query: match_all_query: {} count: 3 select: "(**)" expect: response: jsonrpc: "2.0" id: "attribute-details" result: content: - type: "text" text: "match:regex:[\\s\\S]*\"_type\"[\\s\\S]*\"object_attribute_definition\"[\\s\\S]*\"id\"[\\s\\S]*\"link\"[\\s\\S]*" isError: false stderr: "toBeEmpty" ``` -------------------------------------------------------------------------------- /tests/servers/sfcc-mock-server/src/routes/ocapi/ocapi-utils.js: -------------------------------------------------------------------------------- ```javascript /** * OCAPI Utilities * * Shared utility functions for OCAPI handlers including select parameter parsing, * pagination helpers, and object enhancement functions. */ class OCAPIUtils { /** * Apply select parameter to the root response structure based on SFCC select syntax * Examples: * - (**) = return full response * - (start, total) = return only start and total fields * - (data.(**)) = return only data array with all object properties * - (data.(object_type)) = return only data array with specific fields * - (start, data.(**)) = return start field plus data array with all properties */ static applyRootSelectParameter(response, select) { // Default behavior - return full response if (!select || select === '(**)') { return response; } // Parse select parameter - remove outer parentheses and split by commas const trimmed = select.replace(/^\(|\)$/g, ''); const parts = this.parseSelectParts(trimmed); const result = {}; // Always include metadata fields that SFCC includes result._v = response._v; result._type = response._type; for (const part of parts) { const trimmedPart = part.trim(); if (trimmedPart === 'start') { result.start = response.start; } else if (trimmedPart === 'total') { result.total = response.total; } else if (trimmedPart === 'count') { result.count = response.count; } else if (trimmedPart === 'next') { result.next = response.next; } else if (trimmedPart === 'previous') { result.previous = response.previous; } else if (trimmedPart === 'select') { result.select = response.select; } else if (trimmedPart.startsWith('data.')) { // Handle data.(...) patterns result.data = response.data; // Note: data object filtering is already applied by applySelectParameter } else if (trimmedPart === 'data') { result.data = response.data; } } return result; } /** * Parse select parts handling nested parentheses correctly */ static parseSelectParts(selectString) { const parts = []; let current = ''; let depth = 0; for (let i = 0; i < selectString.length; i++) { const char = selectString[i]; if (char === '(') { depth++; current += char; } else if (char === ')') { depth--; current += char; } else if (char === ',' && depth === 0) { if (current.trim()) { parts.push(current.trim()); } current = ''; } else { current += char; } } if (current.trim()) { parts.push(current.trim()); } return parts; } /** * Apply select parameter to modify object structure based on SFCC select syntax * Examples: * - Default (no select) = return basic format (_type, _resource_state, object_type, link) * - (**) = return all fields (enhanced detailed format) * - (data.(object_type)) = return only object_type field in data objects * - (data.(object_type,display_name)) = return specific fields in data objects */ static applySelectParameter(obj, select) { // Default behavior (no select) - return basic format if (!select) { return obj; // Keep original basic format } // Full select - return enhanced detailed format if (select === '(**)') { return this.enhanceObjectWithDetailedFields(obj); } // Specific field selection using (...) syntax (after data.() processing) if (select.startsWith('(') && select.endsWith(')') && !select.startsWith('(data.(')) { // Extract field list from (field1,field2) const fieldsMatch = select.match(/\(([^)]+)\)/); if (fieldsMatch) { const fields = fieldsMatch[1].split(',').map(f => f.trim()); return this.selectSpecificFields(obj, fields); } } // Specific field selection using data.(...) syntax if (select.startsWith('(data.(') && select.endsWith('))')) { // Extract field list from (data.(field1,field2)) const fieldsMatch = select.match(/\(data\.\(([^)]+)\)\)/); if (fieldsMatch) { const fields = fieldsMatch[1].split(',').map(f => f.trim()); return this.selectSpecificFields(obj, fields); } } // Other select patterns that don't work properly in real API // Return basic format as fallback return obj; } /** * Select only specific fields from the object */ static selectSpecificFields(obj, fields) { const result = { "_type": "object_type_definition", "_resource_state": obj._resource_state }; // Add requested fields fields.forEach(field => { if (field === 'object_type') { result.object_type = obj.object_type; } else if (field === 'display_name') { result.display_name = this.getObjectTypeDisplayName(obj.object_type); } else if (field === 'description') { result.description = this.getObjectTypeDescription(obj.object_type); } else if (field === 'link') { result.link = obj.link; } else if (field === 'read_only') { result.read_only = this.getObjectTypeReadOnly(obj.object_type); } else if (field === 'queryable') { result.queryable = this.getObjectTypeQueryable(obj.object_type); } else if (field === 'content_object') { result.content_object = this.getObjectTypeContentObject(obj.object_type); } else if (field === 'attribute_definition_count') { result.attribute_definition_count = this.getObjectTypeAttributeCount(obj.object_type); } else if (field === 'attribute_group_count') { result.attribute_group_count = this.getObjectTypeAttributeGroupCount(obj.object_type); } else if (field === 'creation_date') { result.creation_date = "2024-02-19T10:18:31.000Z"; } else if (field === 'last_modified') { result.last_modified = "2024-02-19T10:18:31.000Z"; } }); return result; } /** * Enhance basic object definition with detailed fields to match real SFCC API response */ static enhanceObjectWithDetailedFields(obj) { // Return enhanced version with detailed fields (like the real API with select=(**)) const enhanced = { "_type": "object_type_definition", "_resource_state": obj._resource_state, "attribute_definition_count": this.getObjectTypeAttributeCount(obj.object_type), "attribute_group_count": this.getObjectTypeAttributeGroupCount(obj.object_type), "content_object": this.getObjectTypeContentObject(obj.object_type), "creation_date": "2024-02-19T10:18:31.000Z", "description": this.getObjectTypeDescription(obj.object_type), "display_name": this.getObjectTypeDisplayName(obj.object_type), "last_modified": "2024-02-19T10:18:31.000Z", "link": obj.link, "object_type": obj.object_type, "queryable": this.getObjectTypeQueryable(obj.object_type), "read_only": this.getObjectTypeReadOnly(obj.object_type) }; return enhanced; } /** * Generate pagination URLs for API responses */ static generatePaginationUrls(req, start, count, total, select) { const baseUrl = `${req.protocol}://${req.get('host')}${req.baseUrl}${req.path}`; let nextUrl = null; let previousUrl = null; // Generate next URL if there are more items const hasMore = (start + count) < total; if (hasMore) { const nextStart = start + count; nextUrl = `${baseUrl}?start=${nextStart}&count=${count}`; if (select !== '(**)') { nextUrl += `&select=${encodeURIComponent(select)}`; } } // Generate previous URL if not at the beginning if (start > 0) { const prevStart = Math.max(0, start - count); previousUrl = `${baseUrl}?start=${prevStart}&count=${count}`; if (select !== '(**)') { previousUrl += `&select=${encodeURIComponent(select)}`; } } return { nextUrl, previousUrl }; } /** * Apply basic text search filtering to results */ static applyTextSearch(results, query) { if (!query || !query.text_query) { return results; } const searchTerm = query.text_query.search_phrase.toLowerCase(); const searchFields = query.text_query.fields || ["id"]; return results.filter(item => { return searchFields.some(field => { if (field === "id" && item.id) { return item.id.toLowerCase().includes(searchTerm); } if (field === "display_name" && item.display_name) { // Check if any display_name value contains the search term const displayNames = Object.values(item.display_name || {}); return displayNames.some(name => String(name).toLowerCase().includes(searchTerm) ); } if (field === "description" && item.description) { return String(item.description).toLowerCase().includes(searchTerm); } if (field === "object_type" && item.object_type) { return item.object_type.toLowerCase().includes(searchTerm); } return false; }); }); } /** * Build proper query response with SFCC-compliant _type fields */ static buildQueryResponse(originalQuery) { let queryResponse = originalQuery || {"match_all_query": {}}; if (queryResponse.match_all_query && !queryResponse.match_all_query._type) { queryResponse = { ...queryResponse, match_all_query: { ...queryResponse.match_all_query, "_type": "match_all_query" } }; } if (queryResponse.text_query && !queryResponse.text_query._type) { queryResponse = { ...queryResponse, text_query: { ...queryResponse.text_query, "_type": "text_query" } }; } return queryResponse; } // Object type metadata helpers /** * Get display name for object type (with internationalization) */ static getObjectTypeDisplayName(objectType) { const displayNames = { 'Appeasement': { "de": "Beschwerde", "default": "Appeasement", "ja": "譲歩", "it": "Riconciliazione", "fr": "Geste commercial", "zh-CN": "协调", "es": "Compensación", "nl": "Tegemoetkoming" }, 'AppeasementItem': { "de": "Beschwerde-Artikel", "default": "Appeasement Item", "ja": "譲歩項目", "it": "Articolo di riconciliazione", "fr": "Article du geste commercial", "zh-CN": "协调项目", "es": "Artículo de compensación", "nl": "Tegemoetkomingsitem" }, 'Basket': { "de": "Warenkorb", "default": "Basket", "ja": "買い物カゴ", "it": "Carrello", "fr": "Panier", "zh-CN": "购物车", "es": "Canasta", "nl": "Mandje" } }; return displayNames[objectType] || { "default": objectType }; } /** * Get description for object type (with internationalization) */ static getObjectTypeDescription(objectType) { const descriptions = { 'Appeasement': { "default": "Object type representing appeasements." }, 'AppeasementItem': { "default": "Object type representing appeasement items." }, 'Basket': { "default": "Object type representing baskets." }, 'Product': { "default": "Object type representing products." }, 'Category': { "default": "Object type representing categories." }, 'Content': { "default": "Object type representing content assets." } }; return descriptions[objectType] || { "default": `Object type representing ${objectType.toLowerCase()} entities.` }; } /** * Get read_only flag for object type */ static getObjectTypeReadOnly(objectType) { const readOnlyTypes = ['CustomerActiveData', 'CustomerCDPData']; return readOnlyTypes.includes(objectType); } /** * Get queryable flag for object type */ static getObjectTypeQueryable(objectType) { const nonQueryableTypes = ['CustomObject']; return !nonQueryableTypes.includes(objectType); } /** * Get content_object flag for object type */ static getObjectTypeContentObject(objectType) { const contentObjectTypes = ['Content']; return contentObjectTypes.includes(objectType); } /** * Get attribute definition count for object type (matching real API data) */ static getObjectTypeAttributeCount(objectType) { const attributeCounts = { 'Appeasement': 9, 'AppeasementItem': 7, 'Basket': 11, 'BonusDiscountLineItem': 5, 'Campaign': 25, 'Catalog': 12, 'Category': 45, 'CategoryAssignment': 8, 'Content': 60, 'Coupon': 18, 'CouponLineItem': 6, 'CustomObject': 5, 'CustomerActiveData': 10, 'CustomerAddress': 15, 'CustomerCDPData': 10, 'CustomerGroup': 8, 'CustomerPaymentInstrument': 10, 'Customer': 28, 'Order': 65, 'Product': 166 }; return attributeCounts[objectType] || 10; } /** * Get attribute group count for object type (matching real API data) */ static getObjectTypeAttributeGroupCount(objectType) { const groupCounts = { 'Appeasement': 2, 'AppeasementItem': 1, 'Basket': 1, 'BonusDiscountLineItem': 1, 'Campaign': 2, 'Catalog': 1, 'Category': 3, 'CategoryAssignment': 1, 'Content': 3, 'Coupon': 2, 'CouponLineItem': 1, 'CustomObject': 1, 'CustomerActiveData': 2, 'CustomerAddress': 2, 'CustomerCDPData': 2, 'CustomerGroup': 1, 'CustomerPaymentInstrument': 2, 'Customer': 4, 'Order': 6, 'Product': 5 }; return groupCounts[objectType] || 2; } } module.exports = OCAPIUtils; ``` -------------------------------------------------------------------------------- /docs/dw_object/CustomObjectMgr.md: -------------------------------------------------------------------------------- ```markdown ## Package: dw.object # Class CustomObjectMgr ## Inheritance Hierarchy - Object - dw.object.CustomObjectMgr ## Description Manager class which provides methods for creating, retrieving, deleting, and searching for custom objects. To search for system objects, use SystemObjectMgr. ## Constructor Summary ## Method Summary ### createCustomObject **Signature:** `static createCustomObject(type : String, keyValue : String) : CustomObject` Returns a new custom object instance of the specified type, using the given key value. ### createCustomObject **Signature:** `static createCustomObject(type : String, keyValue : Number) : CustomObject` Returns a new custom object instance of the specified type, using the given key value. ### describe **Signature:** `static describe(type : String) : ObjectTypeDefinition` Returns the meta data for the given type. ### getAllCustomObjects **Signature:** `static getAllCustomObjects(type : String) : SeekableIterator` Returns all custom objects of a specific type. ### getCustomObject **Signature:** `static getCustomObject(type : String, keyValue : String) : CustomObject` Returns a custom object based on it's type and unique key. ### getCustomObject **Signature:** `static getCustomObject(type : String, keyValue : Number) : CustomObject` Returns a custom object based on it's type and unique key. ### queryCustomObject **Signature:** `static queryCustomObject(type : String, queryString : String, args : Object...) : CustomObject` Searches for a single custom object instance. ### queryCustomObjects **Signature:** `static queryCustomObjects(type : String, queryString : String, sortString : String, args : Object...) : SeekableIterator` Searches for custom object instances. ### queryCustomObjects **Signature:** `static queryCustomObjects(type : String, queryAttributes : Map, sortString : String) : SeekableIterator` Searches for custom object instances. ### remove **Signature:** `static remove(object : CustomObject) : void` Removes a given custom object. ## Method Detail ## Method Details ### createCustomObject **Signature:** `static createCustomObject(type : String, keyValue : String) : CustomObject` **Description:** Returns a new custom object instance of the specified type, using the given key value. Custom object keys need to be unique for custom object type. **Parameters:** - `type`: The unique name of the custom object type. - `keyValue`: The unique key value for the instance. **Returns:** The newly created custom object instance. **Throws:** IllegalArgumentException - if the given type is invalid --- ### createCustomObject **Signature:** `static createCustomObject(type : String, keyValue : Number) : CustomObject` **Description:** Returns a new custom object instance of the specified type, using the given key value. Custom object keys need to be unique for custom object type. **Parameters:** - `type`: The unique name of the custom object type. - `keyValue`: The unique key value for the instance. **Returns:** The newly created custom object instance. **Throws:** IllegalArgumentException - if the given type is invalid --- ### describe **Signature:** `static describe(type : String) : ObjectTypeDefinition` **Description:** Returns the meta data for the given type. **Parameters:** - `type`: the type whose meta data is returned. **Returns:** the meta data for the given type. **Throws:** IllegalArgumentException - if the given type is invalid --- ### getAllCustomObjects **Signature:** `static getAllCustomObjects(type : String) : SeekableIterator` **Description:** Returns all custom objects of a specific type. 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. **Parameters:** - `type`: The name of the custom object type. **See Also:** SeekableIterator.close() **Throws:** IllegalArgumentException - if the given type is invalid --- ### getCustomObject **Signature:** `static getCustomObject(type : String, keyValue : String) : CustomObject` **Description:** Returns a custom object based on it's type and unique key. **Parameters:** - `type`: The name of the custom object type. - `keyValue`: The unique key value. **Returns:** The matching custom object instance or null in case no matching custom object instance could be found. **Throws:** IllegalArgumentException - if the given type is invalid --- ### getCustomObject **Signature:** `static getCustomObject(type : String, keyValue : Number) : CustomObject` **Description:** Returns a custom object based on it's type and unique key. **Parameters:** - `type`: The name of the custom object type. - `keyValue`: The unique key value. **Returns:** The matching custom object instance or null in case no matching custom object instance could be found. **Throws:** IllegalArgumentException - if the given type is invalid --- ### queryCustomObject **Signature:** `static queryCustomObject(type : String, queryString : String, args : Object...) : CustomObject` **Description:** Searches for a single custom object 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 Case-independent 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: queryCustomObjects("", "custom.myAttr asc", null).first(). The method first() returns only the next element and closes the iterator. This method does not consider locale specific attributes. It returns all objects by checking the default non-localizable attributes. Any locale specific filtering after fetching the objects must be done by other custom code. Example: Get the custom objects using this method with non-localized attributes query. Access the obj.getCustom("myattr"). It returns the localized value of the attribute. **Parameters:** - `type`: the custom object type for the query. - `queryString`: the actual query. - `args`: optional parameters for the queryString. **Returns:** the custom object defined by type which was found when executing the queryString. **Throws:** IllegalArgumentException - if the given type is invalid --- ### queryCustomObjects **Signature:** `static queryCustomObjects(type : String, queryString : String, sortString : String, args : Object...) : SeekableIterator` **Description:** Searches for custom object instances. 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) 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. Sometimes it is desired to get all instances of specified type with a special sorting condition. This can be easily done by providing the 'type' of the custom object and the 'sortString' in combination with an empty 'queryString', e.g. queryCustomObjects("sample", "", "custom.myAttr asc") 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. This method does not consider locale specific attributes. It returns all objects by checking the default non-localizable attributes. Any locale specific filtering after fetching the objects must be done by other custom code. Example: Get the custom objects using this method with non-localized attributes query. Access the obj.getCustom("myattr"). It returns the localized value of the attribute. **Parameters:** - `type`: the custom object type for the query. - `queryString`: the actual query. - `sortString`: an optional sorting or null if no sorting is necessary. - `args`: optional parameters for the queryString. **Returns:** SeekableIterator containing the result set of the query. **See Also:** SeekableIterator.close() **Throws:** IllegalArgumentException - if the given type is invalid --- ### queryCustomObjects **Signature:** `static queryCustomObjects(type : String, queryAttributes : Map, sortString : String) : SeekableIterator` **Description:** Searches for custom object 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. This method does not consider locale specific attributes. It returns all objects by checking the default non-localizable attributes. Any locale specific filtering after fetching the objects must be done by other custom code. Example: Get the custom objects using this method with non-localized attributes query. Access the obj.getCustom("myattr"). It returns the localized value of the attribute. **Parameters:** - `type`: the custom object type for the query. - `queryAttributes`: key-value pairs, which define the query. - `sortString`: an optional sorting or null if no sorting is necessary. **Returns:** SeekableIterator containing the result set of the query. **See Also:** SeekableIterator.close() **Throws:** IllegalArgumentException - if the given type is invalid --- ### remove **Signature:** `static remove(object : CustomObject) : void` **Description:** Removes a given custom object. **Parameters:** - `object`: the custom object to remove, must not be null. --- ``` -------------------------------------------------------------------------------- /docs/best-practices/localserviceregistry.md: -------------------------------------------------------------------------------- ```markdown # SFCC B2C Commerce: LocalServiceRegistry Best Practices This guide provides a concise overview of best practices for creating server-to-server integrations in Salesforce B2C Commerce Cloud using the `dw.svc.LocalServiceRegistry`. ## 1. Core Architecture: Configuration and Code Integrations use a two-par5. Click **Apply** to save the new profile. ### Step 3: Create the Service Definition (Tying It All Together) The Service Definition links the Credential and Profile to create the final, named service that you will call from your code. 1. Navigate to **Administration > Operations > Services**. 2. Ensure you are on the **Services** tab and click **New**. 3. Fill in the following fields on the New Service page: - **Name (ID)**: Enter the unique ID for the service. This ID must exactly match the one used in your `LocalServiceRegistry.createService()` call. **Best Practice**: Use a period-delimited pattern like `{cartridge}.{protocol}.{service}.{operation}` (e.g., `int_myapi.http.customer.get`). This structure automatically organizes your service logs into a helpful hierarchy. - **Service Type**: Select the protocol, typically HTTP for REST APIs. - **Mode**: - **Live**: For making real calls to the external API. - **Mocked**: For testing. This mode will invoke the `mockCall` or `mockFull` function in your script instead of making a network request. - **Credential**: Select the Service Credential you created in Step 1 from the dropdown list. - **Profile**: Select the Service Profile you configured in Step 2 from the dropdown list. - **Log Name Prefix**: (Optional but recommended) Enter a prefix (e.g., `myapi`) to create a dedicated log file for this service (`service-myapi-....log`), which simplifies debugging. - **Communication Log Enabled**: Check this box to log the full request and response data. This is useful for debugging but should be used with caution in production if sensitive data is being transmitted. Always implement a `filterLogMessage` callback in your script to redact sensitive information from these logs. 4. Click **Apply** to save the service definition. Your service is now fully configured in the Business Manager and ready to be implemented and called from your SFRA cartridge code.rchitecture: **Declarative Configuration (Business Manager)**: Defines the what, who, and how of the service call. This includes the endpoint, credentials, and operational policies like timeouts and circuit breakers. **Programmatic Definition (Script Module)**: Defines the dynamic behavior using callbacks. This includes creating the request payload, parsing the response, and defining mock behavior for testing. ### Business Manager Configuration Summary | Component | Purpose | Key Settings | |-----------|---------|--------------| | Credential | Stores endpoint URL and authentication details. | ID, URL, User, Password | | Profile | Defines operational behavior and resilience. | Timeout, Rate Limiting, Circuit Breaker | | Service | Binds a Credential and Profile to a named service ID. | Name (ID), Service Type, Mode (Live/Mocked) | **Best Practice**: Use a period-delimited naming convention for the Service ID (e.g., `int_myapi.http.customer.get`) to organize logs effectively. ## 2. Script Implementation with LocalServiceRegistry The modern approach uses `dw.svc.LocalServiceRegistry` to define service behavior locally, eliminating the need for global initialization scripts. ### Key Callbacks The `createService` method takes a configuration object with the following core callback functions: - **`createRequest(svc,...params)`**: Configures the outgoing request (URL, method, headers) and returns the request body (e.g., a JSON string). - **`parseResponse(svc, client)`**: Processes the raw HTTP response (`dw.net.HTTPClient`). It should parse the response body (e.g., `JSON.parse(client.text)`) and throw an Error if the status code indicates a failure (e.g., `client.statusCode >= 400`). This ensures the `result.ok` flag is set correctly. - **`mockCall(svc, requestObj)`**: Mocks only the network execution. `createRequest` and `parseResponse` still run. Ideal for integration testing your service's logic. - **`mockFull(svc,...params)`**: Mocks the entire service call. No other callbacks are executed. Ideal for unit testing the consumer of the service (e.g., a controller). ### Reusable Service Module Pattern Encapsulate all logic for an integration into a single script module. Use a singleton pattern to avoid re-creating the service definition on every call. ```javascript 'use strict'; var LocalServiceRegistry = require('dw/svc/LocalServiceRegistry'); var Logger = require('dw/system/Logger'); // --- Private Helper for Operation-Specific Details --- /** * Returns configuration details for a specific service operation. * @param {string} operation - The operation ID (e.g., 'getCustomer') * @returns {{method: string, path: string}} */ function getOperationDetails(operation) { switch (operation) { case 'getCustomer': return { method: 'GET', path: '/customers/' }; case 'createCustomer': return { method: 'POST', path: '/customers' }; default: Logger.error('Unknown operation in MyAPIService: {0}', operation); throw new Error('Operation not implemented: ' + operation); } } // --- Service Definition --- var myAPIService = LocalServiceRegistry.createService('int_myapi.http.customer', { /** * @param {dw.svc.HTTPService} svc * @param {string} operation - The name of the operation to perform * @param {Object} params - The parameters for the operation * @returns {string|null} - Request body or null */ createRequest: function (svc, operation, params) { var details = getOperationDetails(operation); var credential = svc.getConfiguration().getCredential(); svc.setRequestMethod(details.method); svc.addHeader('Content-Type', 'application/json'); svc.addHeader('X-API-Key', 'your-api-key'); // Example custom header var url = credential.getURL() + details.path; if (details.method === 'GET' && params && params.id) { svc.setURL(url + params.id); return null; // No body for GET } svc.setURL(url); return params? JSON.stringify(params) : null; }, /** * @param {dw.svc.HTTPService} svc * @param {dw.net.HTTPClient} client * @returns {Object} - The parsed JSON response */ parseResponse: function (svc, client) { if (client.statusCode >= 400) { Logger.error('MyAPIService error: {0} {1} - {2}', client.statusCode, client.statusMessage, client.text); throw new Error('API call failed with status ' + client.statusCode); } try { return JSON.parse(client.text); } catch (e) { Logger.error('Error parsing JSON response from MyAPIService: {0}', client.text); throw new Error('Invalid JSON response'); } }, /** * @param {dw.svc.HTTPService} svc * @param {string} operation * @param {Object} params * @returns {Object} - The final, parsed mock object */ mockFull: function (svc, operation, params) { var details = getOperationDetails(operation); if (details.method === 'GET') { return { id: params.id, name: 'Mock Customer', email: '[email protected]' }; } if (details.method === 'POST') { return { id: 'mock-' + new Date().getTime(), name: params.name, email: params.email }; } return { error: 'Mock not implemented for ' + operation }; }, /** * Redacts sensitive data from logs. * @param {string} msg - The log message * @returns {string} - The filtered message */ filterLogMessage: function (msg) { // Basic redaction example for JSON strings try { var logObject = JSON.parse(msg); if (logObject.password) { logObject.password = '<REDACTED>'; } return JSON.stringify(logObject); } catch (e) { return msg; // Not a JSON message, return as is } } }); // --- Public API --- module.exports = { OPERATION_GET_CUSTOMER: 'getCustomer', OPERATION_CREATE_CUSTOMER: 'createCustomer', call: function (operation, params) { return myAPIService.call(operation, params); } }; ``` ## 3. Practical Examples ### GET Request (SFRA Controller) ```javascript // In controller/MyController.js var server = require('server'); var MyAPIService = require('~/cartridge/scripts/services/MyAPIService'); server.get('ShowCustomer', function(req, res, next) { var customerId = req.querystring.id; var result = MyAPIService.call(MyAPIService.OPERATION_GET_CUSTOMER, { id: customerId }); if (result.ok) { res.render('customer/customerDetails', { customer: result.object }); } else { res.render('error', { message: 'Could not retrieve customer data.', error: result.errorMessage }); } next(); }); ``` ### POST Request with JSON (SFRA Controller) ```javascript // In controller/MyController.js var server = require('server'); var MyAPIService = require('~/cartridge/scripts/services/MyAPIService'); server.post('CreateCustomer', function(req, res, next) { var customerData = { name: req.form.name, email: req.form.email }; var result = MyAPIService.call(MyAPIService.OPERATION_CREATE_CUSTOMER, customerData); if (result.ok) { res.json({ success: true, customer: result.object }); } else { res.setStatusCode(500); res.json({ success: false, error: result.errorMessage }); } next(); }); ``` ### OAuth 2.0 Client Credentials Flow Use a **Two-Service Pattern** for efficiency: one service to get the token, and another to call the API. - **Auth Service (AuthTokenService.js)**: Handles the POST request to the token endpoint. - **API Service (MyAPIService.js)**: - Gets the token from the Auth Service. - Caches the token (`dw.system.CacheMgr`) to avoid re-authenticating on every call. - Adds the `Authorization: Bearer <token>` header in its `createRequest` callback. ```javascript // Conceptual snippet for API Service createRequest with OAuth createRequest: function (svc, params) { var CacheMgr = require('dw/system/CacheMgr'); var AuthTokenService = require('~/cartridge/scripts/services/AuthTokenService'); // Get token from cache or fetch a new one var tokenCache = CacheMgr.getCache('MyAPIToken'); var token = tokenCache.get('access_token', function () { var result = AuthTokenService.call(); if (result.ok && result.object.access_token) { // Set cache expiry to slightly less than the token's actual expiry tokenCache.put('access_token', result.object.access_token, result.object.expires_in - 300); return result.object.access_token; } return null; }); if (!token) { throw new Error('Unable to retrieve valid API token.'); } svc.setRequestMethod('GET'); svc.addHeader('Authorization', 'Bearer ' + token); //... set URL and other request details... return null; } ``` ## 4. Step-by-Step Business Manager Configuration Guide This guide walks through the three essential parts of setting up a new service in the Business Manager: creating the Credential, configuring the Profile, and defining the Service itself. ### Step 1: Create the Service Credential (The "Who" and "What") The Service Credential securely stores the endpoint URL and authentication details for the external system. 1. Navigate to **Administration > Operations > Services**. 2. Click the **Credentials** tab. 3. On the Service Credentials page, click **New**. 4. Fill in the following fields on the New Service Credential page: - **ID**: Enter a unique identifier. A recommended naming convention is `your.service.name.http.credentials`. This name cannot contain spaces. - **URL**: Enter the base URL for the third-party API. For example: `https://api.example.com/v2/`. - **User**: If using Basic Authentication, enter the Client ID or username provided by the third-party service. - **Password**: Enter the Client Secret or password. For security, this field is write-only. Once saved, the value cannot be viewed again, so be sure to store it in a secure location. 5. Click **Apply** to save the new credential. ### Step 2: Configure the Service Profile (The "How") The Service Profile defines the operational behavior, such as timeouts and resilience patterns, for the service call. 1. Navigate to **Administration > Operations > Services**. 2. Click the **Profiles** tab. 3. On the Service Profiles page, click **New**. 4. Fill in the following fields on the New Service Profile page: - **Name (ID)**: Enter a descriptive name for the profile (e.g., `default-api-profile`, `realtime-payment-profile`). - **Timeout**: Enter the connection timeout in milliseconds (e.g., `10000` for 10 seconds). This is the maximum time B2C Commerce will wait for a response before the call fails. - **Enable Circuit Breaker**: Check this box to enable this crucial fault-tolerance feature. It is highly recommended to always enable this. - **Calls**: The number of failed calls that will "trip" the circuit (e.g., `10`). - **Interval (ms)**: The time window in which the failures must occur (e.g., `60000` for 1 minute). When the circuit is tripped, B2C Commerce will stop making calls to the service for a period to allow the external system to recover. - **Enable Rate Limit**: Check this box if the third-party API has call limits. - **Calls**: The maximum number of calls allowed in the interval (e.g., `1000`). - **Interval (ms)**: The time window for the rate limit in milliseconds (e.g., `60000` for 1 minute). 5. Click **Apply** to save the new profile. ### Step 3: Create the Service Definition (Tying It All Together) The Service Definition links the Credential and Profile to create the final, named service that you will call from your code. 1. Navigate to **Administration > Operations > Services**. 2. Ensure you are on the **Services** tab and click **New**. 3. Fill in the following fields on the New Service page: - **Name (ID)**: Enter the unique ID for the service. This ID must exactly match the one used in your `LocalServiceRegistry.createService()` call. **Best Practice**: Use a period-delimited pattern like `{cartridge}.{protocol}.{service}.{operation}` (e.g., `int_myapi.http.customer.get`). This structure automatically organizes your service logs into a helpful hierarchy. - **Service Type**: Select the protocol, typically HTTP for REST APIs. - **Enabled**: Check this box to enable the service. - **Service Mode**: - **Live**: For making real calls to the external API. - **Mocked**: For testing. This mode will invoke the `mockCall` or `mockFull` function in your script instead of making a network request. - **Log Name Prefix**: (Optional but recommended) Enter a prefix (e.g., `myapi`) to create a dedicated log file for this service (`service-myapi-....log`), which simplifies debugging. - **Communication Log Enabled**: Check this box to log the full request and response data. This is useful for debugging but should be used with caution in production if sensitive data is being transmitted. Always implement a `filterLogMessage` callback in your script to redact sensitive information from these logs. - **Force PRD Behavior in Non-PRD Environments**: Check this box to force the service to behave as if it is in a production environment, even when it is not. This can be useful for testing how the service will behave in production. - **Profile**: Select the Service Profile you configured in Step 2 from the dropdown list. - **Credential**: Select the Service Credential you created in Step 1 from the dropdown list. 4. Click **Apply** to save the service definition.Your service is now fully configured in the Business Manager and ready to be implemented and called from your SFRA cartridge code. ```