This is page 28 of 61. Use http://codebase.md/taurgis/sfcc-dev-mcp?lines=true&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 -------------------------------------------------------------------------------- /tests/mcp/node/get-latest-info.full-mode.programmatic.test.js: -------------------------------------------------------------------------------- ```javascript 1 | import { test, describe, before, after, beforeEach } from 'node:test'; 2 | import { strict as assert } from 'node:assert'; 3 | import { connect } from 'mcp-aegis'; 4 | 5 | describe('get_latest_info Tool - Advanced Programmatic Tests (Full Mode)', () => { 6 | let client; 7 | 8 | before(async () => { 9 | client = await connect('./aegis.config.with-dw.json'); 10 | }); 11 | 12 | after(async () => { 13 | if (client?.connected) { 14 | await client.disconnect(); 15 | } 16 | }); 17 | 18 | beforeEach(() => { 19 | // CRITICAL: Clear all buffers to prevent leaking into next tests 20 | client.clearAllBuffers(); 21 | }); 22 | 23 | // Helper function to get current date in YYYYMMDD format 24 | function getCurrentDateString() { 25 | const now = new Date(); 26 | const year = now.getFullYear(); 27 | const month = String(now.getMonth() + 1).padStart(2, '0'); 28 | const day = String(now.getDate()).padStart(2, '0'); 29 | return `${year}${month}${day}`; 30 | } 31 | 32 | // ======================================== 33 | // COMPLEX BUSINESS LOGIC VALIDATION 34 | // ======================================== 35 | 36 | describe('Complex Business Logic Validation', () => { 37 | test('should return info messages in chronological order with detailed parsing', async () => { 38 | const result = await client.callTool('get_latest_info', { limit: 2 }); 39 | 40 | assertValidMCPResponse(result); 41 | const text = result.content[0].text; 42 | 43 | // Complex chronological order validation - requires detailed parsing and comparison 44 | const jobExecution = 'Executing job [sfcc-export-dw-analytics-site-config][2664334]'; 45 | const stepExecution = 'Executing step [ExportDWAnalyticsSiteConfigurationStep][5846619] for [Organization]'; 46 | 47 | assert.ok(text.includes(jobExecution), 'Should contain job execution entry'); 48 | assert.ok(text.includes(stepExecution), 'Should contain step execution entry'); 49 | 50 | // Verify chronological ordering through position analysis 51 | const jobIndex = text.indexOf(jobExecution); 52 | const stepIndex = text.indexOf(stepExecution); 53 | assert.ok(jobIndex < stepIndex && jobIndex !== -1 && stepIndex !== -1, 54 | 'Newest info (job) should appear before older info (step) in response'); 55 | }); 56 | 57 | test('should validate business logic for SFCC info-level log filtering', async () => { 58 | const result = await client.callTool('get_latest_info', { limit: 10 }); 59 | 60 | assertValidMCPResponse(result); 61 | const logText = result.content[0].text; 62 | 63 | // Complex business logic validation - SFCC-specific log filtering 64 | assert.ok(logText.includes('info-blade'), 'Should reference info log files specifically'); 65 | assert.ok(logText.includes('INFO'), 'Should contain INFO log level entries'); 66 | 67 | // Critical: Verify log level isolation (complex filtering validation) 68 | assert.ok(!logText.includes('ERROR'), 'Should not contain ERROR entries'); 69 | assert.ok(!logText.includes('WARN'), 'Should not contain WARN entries'); 70 | assert.ok(!logText.includes('DEBUG'), 'Should not contain DEBUG entries'); 71 | }); 72 | 73 | test('should parse and validate complex SFCC log entry structure', async () => { 74 | const result = await client.callTool('get_latest_info', { limit: 1 }); 75 | 76 | assertValidMCPResponse(result); 77 | const logText = result.content[0].text; 78 | 79 | // Complex log structure parsing and validation 80 | const timestampPattern = /\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3} GMT\]/; 81 | const sfccPattern = /(SystemJobThread|PipelineCallServlet|Sites-)/; 82 | 83 | assert.ok(timestampPattern.test(logText), 'Should contain valid SFCC timestamp format'); 84 | assert.ok(sfccPattern.test(logText), 'Should contain SFCC-specific system components'); 85 | 86 | // Complex structure validation 87 | assert.ok(logText.includes('['), 'Should contain log file reference brackets'); 88 | assert.ok(logText.includes('] ['), 'Should contain proper timestamp delimiters'); 89 | assert.ok(logText.includes('GMT]'), 'Should contain GMT timezone specification'); 90 | }); 91 | }); 92 | 93 | // ======================================== 94 | // DYNAMIC PARAMETER VALIDATION 95 | // ======================================== 96 | 97 | describe('Dynamic Parameter Validation', () => { 98 | test('should validate parameter types dynamically with complex logic', async () => { 99 | const testCases = [ 100 | { params: { limit: null }, shouldSucceed: false, description: 'null limit' }, 101 | { params: { limit: undefined }, shouldSucceed: true, description: 'undefined limit (uses default)' }, 102 | { params: { limit: [] }, shouldSucceed: false, description: 'array limit' }, 103 | { params: { limit: {} }, shouldSucceed: false, description: 'object limit' }, 104 | { params: { date: null }, shouldSucceed: true, description: 'null date (uses default)' }, 105 | { params: { date: 123 }, shouldSucceed: true, description: 'numeric date (handled gracefully)' } 106 | ]; 107 | 108 | // Complex dynamic validation with detailed result analysis 109 | for (const testCase of testCases) { 110 | const result = await client.callTool('get_latest_info', testCase.params); 111 | assertValidMCPResponse(result); 112 | 113 | if (testCase.shouldSucceed) { 114 | assert.ok(result.content.length > 0, 115 | `Should have content for ${testCase.description}: ${JSON.stringify(testCase.params)}`); 116 | } else { 117 | // Complex validation: either error response or graceful handling 118 | assert.ok(result.isError || result.content.length > 0, 119 | `Should either error or handle gracefully for ${testCase.description}`); 120 | } 121 | } 122 | }); 123 | 124 | test('should handle complex boundary condition matrix', async () => { 125 | const boundaryTests = [ 126 | { limit: 1, shouldSucceed: true, category: 'minimum valid' }, 127 | { limit: 1000, shouldSucceed: true, category: 'maximum valid' }, 128 | { limit: 1001, shouldSucceed: false, category: 'over maximum' }, 129 | { limit: 0, shouldSucceed: false, category: 'zero (invalid)' }, 130 | { limit: -1, shouldSucceed: false, category: 'negative' } 131 | ]; 132 | 133 | // Complex boundary validation with categorization 134 | for (const test of boundaryTests) { 135 | const result = await client.callTool('get_latest_info', { limit: test.limit }); 136 | assertValidMCPResponse(result); 137 | 138 | if (test.shouldSucceed) { 139 | assert.equal(result.isError, false, 140 | `Boundary test ${test.category} (limit: ${test.limit}) should succeed`); 141 | } else { 142 | assert.equal(result.isError, true, 143 | `Boundary test ${test.category} (limit: ${test.limit}) should fail validation`); 144 | } 145 | } 146 | }); 147 | }); 148 | 149 | // ======================================== 150 | // MULTI-STEP WORKFLOWS AND CONSISTENCY 151 | // ======================================== 152 | 153 | describe('Multi-Step Workflows and Consistency', () => { 154 | test('should maintain consistency across sequential operations', async () => { 155 | const results = []; 156 | 157 | // Complex multi-step workflow simulation 158 | for (let i = 0; i < 5; i++) { 159 | const result = await client.callTool('get_latest_info', { limit: 5 }); 160 | results.push(result); 161 | } 162 | 163 | // Complex consistency validation across multiple calls 164 | results.forEach((result, index) => { 165 | assertValidMCPResponse(result); 166 | assert.equal(result.isError, false, `Sequential call ${index + 1} should succeed`); 167 | assert.ok(result.content[0].text.includes('Latest 5 info messages'), 168 | `Sequential call ${index + 1} should show consistent limit`); 169 | }); 170 | 171 | // Complex cross-result consistency validation 172 | const firstText = results[0].content[0].text; 173 | const lastText = results[results.length - 1].content[0].text; 174 | assert.equal(firstText, lastText, 'Sequential calls should return identical results for same parameters'); 175 | }); 176 | 177 | test('should handle complex parameter combination workflows', async () => { 178 | const paramCombinations = [ 179 | {}, 180 | { limit: 5 }, 181 | { date: getCurrentDateString() }, 182 | { limit: 3, date: getCurrentDateString() }, 183 | { limit: 1 }, 184 | { limit: 50 } 185 | ]; 186 | 187 | // Complex workflow validation with parameter combinations 188 | for (const params of paramCombinations) { 189 | const result = await client.callTool('get_latest_info', params); 190 | assertValidMCPResponse(result); 191 | 192 | // Complex validation logic for parameter combinations 193 | const hasValidLimit = !params.limit || (params.limit > 0 && params.limit <= 1000); 194 | if (hasValidLimit) { 195 | assert.equal(result.isError, false, 196 | `Valid parameter combination should succeed: ${JSON.stringify(params)}`); 197 | 198 | // Verify expected limit is reflected in response 199 | const expectedLimit = params.limit || 10; // default limit 200 | assert.ok(result.content[0].text.includes(`Latest ${expectedLimit} info messages`), 201 | `Should show correct limit for combination: ${JSON.stringify(params)}`); 202 | } 203 | } 204 | }); 205 | 206 | test('should validate complex limit range processing', async () => { 207 | const limitTests = [1, 5, 10, 25, 50, 100]; 208 | 209 | // Complex range validation with detailed analysis 210 | for (const limit of limitTests) { 211 | const result = await client.callTool('get_latest_info', { limit }); 212 | 213 | assertValidMCPResponse(result); 214 | assert.equal(result.isError, false, `Limit ${limit} should process successfully`); 215 | assert.ok(result.content[0].text.includes(`Latest ${limit} info messages`), 216 | `Should correctly process limit ${limit}`); 217 | 218 | // Complex content length analysis 219 | const logText = result.content[0].text; 220 | const separatorCount = (logText.match(/---/g) || []).length; 221 | 222 | // Validate separator count matches expected entries (limit - 1 separators for limit entries) 223 | if (limit > 1) { 224 | assert.ok(separatorCount >= 1, 225 | `Should have separators for limit ${limit} (found ${separatorCount})`); 226 | } 227 | } 228 | }); 229 | }); 230 | 231 | // ======================================== 232 | // ERROR RECOVERY AND RESILIENCE 233 | // ======================================== 234 | 235 | describe('Error Recovery and Resilience', () => { 236 | test('should demonstrate complex error recovery workflow', async () => { 237 | // Complex error recovery scenario with state validation 238 | 239 | // Step 1: Trigger error condition 240 | const errorResult = await client.callTool('get_latest_info', { limit: -1 }); 241 | assert.equal(errorResult.isError, true, 'Should error with negative limit'); 242 | 243 | // Step 2: Verify normal operation resumes immediately 244 | const normalResult = await client.callTool('get_latest_info', { limit: 5 }); 245 | assertValidMCPResponse(normalResult); 246 | assert.equal(normalResult.isError, false, 'Should recover and work normally'); 247 | 248 | // Step 3: Verify complex recovery with different parameters 249 | const recoveryResult = await client.callTool('get_latest_info', { 250 | limit: 3, 251 | date: getCurrentDateString() 252 | }); 253 | assertValidMCPResponse(recoveryResult); 254 | assert.equal(recoveryResult.isError, false, 'Should handle complex parameters after error'); 255 | }); 256 | 257 | test('should maintain state integrity after complex invalid operations', async () => { 258 | // Complex state integrity validation 259 | const invalidOperations = [ 260 | { limit: -10, description: 'negative limit' }, 261 | { limit: 'abc', description: 'string limit' }, 262 | { date: 'invalid', description: 'invalid date' }, 263 | { limit: null, description: 'null limit' }, 264 | { limit: 1001, description: 'over-limit' } 265 | ]; 266 | 267 | // Execute complex series of invalid operations 268 | for (const invalidOp of invalidOperations) { 269 | await client.callTool('get_latest_info', invalidOp); 270 | // Note: Don't assert on individual results as some may be handled gracefully 271 | } 272 | 273 | // Complex state validation after error series 274 | const result = await client.callTool('get_latest_info', { limit: 3 }); 275 | assertValidMCPResponse(result); 276 | assert.equal(result.isError, false, 'Should work normally after series of invalid operations'); 277 | assert.ok(result.content[0].text.includes('Latest 3 info messages'), 278 | 'Should produce correct output after error recovery'); 279 | }); 280 | }); 281 | 282 | // ======================================== 283 | // INTEGRATION AND PROTOCOL COMPLIANCE 284 | // ======================================== 285 | 286 | describe('Integration and Protocol Compliance', () => { 287 | test('should integrate properly with MCP protocol and tool discovery', async () => { 288 | // Complex MCP protocol compliance validation 289 | const tools = await client.listTools(); 290 | const infoTool = tools.find(tool => tool.name === 'get_latest_info'); 291 | 292 | assert.ok(infoTool, 'get_latest_info tool should be discoverable via MCP protocol'); 293 | assert.ok(infoTool.description, 'Tool should have comprehensive description'); 294 | assert.ok(infoTool.inputSchema, 'Tool should have valid input schema'); 295 | 296 | // Complex schema validation 297 | const schema = infoTool.inputSchema; 298 | assert.equal(schema.type, 'object', 'Input schema should be object type'); 299 | assert.ok(schema.properties, 'Schema should define properties'); 300 | 301 | // Verify tool execution works through MCP protocol 302 | const result = await client.callTool('get_latest_info', {}); 303 | assertValidMCPResponse(result); 304 | assert.equal(result.isError, false, 'Tool should execute successfully via MCP protocol'); 305 | }); 306 | 307 | test('should validate complex date parameter integration', async () => { 308 | // Complex date parameter validation with current date integration 309 | const testDate = getCurrentDateString(); 310 | 311 | const result = await client.callTool('get_latest_info', { 312 | date: testDate, 313 | limit: 2 314 | }); 315 | 316 | assertValidMCPResponse(result); 317 | assert.equal(result.isError, false, 'Should handle current date parameter'); 318 | assert.ok(result.content[0].text.includes('Latest 2 info messages'), 319 | 'Should show correct limit with date parameter'); 320 | 321 | // Complex validation: date parameter should not affect content structure 322 | const defaultResult = await client.callTool('get_latest_info', { limit: 2 }); 323 | assertValidMCPResponse(defaultResult); 324 | 325 | // Compare structure consistency 326 | const dateTextLines = result.content[0].text.split('\n').length; 327 | const defaultTextLines = defaultResult.content[0].text.split('\n').length; 328 | assert.ok(Math.abs(dateTextLines - defaultTextLines) <= 1, 329 | 'Date parameter should not significantly alter response structure'); 330 | }); 331 | }); 332 | }); 333 | 334 | // ======================================== 335 | // HELPER FUNCTIONS 336 | // ======================================== 337 | 338 | /** 339 | * Validates that a result follows the MCP response structure 340 | */ 341 | function assertValidMCPResponse(result) { 342 | assert.ok(result, 'Result should exist'); 343 | assert.ok(result.content, 'Result should have content'); 344 | assert.ok(Array.isArray(result.content), 'Content should be array'); 345 | assert.equal(typeof result.isError, 'boolean', 'isError should be boolean'); 346 | assert.ok(result.content.length > 0, 'Content should not be empty'); 347 | } 348 | ``` -------------------------------------------------------------------------------- /docs/dw_order/ShippingOrder.md: -------------------------------------------------------------------------------- ```markdown 1 | ## Package: dw.order 2 | 3 | # Class ShippingOrder 4 | 5 | ## Inheritance Hierarchy 6 | 7 | - Object 8 | - dw.object.Extensible 9 | - dw.order.AbstractItemCtnr 10 | - dw.order.ShippingOrder 11 | 12 | ## Description 13 | 14 | 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. 15 | 16 | ## Constants 17 | 18 | ### ORDERBY_ITEMID 19 | 20 | **Type:** Object 21 | 22 | Sorting by item id. Use with method getItems() as an argument to method FilteringCollection.sort(Object). 23 | 24 | ### ORDERBY_ITEMPOSITION 25 | 26 | **Type:** Object 27 | 28 | Sorting by the position of the related oder item. Use with method getItems() as an argument to method FilteringCollection.sort(Object). 29 | 30 | ### ORDERBY_UNSORTED 31 | 32 | **Type:** Object 33 | 34 | Unsorted , as it is. Use with method getItems() as an argument to method FilteringCollection.sort(Object). 35 | 36 | ### QUALIFIER_PRODUCTITEMS 37 | 38 | **Type:** Object 39 | 40 | Selects the product items. Use with method getItems() as an argument to method FilteringCollection.select(Object). 41 | 42 | ### QUALIFIER_SERVICEITEMS 43 | 44 | **Type:** Object 45 | 46 | Selects for the service items. Use with method getItems() as an argument to method FilteringCollection.select(Object). 47 | 48 | ### STATUS_CANCELLED 49 | 50 | **Type:** String = "CANCELLED" 51 | 52 | Constant for Shipping Order Status CANCELLED 53 | 54 | ### STATUS_CONFIRMED 55 | 56 | **Type:** String = "CONFIRMED" 57 | 58 | Constant for Shipping Order Status CONFIRMED 59 | 60 | ### STATUS_SHIPPED 61 | 62 | **Type:** String = "SHIPPED" 63 | 64 | Constant for Shipping Order Status SHIPPED 65 | 66 | ### STATUS_WAREHOUSE 67 | 68 | **Type:** String = "WAREHOUSE" 69 | 70 | Constant for Shipping Order Status WAREHOUSE 71 | 72 | ## Properties 73 | 74 | ### invoice 75 | 76 | **Type:** Invoice (Read Only) 77 | 78 | Returns null or the previously created Invoice. 79 | 80 | ### invoiceNumber 81 | 82 | **Type:** String (Read Only) 83 | 84 | Returns null or the invoice-number. 85 | 86 | ### items 87 | 88 | **Type:** FilteringCollection (Read Only) 89 | 90 | A filtering collection of the shipping order items belonging to the 91 | shipping order. 92 | 93 | This FilteringCollection could be sorted / filtered 94 | using: 95 | 96 | FilteringCollection.sort(Object) with 97 | ORDERBY_ITEMID 98 | FilteringCollection.sort(Object) with 99 | ORDERBY_ITEMPOSITION 100 | FilteringCollection.sort(Object) with 101 | ORDERBY_UNSORTED 102 | FilteringCollection.select(Object) with 103 | QUALIFIER_PRODUCTITEMS 104 | FilteringCollection.select(Object) with 105 | QUALIFIER_SERVICEITEMS 106 | 107 | ### shipDate 108 | 109 | **Type:** Date 110 | 111 | Gets the shipping date. 112 | 113 | Returns null if this shipping order is not yet shipped. 114 | 115 | ### shippingAddress 116 | 117 | **Type:** OrderAddress 118 | 119 | The shipping address (optional, can be null). 120 | 121 | 122 | Note: the shipping address is not copied into the 123 | ShippingOrder but is a link to a 124 | OrderAddress held in the Order. 125 | 126 | ### shippingMethod 127 | 128 | **Type:** ShippingMethod (Read Only) 129 | 130 | The shipping method of the shipping order. 131 | 132 | Can be null. 133 | 134 | ### shippingOrderNumber 135 | 136 | **Type:** String (Read Only) 137 | 138 | Gets the shipping order number. 139 | 140 | ### status 141 | 142 | **Type:** EnumValue (Read Only) 143 | 144 | Gets the status of this shipping order. The status is read-only and 145 | calculated from the item status. See class documentation for more 146 | details. 147 | The possible values are STATUS_CONFIRMED, 148 | STATUS_WAREHOUSE, STATUS_SHIPPED, 149 | STATUS_CANCELLED. 150 | 151 | ### trackingInfos 152 | 153 | **Type:** Collection (Read Only) 154 | 155 | Gets all tracking informations for this shipping order. 156 | 157 | ## Constructor Summary 158 | 159 | ## Method Summary 160 | 161 | ### addTrackingInfo 162 | 163 | **Signature:** `addTrackingInfo(trackingInfoID : String) : TrackingInfo` 164 | 165 | Adds a tracking info to this shipping order with the given ID. 166 | 167 | ### createInvoice 168 | 169 | **Signature:** `createInvoice() : Invoice` 170 | 171 | Creates a new Invoice based on this ShippingOrder. 172 | 173 | ### createInvoice 174 | 175 | **Signature:** `createInvoice(invoiceNumber : String) : Invoice` 176 | 177 | Creates a new Invoice based on this ShippingOrder. 178 | 179 | ### createShippingOrderItem 180 | 181 | **Signature:** `createShippingOrderItem(orderItem : OrderItem, quantity : Quantity) : ShippingOrderItem` 182 | 183 | Create a ShippingOrderItem in the shipping order with the number shippingOrderNumber. 184 | 185 | ### createShippingOrderItem 186 | 187 | **Signature:** `createShippingOrderItem(orderItem : OrderItem, quantity : Quantity, splitIfPartial : boolean) : ShippingOrderItem` 188 | 189 | Create a ShippingOrderItem in the shipping order with the number shippingOrderNumber. 190 | 191 | ### getInvoice 192 | 193 | **Signature:** `getInvoice() : Invoice` 194 | 195 | Returns null or the previously created Invoice. 196 | 197 | ### getInvoiceNumber 198 | 199 | **Signature:** `getInvoiceNumber() : String` 200 | 201 | Returns null or the invoice-number. 202 | 203 | ### getItems 204 | 205 | **Signature:** `getItems() : FilteringCollection` 206 | 207 | A filtering collection of the shipping order items belonging to the shipping order. 208 | 209 | ### getShipDate 210 | 211 | **Signature:** `getShipDate() : Date` 212 | 213 | Gets the shipping date. 214 | 215 | ### getShippingAddress 216 | 217 | **Signature:** `getShippingAddress() : OrderAddress` 218 | 219 | Returns the shipping address (optional, can be null). 220 | 221 | ### getShippingMethod 222 | 223 | **Signature:** `getShippingMethod() : ShippingMethod` 224 | 225 | Returns the shipping method of the shipping order. 226 | 227 | ### getShippingOrderNumber 228 | 229 | **Signature:** `getShippingOrderNumber() : String` 230 | 231 | Gets the shipping order number. 232 | 233 | ### getStatus 234 | 235 | **Signature:** `getStatus() : EnumValue` 236 | 237 | Gets the status of this shipping order. 238 | 239 | ### getTrackingInfo 240 | 241 | **Signature:** `getTrackingInfo(trackingInfoID : String) : TrackingInfo` 242 | 243 | Gets a tracking info for this shipping order. 244 | 245 | ### getTrackingInfos 246 | 247 | **Signature:** `getTrackingInfos() : Collection` 248 | 249 | Gets all tracking informations for this shipping order. 250 | 251 | ### setShipDate 252 | 253 | **Signature:** `setShipDate(date : Date) : void` 254 | 255 | Sets the shipping date. 256 | 257 | ### setShippingAddress 258 | 259 | **Signature:** `setShippingAddress(address : OrderAddress) : void` 260 | 261 | Set a shipping address for the shipping order. 262 | 263 | ### setShippingMethodID 264 | 265 | **Signature:** `setShippingMethodID(shippingMethodID : String) : void` 266 | 267 | Set the id of shipping method. 268 | 269 | ### setStatusWarehouse 270 | 271 | **Signature:** `setStatusWarehouse() : void` 272 | 273 | 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. 274 | 275 | ## Method Detail 276 | 277 | ## Method Details 278 | 279 | ### addTrackingInfo 280 | 281 | **Signature:** `addTrackingInfo(trackingInfoID : String) : TrackingInfo` 282 | 283 | **Description:** Adds a tracking info to this shipping order with the given ID. 284 | 285 | **Parameters:** 286 | 287 | - `trackingInfoID`: the tracking info id 288 | 289 | **Returns:** 290 | 291 | the new tracking info 292 | 293 | **See Also:** 294 | 295 | TrackingInfo 296 | 297 | --- 298 | 299 | ### createInvoice 300 | 301 | **Signature:** `createInvoice() : Invoice` 302 | 303 | **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. 304 | 305 | **Returns:** 306 | 307 | new invoice 308 | 309 | --- 310 | 311 | ### createInvoice 312 | 313 | **Signature:** `createInvoice(invoiceNumber : String) : Invoice` 314 | 315 | **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. 316 | 317 | **Parameters:** 318 | 319 | - `invoiceNumber`: the invoice-number to use 320 | 321 | **Returns:** 322 | 323 | new invoice 324 | 325 | --- 326 | 327 | ### createShippingOrderItem 328 | 329 | **Signature:** `createShippingOrderItem(orderItem : OrderItem, quantity : Quantity) : ShippingOrderItem` 330 | 331 | **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). 332 | 333 | **Parameters:** 334 | 335 | - `orderItem`: the order item for which the shipping order item is to be created 336 | - `quantity`: the quantity for which the shipping order item will be created 337 | 338 | **Returns:** 339 | 340 | the created shipping order item 341 | 342 | --- 343 | 344 | ### createShippingOrderItem 345 | 346 | **Signature:** `createShippingOrderItem(orderItem : OrderItem, quantity : Quantity, splitIfPartial : boolean) : ShippingOrderItem` 347 | 348 | **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). 349 | 350 | **Parameters:** 351 | 352 | - `orderItem`: the order item for which the shipping order item is to be created 353 | - `quantity`: the quantity for which the shipping order item will be created, not null 354 | - `splitIfPartial`: the flag whether ProductLineItem should be split when requested quantity is less than ProductLineItem's quantity 355 | 356 | **Returns:** 357 | 358 | the created shipping order item 359 | 360 | --- 361 | 362 | ### getInvoice 363 | 364 | **Signature:** `getInvoice() : Invoice` 365 | 366 | **Description:** Returns null or the previously created Invoice. 367 | 368 | **Returns:** 369 | 370 | null or the previously created invoice. 371 | 372 | **See Also:** 373 | 374 | createInvoice(String) 375 | 376 | --- 377 | 378 | ### getInvoiceNumber 379 | 380 | **Signature:** `getInvoiceNumber() : String` 381 | 382 | **Description:** Returns null or the invoice-number. 383 | 384 | **Returns:** 385 | 386 | null or the previously created invoice number. 387 | 388 | **See Also:** 389 | 390 | createInvoice(String) 391 | 392 | --- 393 | 394 | ### getItems 395 | 396 | **Signature:** `getItems() : FilteringCollection` 397 | 398 | **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 399 | 400 | **Returns:** 401 | 402 | the filtering collection of the shipping items. 403 | 404 | **See Also:** 405 | 406 | createShippingOrderItem(OrderItem, Quantity) 407 | ShippingOrderItem 408 | 409 | --- 410 | 411 | ### getShipDate 412 | 413 | **Signature:** `getShipDate() : Date` 414 | 415 | **Description:** Gets the shipping date. Returns null if this shipping order is not yet shipped. 416 | 417 | **Returns:** 418 | 419 | the shipping date or null 420 | 421 | --- 422 | 423 | ### getShippingAddress 424 | 425 | **Signature:** `getShippingAddress() : OrderAddress` 426 | 427 | **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. 428 | 429 | **Returns:** 430 | 431 | the shipping address or null 432 | 433 | --- 434 | 435 | ### getShippingMethod 436 | 437 | **Signature:** `getShippingMethod() : ShippingMethod` 438 | 439 | **Description:** Returns the shipping method of the shipping order. Can be null. 440 | 441 | **Returns:** 442 | 443 | the shipping method or null 444 | 445 | --- 446 | 447 | ### getShippingOrderNumber 448 | 449 | **Signature:** `getShippingOrderNumber() : String` 450 | 451 | **Description:** Gets the shipping order number. 452 | 453 | **Returns:** 454 | 455 | the shipping order number 456 | 457 | --- 458 | 459 | ### getStatus 460 | 461 | **Signature:** `getStatus() : EnumValue` 462 | 463 | **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. 464 | 465 | **Returns:** 466 | 467 | the status 468 | 469 | --- 470 | 471 | ### getTrackingInfo 472 | 473 | **Signature:** `getTrackingInfo(trackingInfoID : String) : TrackingInfo` 474 | 475 | **Description:** Gets a tracking info for this shipping order. 476 | 477 | **Parameters:** 478 | 479 | - `trackingInfoID`: the tracking info id 480 | 481 | **Returns:** 482 | 483 | the tracking info or null 484 | 485 | **See Also:** 486 | 487 | TrackingInfo 488 | 489 | --- 490 | 491 | ### getTrackingInfos 492 | 493 | **Signature:** `getTrackingInfos() : Collection` 494 | 495 | **Description:** Gets all tracking informations for this shipping order. 496 | 497 | **Returns:** 498 | 499 | all tracking informations for this shipping order 500 | 501 | **See Also:** 502 | 503 | TrackingInfo 504 | 505 | --- 506 | 507 | ### setShipDate 508 | 509 | **Signature:** `setShipDate(date : Date) : void` 510 | 511 | **Description:** Sets the shipping date. 512 | 513 | **Parameters:** 514 | 515 | - `date`: the ship date 516 | 517 | --- 518 | 519 | ### setShippingAddress 520 | 521 | **Signature:** `setShippingAddress(address : OrderAddress) : void` 522 | 523 | **Description:** Set a shipping address for the shipping order. 524 | 525 | **Parameters:** 526 | 527 | - `address`: the shipping address 528 | 529 | **See Also:** 530 | 531 | getShippingAddress() 532 | 533 | --- 534 | 535 | ### setShippingMethodID 536 | 537 | **Signature:** `setShippingMethodID(shippingMethodID : String) : void` 538 | 539 | **Description:** Set the id of shipping method. 540 | 541 | **Parameters:** 542 | 543 | - `shippingMethodID`: the id of the shipping method 544 | 545 | **See Also:** 546 | 547 | ShippingMethod.getID() 548 | 549 | --- 550 | 551 | ### setStatusWarehouse 552 | 553 | **Signature:** `setStatusWarehouse() : void` 554 | 555 | **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. 556 | 557 | **Throws:** 558 | 559 | IllegalArgumentException - if the shipping order is in a status other than CONFIRMED. 560 | 561 | --- ``` -------------------------------------------------------------------------------- /tests/mcp/yaml/activate-code-version.full-mode.test.mcp.yml: -------------------------------------------------------------------------------- ```yaml 1 | # ================================================================================== 2 | # SFCC MCP Server - activate_code_version Tool YAML Tests (Full Mode) 3 | # Tests code version activation functionality with SFCC credentials 4 | # This tool provides code version management for deployment troubleshooting 5 | # 6 | # Test Coverage: 7 | # - Tool availability and definition validation in full mode 8 | # - Parameter validation (required codeVersionId, empty/null handling) 9 | # - Error responses for non-existent and invalid code versions 10 | # - Response structure validation for both success and error cases 11 | # - Performance requirements (under 2000ms for OCAPI calls) 12 | # - Edge cases (long IDs, special characters) 13 | # - Consistency across different invalid inputs 14 | # - SFCC fault information parsing 15 | # 16 | # Quick Test Commands: 17 | # aegis "tests/mcp/yaml/activate-code-version.full-mode.test.mcp.yml" --config "aegis.config.with-dw.json" --verbose 18 | # aegis query activate_code_version 'codeVersionId:test-version-001' --config "aegis.config.with-dw.json" 19 | # ================================================================================== 20 | 21 | description: "activate_code_version tool full mode tests - Core functionality validation" 22 | 23 | tests: 24 | # ================================================================================== 25 | # TOOL AVAILABILITY TESTS 26 | # ================================================================================== 27 | - it: "should list activate_code_version tool in full mode" 28 | request: 29 | jsonrpc: "2.0" 30 | id: "tool-availability-full" 31 | method: "tools/list" 32 | params: {} 33 | expect: 34 | response: 35 | jsonrpc: "2.0" 36 | id: "tool-availability-full" 37 | result: 38 | tools: 39 | match:arrayElements: 40 | match:partial: 41 | name: "match:type:string" 42 | description: "match:type:string" 43 | match:extractField: "tools.*.name" 44 | value: "match:arrayContains:activate_code_version" 45 | stderr: "toBeEmpty" 46 | 47 | - it: "should have correct tool definition in full mode" 48 | request: 49 | jsonrpc: "2.0" 50 | id: "tool-definition-full" 51 | method: "tools/list" 52 | params: {} 53 | expect: 54 | response: 55 | jsonrpc: "2.0" 56 | id: "tool-definition-full" 57 | result: 58 | tools: "match:arrayContains:name:activate_code_version" 59 | stderr: "toBeEmpty" 60 | 61 | - it: "should have proper tool schema definition" 62 | request: 63 | jsonrpc: "2.0" 64 | id: "tool-schema-validation" 65 | method: "tools/list" 66 | params: {} 67 | expect: 68 | response: 69 | jsonrpc: "2.0" 70 | id: "tool-schema-validation" 71 | result: 72 | tools: "match:arrayContains:name:activate_code_version" 73 | stderr: "toBeEmpty" 74 | 75 | # ================================================================================== 76 | # SETUP - RESET TO KNOWN STATE 77 | # ================================================================================== 78 | - it: "should reset to known state before testing" 79 | request: 80 | jsonrpc: "2.0" 81 | id: "reset-state" 82 | method: "tools/call" 83 | params: 84 | name: "activate_code_version" 85 | arguments: 86 | codeVersionId: "reset_version" 87 | expect: 88 | response: 89 | jsonrpc: "2.0" 90 | id: "reset-state" 91 | result: 92 | content: 93 | match:arrayElements: 94 | type: "text" 95 | text: "match:type:string" 96 | isError: false 97 | stderr: "toBeEmpty" 98 | 99 | # ================================================================================== 100 | # SUCCESSFUL ACTIVATION TESTS 101 | # ================================================================================== 102 | - it: "should successfully activate first test code version" 103 | request: 104 | jsonrpc: "2.0" 105 | id: "activate-success-1" 106 | method: "tools/call" 107 | params: 108 | name: "activate_code_version" 109 | arguments: 110 | codeVersionId: "test_activation" 111 | expect: 112 | response: 113 | jsonrpc: "2.0" 114 | id: "activate-success-1" 115 | result: 116 | content: 117 | match:arrayElements: 118 | type: "text" 119 | text: "match:contains:\"id\": \"test_activation\"" 120 | isError: false 121 | stderr: "toBeEmpty" 122 | performance: 123 | maxResponseTime: "2000ms" 124 | 125 | - it: "should successfully activate second test code version" 126 | request: 127 | jsonrpc: "2.0" 128 | id: "activate-success-2" 129 | method: "tools/call" 130 | params: 131 | name: "activate_code_version" 132 | arguments: 133 | codeVersionId: "simple_id" 134 | expect: 135 | response: 136 | jsonrpc: "2.0" 137 | id: "activate-success-2" 138 | result: 139 | content: 140 | match:arrayElements: 141 | type: "text" 142 | text: "match:contains:\"id\": \"simple_id\"" 143 | isError: false 144 | stderr: "toBeEmpty" 145 | performance: 146 | maxResponseTime: "2000ms" 147 | 148 | - it: "should successfully activate third test code version with dashes" 149 | request: 150 | jsonrpc: "2.0" 151 | id: "activate-success-3" 152 | method: "tools/call" 153 | params: 154 | name: "activate_code_version" 155 | arguments: 156 | codeVersionId: "version-with-dashes" 157 | expect: 158 | response: 159 | jsonrpc: "2.0" 160 | id: "activate-success-3" 161 | result: 162 | content: 163 | match:arrayElements: 164 | type: "text" 165 | text: "match:contains:\"id\": \"version-with-dashes\"" 166 | isError: false 167 | stderr: "toBeEmpty" 168 | performance: 169 | maxResponseTime: "2000ms" 170 | 171 | # ================================================================================== 172 | # RE-ACTIVATION FAILURE TEST 173 | # ================================================================================== 174 | - it: "should fail when attempting to re-activate currently active version" 175 | request: 176 | jsonrpc: "2.0" 177 | id: "reactivate-failure" 178 | method: "tools/call" 179 | params: 180 | name: "activate_code_version" 181 | arguments: 182 | codeVersionId: "version-with-dashes" 183 | expect: 184 | response: 185 | jsonrpc: "2.0" 186 | id: "reactivate-failure" 187 | result: 188 | content: 189 | match:arrayElements: 190 | type: "text" 191 | text: "match:contains:already active" 192 | isError: true 193 | stderr: "toBeEmpty" 194 | performance: 195 | maxResponseTime: "2000ms" 196 | 197 | # ================================================================================== 198 | # RESET TO KNOWN STATE (Final) 199 | # ================================================================================== 200 | - it: "should reset back to reset_version to clean up test state" 201 | request: 202 | jsonrpc: "2.0" 203 | id: "final-reset" 204 | method: "tools/call" 205 | params: 206 | name: "activate_code_version" 207 | arguments: 208 | codeVersionId: "reset_version" 209 | expect: 210 | response: 211 | jsonrpc: "2.0" 212 | id: "final-reset" 213 | result: 214 | content: 215 | match:arrayElements: 216 | type: "text" 217 | text: "match:contains:\"id\": \"reset_version\"" 218 | isError: false 219 | stderr: "toBeEmpty" 220 | performance: 221 | maxResponseTime: "2000ms" 222 | 223 | # ================================================================================== 224 | # PARAMETER VALIDATION TESTS 225 | # ================================================================================== 226 | - it: "should reject missing required parameter" 227 | request: 228 | jsonrpc: "2.0" 229 | id: "missing-param" 230 | method: "tools/call" 231 | params: 232 | name: "activate_code_version" 233 | arguments: {} 234 | expect: 235 | response: 236 | jsonrpc: "2.0" 237 | id: "missing-param" 238 | result: 239 | content: 240 | match:arrayElements: 241 | type: "text" 242 | text: "match:contains:codeVersionId must be a non-empty string" 243 | isError: true 244 | stderr: "toBeEmpty" 245 | performance: 246 | maxResponseTime: "1000ms" 247 | 248 | - it: "should reject empty codeVersionId" 249 | request: 250 | jsonrpc: "2.0" 251 | id: "empty-param" 252 | method: "tools/call" 253 | params: 254 | name: "activate_code_version" 255 | arguments: 256 | codeVersionId: "" 257 | expect: 258 | response: 259 | jsonrpc: "2.0" 260 | id: "empty-param" 261 | result: 262 | content: 263 | match:arrayElements: 264 | type: "text" 265 | text: "match:contains:codeVersionId must be a non-empty string" 266 | isError: true 267 | stderr: "toBeEmpty" 268 | performance: 269 | maxResponseTime: "1000ms" 270 | 271 | - it: "should reject null codeVersionId" 272 | request: 273 | jsonrpc: "2.0" 274 | id: "null-param" 275 | method: "tools/call" 276 | params: 277 | name: "activate_code_version" 278 | arguments: 279 | codeVersionId: null 280 | expect: 281 | response: 282 | jsonrpc: "2.0" 283 | id: "null-param" 284 | result: 285 | content: 286 | match:arrayElements: 287 | type: "text" 288 | text: "match:contains:codeVersionId must be a non-empty string" 289 | isError: true 290 | stderr: "toBeEmpty" 291 | performance: 292 | maxResponseTime: "1000ms" 293 | 294 | # ================================================================================== 295 | # BASIC FUNCTIONALITY TESTS (Using Non-Existent Code Version) 296 | # ================================================================================== 297 | - it: "should handle non-existent code version gracefully" 298 | request: 299 | jsonrpc: "2.0" 300 | id: "non-existent-version" 301 | method: "tools/call" 302 | params: 303 | name: "activate_code_version" 304 | arguments: 305 | codeVersionId: "test-version-nonexistent" 306 | expect: 307 | response: 308 | jsonrpc: "2.0" 309 | id: "non-existent-version" 310 | result: 311 | content: 312 | match:arrayElements: 313 | type: "text" 314 | text: "match:regex:Error[\\s\\S]*404[\\s\\S]*not found[\\s\\S]*" 315 | isError: true 316 | stderr: "toBeEmpty" 317 | performance: 318 | maxResponseTime: "2000ms" 319 | 320 | - it: "should provide meaningful error for invalid code version ID" 321 | request: 322 | jsonrpc: "2.0" 323 | id: "invalid-version-id" 324 | method: "tools/call" 325 | params: 326 | name: "activate_code_version" 327 | arguments: 328 | codeVersionId: "invalid@#$%^&*()" 329 | expect: 330 | response: 331 | jsonrpc: "2.0" 332 | id: "invalid-version-id" 333 | result: 334 | content: 335 | match:arrayElements: 336 | type: "text" 337 | text: "match:regex:Error[\\s\\S]*(404|not found|invalid)[\\s\\S]*" 338 | isError: true 339 | stderr: "toBeEmpty" 340 | performance: 341 | maxResponseTime: "2000ms" 342 | 343 | # ================================================================================== 344 | # RESPONSE STRUCTURE VALIDATION 345 | # ================================================================================== 346 | - it: "should return proper error response structure" 347 | request: 348 | jsonrpc: "2.0" 349 | id: "error-structure" 350 | method: "tools/call" 351 | params: 352 | name: "activate_code_version" 353 | arguments: 354 | codeVersionId: "test-nonexistent" 355 | expect: 356 | response: 357 | jsonrpc: "2.0" 358 | id: "error-structure" 359 | result: 360 | content: 361 | match:arrayElements: 362 | match:partial: 363 | type: "text" 364 | text: "match:type:string" 365 | isError: true 366 | stderr: "toBeEmpty" 367 | 368 | - it: "should include SFCC fault information in error response" 369 | request: 370 | jsonrpc: "2.0" 371 | id: "sfcc-fault-info" 372 | method: "tools/call" 373 | params: 374 | name: "activate_code_version" 375 | arguments: 376 | codeVersionId: "test-fault-info" 377 | expect: 378 | response: 379 | jsonrpc: "2.0" 380 | id: "sfcc-fault-info" 381 | result: 382 | content: 383 | match:arrayElements: 384 | type: "text" 385 | text: "match:regex:Error[\\s\\S]*(fault|InvalidParameterException|404)[\\s\\S]*" 386 | isError: true 387 | stderr: "toBeEmpty" 388 | 389 | # ================================================================================== 390 | # PERFORMANCE TESTS 391 | # ================================================================================== 392 | - it: "should meet performance requirements for error responses" 393 | request: 394 | jsonrpc: "2.0" 395 | id: "performance-error" 396 | method: "tools/call" 397 | params: 398 | name: "activate_code_version" 399 | arguments: 400 | codeVersionId: "test-performance" 401 | expect: 402 | response: 403 | jsonrpc: "2.0" 404 | id: "performance-error" 405 | result: 406 | content: 407 | match:arrayElements: 408 | type: "text" 409 | text: "match:contains:Error" 410 | isError: true 411 | stderr: "toBeEmpty" 412 | performance: 413 | maxResponseTime: "2000ms" 414 | 415 | # ================================================================================== 416 | # EDGE CASE TESTS 417 | # ================================================================================== 418 | - it: "should handle very long code version ID" 419 | request: 420 | jsonrpc: "2.0" 421 | id: "long-version-id" 422 | method: "tools/call" 423 | params: 424 | name: "activate_code_version" 425 | arguments: 426 | codeVersionId: "test-very-long-code-version-id-that-exceeds-normal-length-expectations-and-might-cause-issues" 427 | expect: 428 | response: 429 | jsonrpc: "2.0" 430 | id: "long-version-id" 431 | result: 432 | content: 433 | match:arrayElements: 434 | type: "text" 435 | text: "match:regex:Error[\\s\\S]*(404|not found)[\\s\\S]*" 436 | isError: true 437 | stderr: "toBeEmpty" 438 | performance: 439 | maxResponseTime: "2000ms" 440 | 441 | - it: "should handle special characters in code version ID" 442 | request: 443 | jsonrpc: "2.0" 444 | id: "special-chars" 445 | method: "tools/call" 446 | params: 447 | name: "activate_code_version" 448 | arguments: 449 | codeVersionId: "test-version-with-spaces and-chars" 450 | expect: 451 | response: 452 | jsonrpc: "2.0" 453 | id: "special-chars" 454 | result: 455 | content: 456 | match:arrayElements: 457 | type: "text" 458 | text: "match:regex:Error[\\s\\S]*(404|not found)[\\s\\S]*" 459 | isError: true 460 | stderr: "toBeEmpty" 461 | performance: 462 | maxResponseTime: "2000ms" 463 | 464 | # ================================================================================== 465 | # CONSISTENCY TESTS 466 | # ================================================================================== 467 | - it: "should return consistent error format across different invalid inputs" 468 | request: 469 | jsonrpc: "2.0" 470 | id: "consistent-errors-1" 471 | method: "tools/call" 472 | params: 473 | name: "activate_code_version" 474 | arguments: 475 | codeVersionId: "test-consistency-1" 476 | expect: 477 | response: 478 | jsonrpc: "2.0" 479 | id: "consistent-errors-1" 480 | result: 481 | content: 482 | match:arrayElements: 483 | match:partial: 484 | type: "text" 485 | isError: true 486 | stderr: "toBeEmpty" 487 | 488 | - it: "should return consistent error format for different invalid version" 489 | request: 490 | jsonrpc: "2.0" 491 | id: "consistent-errors-2" 492 | method: "tools/call" 493 | params: 494 | name: "activate_code_version" 495 | arguments: 496 | codeVersionId: "test-consistency-2" 497 | expect: 498 | response: 499 | jsonrpc: "2.0" 500 | id: "consistent-errors-2" 501 | result: 502 | content: 503 | match:arrayElements: 504 | match:partial: 505 | type: "text" 506 | isError: true 507 | stderr: "toBeEmpty" ``` -------------------------------------------------------------------------------- /src/clients/cartridge-generation-client.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * SFCC Cartridge Generation Client 3 | * 4 | * This client handles the generation of SFCC cartridge directory structures 5 | * with all necessary files and configurations, replacing the outdated sgmf-scripts 6 | * with a modern, integrated approach. 7 | */ 8 | 9 | import { Logger } from '../utils/logger.js'; 10 | import { IFileSystemService, IPathService } from '../services/index.js'; 11 | 12 | interface CartridgeGenerationOptions { 13 | cartridgeName: string; 14 | targetPath?: string; 15 | fullProjectSetup?: boolean; 16 | } 17 | 18 | interface CartridgeTemplates { 19 | packageJson: (cartridgeName: string) => object; 20 | dwJson: () => object; 21 | webpackConfig: (cartridgeName: string) => string; 22 | dotProject: (cartridgeName: string) => string; 23 | projectProperties: (cartridgeName: string) => string; 24 | eslintrc: () => object; 25 | stylelintrc: () => object; 26 | eslintignore: () => string; 27 | gitignore: () => string; 28 | } 29 | 30 | export class CartridgeGenerationClient { 31 | private logger: Logger; 32 | private templates: CartridgeTemplates; 33 | private fileSystem: IFileSystemService; 34 | private pathService: IPathService; 35 | 36 | constructor(fileSystem: IFileSystemService, pathService: IPathService) { 37 | this.logger = Logger.getChildLogger('CartridgeGenerationClient'); 38 | this.fileSystem = fileSystem; 39 | this.pathService = pathService; 40 | this.templates = this.initializeTemplates(); 41 | } 42 | 43 | /** 44 | * Normalize the target path by removing /cartridges or /cartridges/ from the end 45 | * The cartridge creation always happens from the root folder 46 | */ 47 | private normalizeTargetPath(targetPath: string): string { 48 | // Remove trailing slashes first 49 | let normalized = targetPath.replace(/\/+$/, ''); 50 | 51 | // Remove /cartridges from the end if present 52 | if (normalized.endsWith('/cartridges')) { 53 | normalized = normalized.slice(0, -11); // Remove '/cartridges' (11 characters) 54 | } 55 | 56 | this.logger.debug(`Normalized target path from '${targetPath}' to '${normalized}'`); 57 | return normalized; 58 | } 59 | 60 | /** 61 | * Generate a complete cartridge structure 62 | */ 63 | async generateCartridgeStructure(options: CartridgeGenerationOptions): Promise<{ 64 | success: boolean; 65 | message: string; 66 | createdFiles: string[]; 67 | createdDirectories: string[]; 68 | skippedFiles: string[]; 69 | }> { 70 | const { cartridgeName, targetPath, fullProjectSetup = true } = options; 71 | const createdFiles: string[] = []; 72 | const createdDirectories: string[] = []; 73 | const skippedFiles: string[] = []; 74 | 75 | try { 76 | this.logger.info(`Starting cartridge generation for: ${cartridgeName}`); 77 | 78 | // Determine the working directory and normalize path 79 | let workingDir = targetPath ?? process.cwd(); 80 | workingDir = this.normalizeTargetPath(workingDir); 81 | 82 | if (fullProjectSetup) { 83 | // Full project setup - create everything directly in the working directory 84 | this.logger.info(`Creating full project setup directly in: ${workingDir}`); 85 | 86 | // Ensure the working directory exists 87 | await this.ensureDirectory(workingDir); 88 | if (!createdDirectories.includes(workingDir)) { 89 | createdDirectories.push(workingDir); 90 | } 91 | 92 | // Create root files directly in working directory 93 | await this.createRootFiles(workingDir, cartridgeName, createdFiles, skippedFiles); 94 | 95 | // Create cartridge structure directly in working directory 96 | await this.createCartridgeStructure(workingDir, cartridgeName, createdFiles, createdDirectories, skippedFiles); 97 | 98 | return { 99 | success: true, 100 | message: `Successfully created full project setup for cartridge '${cartridgeName}' in '${workingDir}'`, 101 | createdFiles, 102 | createdDirectories, 103 | skippedFiles, 104 | }; 105 | } else { 106 | // Cartridge-only setup - add to existing project 107 | const cartridgesDir = this.pathService.join(workingDir, 'cartridges'); 108 | 109 | // Ensure cartridges directory exists 110 | await this.ensureDirectory(cartridgesDir); 111 | if (!createdDirectories.includes(cartridgesDir)) { 112 | createdDirectories.push(cartridgesDir); 113 | } 114 | 115 | // Create cartridge structure 116 | await this.createCartridgeStructure(workingDir, cartridgeName, createdFiles, createdDirectories, skippedFiles); 117 | 118 | return { 119 | success: true, 120 | message: `Successfully created cartridge '${cartridgeName}' in existing project at '${workingDir}'`, 121 | createdFiles, 122 | createdDirectories, 123 | skippedFiles, 124 | }; 125 | } 126 | } catch (error) { 127 | this.logger.error('Error generating cartridge structure:', error); 128 | return { 129 | success: false, 130 | message: `Failed to generate cartridge structure: ${error instanceof Error ? error.message : 'Unknown error'}`, 131 | createdFiles, 132 | createdDirectories, 133 | skippedFiles, 134 | }; 135 | } 136 | } 137 | 138 | /** 139 | * Create root project files (package.json, webpack, etc.) 140 | */ 141 | private async createRootFiles( 142 | projectDir: string, 143 | cartridgeName: string, 144 | createdFiles: string[], 145 | skippedFiles: string[], 146 | ): Promise<void> { 147 | // Create package.json 148 | const packageJsonPath = this.pathService.join(projectDir, 'package.json'); 149 | await this.safeWriteFile( 150 | packageJsonPath, 151 | JSON.stringify(this.templates.packageJson(cartridgeName), null, 2), 152 | createdFiles, 153 | skippedFiles, 154 | ); 155 | 156 | // Create dw.json 157 | const dwJsonPath = this.pathService.join(projectDir, 'dw.json'); 158 | await this.safeWriteFile( 159 | dwJsonPath, 160 | JSON.stringify(this.templates.dwJson(), null, 2), 161 | createdFiles, 162 | skippedFiles, 163 | ); 164 | 165 | // Create webpack.config.js 166 | const webpackPath = this.pathService.join(projectDir, 'webpack.config.js'); 167 | await this.safeWriteFile( 168 | webpackPath, 169 | this.templates.webpackConfig(cartridgeName), 170 | createdFiles, 171 | skippedFiles, 172 | ); 173 | 174 | // Create .eslintrc.json 175 | const eslintrcPath = this.pathService.join(projectDir, '.eslintrc.json'); 176 | await this.safeWriteFile( 177 | eslintrcPath, 178 | JSON.stringify(this.templates.eslintrc(), null, 2), 179 | createdFiles, 180 | skippedFiles, 181 | ); 182 | 183 | // Create .stylelintrc.json 184 | const stylelintrcPath = this.pathService.join(projectDir, '.stylelintrc.json'); 185 | await this.safeWriteFile( 186 | stylelintrcPath, 187 | JSON.stringify(this.templates.stylelintrc(), null, 2), 188 | createdFiles, 189 | skippedFiles, 190 | ); 191 | 192 | // Create .eslintignore 193 | const eslintignorePath = this.pathService.join(projectDir, '.eslintignore'); 194 | await this.safeWriteFile( 195 | eslintignorePath, 196 | this.templates.eslintignore(), 197 | createdFiles, 198 | skippedFiles, 199 | ); 200 | 201 | // Create .gitignore 202 | const gitignorePath = this.pathService.join(projectDir, '.gitignore'); 203 | await this.safeWriteFile( 204 | gitignorePath, 205 | this.templates.gitignore(), 206 | createdFiles, 207 | skippedFiles, 208 | ); 209 | } 210 | 211 | /** 212 | * Create the cartridge directory structure 213 | */ 214 | private async createCartridgeStructure( 215 | baseDir: string, 216 | cartridgeName: string, 217 | createdFiles: string[], 218 | createdDirectories: string[], 219 | skippedFiles: string[], 220 | ): Promise<void> { 221 | // Create cartridges directory 222 | const cartridgesDir = this.pathService.join(baseDir, 'cartridges'); 223 | await this.ensureDirectory(cartridgesDir); 224 | createdDirectories.push(cartridgesDir); 225 | 226 | // Create specific cartridge directory 227 | const cartridgeDir = this.pathService.join(cartridgesDir, cartridgeName); 228 | await this.ensureDirectory(cartridgeDir); 229 | createdDirectories.push(cartridgeDir); 230 | 231 | // Create .project file 232 | const projectPath = this.pathService.join(cartridgeDir, '.project'); 233 | await this.safeWriteFile( 234 | projectPath, 235 | this.templates.dotProject(cartridgeName), 236 | createdFiles, 237 | skippedFiles, 238 | ); 239 | 240 | // Create cartridge subdirectory 241 | const cartridgeSubDir = this.pathService.join(cartridgeDir, 'cartridge'); 242 | await this.ensureDirectory(cartridgeSubDir); 243 | createdDirectories.push(cartridgeSubDir); 244 | 245 | // Create cartridge properties file 246 | const propertiesPath = this.pathService.join(cartridgeSubDir, `${cartridgeName}.properties`); 247 | await this.safeWriteFile( 248 | propertiesPath, 249 | this.templates.projectProperties(cartridgeName), 250 | createdFiles, 251 | skippedFiles, 252 | ); 253 | 254 | // Create directory structure 255 | const directories = [ 256 | 'controllers', 257 | 'models', 258 | 'templates', 259 | 'templates/default', 260 | 'templates/resources', 261 | 'client', 262 | 'client/default', 263 | 'client/default/js', 264 | 'client/default/scss', 265 | ]; 266 | 267 | for (const dir of directories) { 268 | const fullPath = this.pathService.join(cartridgeSubDir, dir); 269 | await this.ensureDirectory(fullPath); 270 | createdDirectories.push(fullPath); 271 | } 272 | } 273 | 274 | /** 275 | * Ensure a directory exists, create if it doesn't 276 | */ 277 | private async ensureDirectory(dirPath: string): Promise<void> { 278 | try { 279 | await this.fileSystem.access(dirPath); 280 | } catch { 281 | await this.fileSystem.mkdir(dirPath, { recursive: true }); 282 | this.logger.info(`Created directory: ${dirPath}`); 283 | } 284 | } 285 | 286 | /** 287 | * Safely write a file, skipping if it already exists 288 | */ 289 | private async safeWriteFile( 290 | filePath: string, 291 | content: string, 292 | createdFiles: string[], 293 | skippedFiles: string[], 294 | ): Promise<void> { 295 | try { 296 | await this.fileSystem.access(filePath); 297 | // File exists, skip it 298 | skippedFiles.push(filePath); 299 | this.logger.info(`Skipped existing file: ${filePath}`); 300 | } catch { 301 | // File doesn't exist, create it 302 | await this.fileSystem.writeFile(filePath, content); 303 | createdFiles.push(filePath); 304 | this.logger.info(`Created file: ${filePath}`); 305 | } 306 | } 307 | 308 | /** 309 | * Initialize all file templates 310 | */ 311 | private initializeTemplates(): CartridgeTemplates { 312 | return { 313 | packageJson: (cartridgeName: string) => ({ 314 | name: cartridgeName, 315 | version: '0.0.1', 316 | description: 'New overlay cartridge', 317 | main: 'index.js', 318 | scripts: { 319 | 'lint': 'npm run lint:css && npm run lint:js', 320 | 'lint:css': 'sgmf-scripts --lint css', 321 | 'lint:js': 'sgmf-scripts --lint js', 322 | 'lint:fix': 'eslint cartridges --fix', 323 | upload: 'sgmf-scripts --upload -- ', 324 | uploadCartridge: `sgmf-scripts --uploadCartridge ${cartridgeName}`, 325 | 'compile:js': 'sgmf-scripts --compile js', 326 | 'compile:scss': 'sgmf-scripts --compile css', 327 | }, 328 | devDependencies: { 329 | autoprefixer: '^10.4.14', 330 | bestzip: '^2.2.1', 331 | 'css-loader': '^6.0.0', 332 | 'css-minimizer-webpack-plugin': '^5.0.1', 333 | eslint: '^8.56.0', 334 | 'eslint-config-airbnb-base': '^15.0.0', 335 | 'eslint-config-prettier': '^9.1.0', 336 | 'eslint-plugin-import': '^2.29.0', 337 | 'mini-css-extract-plugin': '^2.7.6', 338 | 'postcss-loader': '^7.0.0', 339 | sass: '^1.69.7', 340 | 'sass-loader': '^13.3.2', 341 | 'sgmf-scripts': '^3.0.0', 342 | shx: '^0.3.4', 343 | stylelint: '^15.4.0', 344 | 'stylelint-config-standard-scss': '^11.0.0', 345 | 'webpack-remove-empty-scripts': '^1.0.4', 346 | }, 347 | browserslist: [ 348 | 'last 2 versions', 349 | 'ie >= 10', 350 | ], 351 | }), 352 | 353 | dwJson: () => ({ 354 | hostname: '', 355 | username: '', 356 | password: '', 357 | 'code-version': '', 358 | }), 359 | 360 | webpackConfig: (cartridgeName: string) => `'use strict'; 361 | 362 | var path = require('path'); 363 | var MiniCssExtractPlugin = require('mini-css-extract-plugin'); 364 | var CssMinimizerPlugin = require('css-minimizer-webpack-plugin'); 365 | var sgmfScripts = require('sgmf-scripts'); 366 | var RemoveEmptyScriptsPlugin = require('webpack-remove-empty-scripts'); 367 | 368 | module.exports = [{ 369 | mode: 'development', 370 | name: 'js', 371 | entry: sgmfScripts.createJsPath(), 372 | output: { 373 | path: path.resolve('./cartridges/${cartridgeName}/cartridge/static'), 374 | filename: '[name].js' 375 | } 376 | }, { 377 | mode: 'none', 378 | name: 'scss', 379 | entry: sgmfScripts.createScssPath(), 380 | output: { 381 | path: path.resolve('./cartridges/${cartridgeName}/cartridge/static') 382 | }, 383 | module: { 384 | rules: [{ 385 | test: /\\.scss$/, 386 | use: [{ 387 | loader: MiniCssExtractPlugin.loader, 388 | options: { 389 | esModule: false 390 | } 391 | }, 392 | { 393 | loader: 'css-loader', 394 | options: { 395 | url: false 396 | } 397 | }, { 398 | loader: 'postcss-loader', 399 | options: { 400 | postcssOptions: { 401 | plugins: [require('autoprefixer')] 402 | } 403 | } 404 | }, { 405 | loader: 'sass-loader', 406 | options: { 407 | implementation: require('sass'), 408 | sassOptions: { 409 | includePaths: [ 410 | path.resolve(path.resolve(process.cwd(), '../storefront-reference-architecture/node_modules/')), 411 | path.resolve(process.cwd(), '../storefront-reference-architecture/node_modules/flag-icons/sass') 412 | ] 413 | } 414 | } 415 | }] 416 | }] 417 | }, 418 | plugins: [ 419 | new RemoveEmptyScriptsPlugin(), 420 | new MiniCssExtractPlugin({ 421 | filename: '[name].css', 422 | chunkFilename: '[name].css' 423 | }) 424 | ], 425 | optimization: { 426 | minimizer: ['...', new CssMinimizerPlugin()] 427 | } 428 | }];`, 429 | 430 | dotProject: (cartridgeName: string) => `<?xml version="1.0" encoding="UTF-8"?> 431 | <projectDescription> 432 | <name>${cartridgeName}</name> 433 | <comment></comment> 434 | <projects> 435 | </projects> 436 | <buildSpec> 437 | <buildCommand> 438 | <name>com.demandware.studio.core.beehiveElementBuilder</name> 439 | <arguments> 440 | </arguments> 441 | </buildCommand> 442 | </buildSpec> 443 | <natures> 444 | <nature>com.demandware.studio.core.beehiveNature</nature> 445 | </natures> 446 | </projectDescription>`, 447 | 448 | projectProperties: (cartridgeName: string) => `## cartridge.properties for cartridge ${cartridgeName} 449 | #demandware.cartridges.${cartridgeName}.multipleLanguageStorefront=true`, 450 | 451 | eslintrc: () => ({ 452 | root: true, 453 | extends: 'airbnb-base/legacy', 454 | globals: { 455 | session: 'readonly', 456 | request: 'readonly', 457 | }, 458 | rules: { 459 | 'import/no-unresolved': 'off', 460 | indent: ['error', 4, { SwitchCase: 1, VariableDeclarator: 1 }], 461 | 'func-names': 'off', 462 | 'require-jsdoc': 'error', 463 | 'valid-jsdoc': ['error', { 464 | preferType: { 465 | Boolean: 'boolean', 466 | Number: 'number', 467 | object: 'Object', 468 | String: 'string', 469 | }, 470 | requireReturn: false, 471 | }], 472 | 'vars-on-top': 'off', 473 | 'global-require': 'off', 474 | 'no-shadow': ['error', { allow: ['err', 'callback'] }], 475 | 'max-len': 'off', 476 | 'no-plusplus': 'off', 477 | }, 478 | }), 479 | 480 | stylelintrc: () => ({ 481 | extends: 'stylelint-config-standard-scss', 482 | plugins: [ 483 | 'stylelint-scss', 484 | ], 485 | }), 486 | 487 | eslintignore: () => `node_modules/ 488 | cartridges/**/cartridge/static/ 489 | coverage/ 490 | doc/ 491 | bin/ 492 | codecept.conf.js`, 493 | 494 | gitignore: () => `node_modules/ 495 | cartridges/*/cartridge/static/ 496 | .DS_Store 497 | *.log 498 | npm-debug.log* 499 | yarn-debug.log* 500 | yarn-error.log* 501 | coverage/ 502 | .nyc_output/ 503 | .env 504 | dw.json`, 505 | }; 506 | } 507 | } 508 | ``` -------------------------------------------------------------------------------- /docs/dw_io/File.md: -------------------------------------------------------------------------------- ```markdown 1 | ## Package: dw.io 2 | 3 | # Class File 4 | 5 | ## Inheritance Hierarchy 6 | 7 | - Object 8 | - dw.io.File 9 | 10 | ## Description 11 | 12 | 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. 13 | 14 | ## Constants 15 | 16 | ### CATALOGS 17 | 18 | **Type:** String = "CATALOGS" 19 | 20 | Catalogs root directory. 21 | 22 | ### CUSTOMER_SNAPSHOTS 23 | 24 | **Type:** String = "CUSTOMERSNAPSHOTS" 25 | 26 | Customer snapshots root directory. 27 | 28 | ### CUSTOMERPI 29 | 30 | **Type:** String = "CUSTOMERPI" 31 | 32 | Customer Payment Instrument root directory. 33 | 34 | ### DYNAMIC 35 | 36 | **Type:** String = "DYNAMIC" 37 | 38 | Reserved for future use. 39 | 40 | ### IMPEX 41 | 42 | **Type:** String = "IMPEX" 43 | 44 | Import/export root directory. 45 | 46 | ### LIBRARIES 47 | 48 | **Type:** String = "LIBRARIES" 49 | 50 | Libraries root directory. 51 | 52 | ### REALMDATA 53 | 54 | **Type:** String = "REALMDATA" 55 | 56 | RealmData root directory. 57 | 58 | ### SEPARATOR 59 | 60 | **Type:** String = "/" 61 | 62 | The UNIX style '/' path separator, which must be used for files paths. 63 | 64 | ### STATIC 65 | 66 | **Type:** String = "STATIC" 67 | 68 | Static content root directory. 69 | 70 | ### TEMP 71 | 72 | **Type:** String = "TEMP" 73 | 74 | Temp root directory. 75 | 76 | ## Properties 77 | 78 | ### directory 79 | 80 | **Type:** boolean (Read Only) 81 | 82 | Indicates that this file is a directory. 83 | 84 | ### file 85 | 86 | **Type:** boolean (Read Only) 87 | 88 | Indicates if this file is a file. 89 | 90 | ### fullPath 91 | 92 | **Type:** String (Read Only) 93 | 94 | Return the full file path denoted by this File. 95 | This value will be the same regardless of which constructor was 96 | used to create this File. 97 | 98 | ### name 99 | 100 | **Type:** String (Read Only) 101 | 102 | The name of the file or directory denoted by this object. This is 103 | just the last name in the pathname's name sequence. If the pathname's 104 | name sequence is empty, then the empty string is returned. 105 | 106 | ### path 107 | 108 | **Type:** String (Read Only) 109 | 110 | The portion of the path relative to the root directory. 111 | 112 | ### rootDirectoryType 113 | 114 | **Type:** String (Read Only) 115 | 116 | The root directory type, e.g. "IMPEX" represented by this 117 | File. 118 | 119 | ## Constructor Summary 120 | 121 | File(absPath : String) Creates a File from the given absolute file path in the file namespace. 122 | 123 | File(rootDir : File, relPath : String) Creates a File given a root directory and a relative path. 124 | 125 | ## Method Summary 126 | 127 | ### copyTo 128 | 129 | **Signature:** `copyTo(file : File) : File` 130 | 131 | Copy a file. 132 | 133 | ### createNewFile 134 | 135 | **Signature:** `createNewFile() : boolean` 136 | 137 | Create file. 138 | 139 | ### exists 140 | 141 | **Signature:** `exists() : boolean` 142 | 143 | Indicates if the file exists. 144 | 145 | ### getFullPath 146 | 147 | **Signature:** `getFullPath() : String` 148 | 149 | Return the full file path denoted by this File. 150 | 151 | ### getName 152 | 153 | **Signature:** `getName() : String` 154 | 155 | Returns the name of the file or directory denoted by this object. 156 | 157 | ### getPath 158 | 159 | **Signature:** `getPath() : String` 160 | 161 | Returns the portion of the path relative to the root directory. 162 | 163 | ### getRootDirectory 164 | 165 | **Signature:** `static getRootDirectory(rootDir : String, args : String...) : File` 166 | 167 | Returns a File representing a directory for the specified root directory type. 168 | 169 | ### getRootDirectoryType 170 | 171 | **Signature:** `getRootDirectoryType() : String` 172 | 173 | Returns the root directory type, e.g. 174 | 175 | ### gunzip 176 | 177 | **Signature:** `gunzip(root : File) : void` 178 | 179 | Assumes this instance is a gzip file. 180 | 181 | ### gzip 182 | 183 | **Signature:** `gzip(outputZipFile : File) : void` 184 | 185 | GZip this instance into a new gzip file. 186 | 187 | ### isDirectory 188 | 189 | **Signature:** `isDirectory() : boolean` 190 | 191 | Indicates that this file is a directory. 192 | 193 | ### isFile 194 | 195 | **Signature:** `isFile() : boolean` 196 | 197 | Indicates if this file is a file. 198 | 199 | ### lastModified 200 | 201 | **Signature:** `lastModified() : Number` 202 | 203 | Return the time, in milliseconds, that this file was last modified. 204 | 205 | ### length 206 | 207 | **Signature:** `length() : Number` 208 | 209 | Return the length of the file in bytes. 210 | 211 | ### list 212 | 213 | **Signature:** `list() : String[]` 214 | 215 | Returns an array of strings naming the files and directories in the directory denoted by this object. 216 | 217 | ### listFiles 218 | 219 | **Signature:** `listFiles() : List` 220 | 221 | Returns an array of File objects in the directory denoted by this File. 222 | 223 | ### listFiles 224 | 225 | **Signature:** `listFiles(filter : Function) : List` 226 | 227 | Returns an array of File objects denoting the files and directories in the directory denoted by this object that satisfy the specified filter. 228 | 229 | ### md5 230 | 231 | **Signature:** `md5() : String` 232 | 233 | Returns an MD5 hash of the content of the file of this instance. 234 | 235 | ### mkdir 236 | 237 | **Signature:** `mkdir() : boolean` 238 | 239 | Creates a directory. 240 | 241 | ### mkdirs 242 | 243 | **Signature:** `mkdirs() : boolean` 244 | 245 | Creates a directory, including, its parent directories, as needed. 246 | 247 | ### remove 248 | 249 | **Signature:** `remove() : boolean` 250 | 251 | Deletes the file or directory denoted by this object. 252 | 253 | ### renameTo 254 | 255 | **Signature:** `renameTo(file : File) : boolean` 256 | 257 | Rename file. 258 | 259 | ### unzip 260 | 261 | **Signature:** `unzip(root : File) : void` 262 | 263 | Assumes this instance is a zip file. 264 | 265 | ### zip 266 | 267 | **Signature:** `zip(outputZipFile : File) : void` 268 | 269 | Zip this instance into a new zip file. 270 | 271 | ## Constructor Detail 272 | 273 | ## Method Detail 274 | 275 | ## Method Details 276 | 277 | ### copyTo 278 | 279 | **Signature:** `copyTo(file : File) : File` 280 | 281 | **Description:** Copy a file. Directories cannot be copied. This method cannot be used from storefront requests. 282 | 283 | **Parameters:** 284 | 285 | - `file`: the File object to copy to 286 | 287 | **Returns:** 288 | 289 | a reference to the copied file. 290 | 291 | **Throws:** 292 | 293 | IOException - if there is an interruption during file copy. 294 | FileAlreadyExistsException - if the file to copy to already exists 295 | UnsupportedOperationException - if invoked from a storefront request 296 | 297 | --- 298 | 299 | ### createNewFile 300 | 301 | **Signature:** `createNewFile() : boolean` 302 | 303 | **Description:** Create file. 304 | 305 | **Returns:** 306 | 307 | boolean, true - if file has been created, false - file already exists 308 | 309 | **Throws:** 310 | 311 | - Exception 312 | 313 | --- 314 | 315 | ### exists 316 | 317 | **Signature:** `exists() : boolean` 318 | 319 | **Description:** Indicates if the file exists. 320 | 321 | **Returns:** 322 | 323 | true if file exists, false otherwise. 324 | 325 | --- 326 | 327 | ### getFullPath 328 | 329 | **Signature:** `getFullPath() : String` 330 | 331 | **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. 332 | 333 | **Returns:** 334 | 335 | the full file path. 336 | 337 | --- 338 | 339 | ### getName 340 | 341 | **Signature:** `getName() : String` 342 | 343 | **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. 344 | 345 | **Returns:** 346 | 347 | The name of the file or directory denoted by this object. 348 | 349 | --- 350 | 351 | ### getPath 352 | 353 | **Signature:** `getPath() : String` 354 | 355 | **Description:** Returns the portion of the path relative to the root directory. 356 | 357 | **Deprecated:** 358 | 359 | Use getFullPath() to access the full path. This method does not return the correct path for files in the CATALOGS or LIBRARIES virtual directories. 360 | 361 | **Returns:** 362 | 363 | the relative file path, possibly blank but not null. 364 | 365 | --- 366 | 367 | ### getRootDirectory 368 | 369 | **Signature:** `static getRootDirectory(rootDir : String, args : String...) : File` 370 | 371 | **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. 372 | 373 | **Parameters:** 374 | 375 | - `rootDir`: root directory type (see the constants defined in this class) 376 | - `args`: root directory specific arguments 377 | 378 | **Returns:** 379 | 380 | File object representing the directory 381 | 382 | --- 383 | 384 | ### getRootDirectoryType 385 | 386 | **Signature:** `getRootDirectoryType() : String` 387 | 388 | **Description:** Returns the root directory type, e.g. "IMPEX" represented by this File. 389 | 390 | **Returns:** 391 | 392 | root directory type 393 | 394 | --- 395 | 396 | ### gunzip 397 | 398 | **Signature:** `gunzip(root : File) : void` 399 | 400 | **Description:** Assumes this instance is a gzip file. Unzipping it will explode the contents in the directory passed in (root). 401 | 402 | **Parameters:** 403 | 404 | - `root`: a File indicating root. root must be a directory. 405 | 406 | **Throws:** 407 | 408 | Exception - if the zip files contents can't be exploded. 409 | 410 | --- 411 | 412 | ### gzip 413 | 414 | **Signature:** `gzip(outputZipFile : File) : void` 415 | 416 | **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. 417 | 418 | **Parameters:** 419 | 420 | - `outputZipFile`: the zip file created. 421 | 422 | **Throws:** 423 | 424 | IOException - if the zip file can't be created. 425 | 426 | --- 427 | 428 | ### isDirectory 429 | 430 | **Signature:** `isDirectory() : boolean` 431 | 432 | **Description:** Indicates that this file is a directory. 433 | 434 | **Returns:** 435 | 436 | true if the file is a directory, false otherwise. 437 | 438 | --- 439 | 440 | ### isFile 441 | 442 | **Signature:** `isFile() : boolean` 443 | 444 | **Description:** Indicates if this file is a file. 445 | 446 | **Returns:** 447 | 448 | true if the file is a file, false otherwise. 449 | 450 | --- 451 | 452 | ### lastModified 453 | 454 | **Signature:** `lastModified() : Number` 455 | 456 | **Description:** Return the time, in milliseconds, that this file was last modified. 457 | 458 | **Returns:** 459 | 460 | the time, in milliseconds, that this file was last modified. 461 | 462 | --- 463 | 464 | ### length 465 | 466 | **Signature:** `length() : Number` 467 | 468 | **Description:** Return the length of the file in bytes. 469 | 470 | **Returns:** 471 | 472 | the file length in bytes. 473 | 474 | --- 475 | 476 | ### list 477 | 478 | **Signature:** `list() : String[]` 479 | 480 | **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. 481 | 482 | **Returns:** 483 | 484 | 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. 485 | 486 | --- 487 | 488 | ### listFiles 489 | 490 | **Signature:** `listFiles() : List` 491 | 492 | **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); 493 | 494 | **Returns:** 495 | 496 | a list of File objects or null if this is not a directory. 497 | 498 | --- 499 | 500 | ### listFiles 501 | 502 | **Signature:** `listFiles(filter : Function) : List` 503 | 504 | **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); 505 | 506 | **Parameters:** 507 | 508 | - `filter`: a Javascript function which accepts a File argument and returns true or false. 509 | 510 | **Returns:** 511 | 512 | list of File objects or null if this is not a directory 513 | 514 | --- 515 | 516 | ### md5 517 | 518 | **Signature:** `md5() : String` 519 | 520 | **Description:** Returns an MD5 hash of the content of the file of this instance. 521 | 522 | **Returns:** 523 | 524 | The MD5 hash of the file's content. 525 | 526 | **Throws:** 527 | 528 | Exception - if the file could not be read or is a directory. 529 | 530 | --- 531 | 532 | ### mkdir 533 | 534 | **Signature:** `mkdir() : boolean` 535 | 536 | **Description:** Creates a directory. 537 | 538 | **Returns:** 539 | 540 | true if file creation succeeded, false otherwise. 541 | 542 | --- 543 | 544 | ### mkdirs 545 | 546 | **Signature:** `mkdirs() : boolean` 547 | 548 | **Description:** Creates a directory, including, its parent directories, as needed. 549 | 550 | **Returns:** 551 | 552 | true if file creation succeeded, false otherwise. 553 | 554 | --- 555 | 556 | ### remove 557 | 558 | **Signature:** `remove() : boolean` 559 | 560 | **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. 561 | 562 | **Returns:** 563 | 564 | true if file deletion succeeded, false otherwise 565 | 566 | --- 567 | 568 | ### renameTo 569 | 570 | **Signature:** `renameTo(file : File) : boolean` 571 | 572 | **Description:** Rename file. 573 | 574 | **Parameters:** 575 | 576 | - `file`: the File object to rename to 577 | 578 | **Returns:** 579 | 580 | boolean, true - if file rename succeeded, false - failed 581 | 582 | --- 583 | 584 | ### unzip 585 | 586 | **Signature:** `unzip(root : File) : void` 587 | 588 | **Description:** Assumes this instance is a zip file. Unzipping it will explode the contents in the directory passed in (root). 589 | 590 | **Parameters:** 591 | 592 | - `root`: a File indicating root. root must be a directory. 593 | 594 | **Throws:** 595 | 596 | Exception - if the zip files contents can't be exploded. 597 | 598 | --- 599 | 600 | ### zip 601 | 602 | **Signature:** `zip(outputZipFile : File) : void` 603 | 604 | **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. 605 | 606 | **Parameters:** 607 | 608 | - `outputZipFile`: the zip file created. 609 | 610 | **Throws:** 611 | 612 | IOException - if the zip file can't be created. 613 | 614 | --- ``` -------------------------------------------------------------------------------- /docs/dw_system/Response.md: -------------------------------------------------------------------------------- ```markdown 1 | ## Package: dw.system 2 | 3 | # Class Response 4 | 5 | ## Inheritance Hierarchy 6 | 7 | - Object 8 | - dw.system.Response 9 | 10 | ## Description 11 | 12 | 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. 13 | 14 | ## Constants 15 | 16 | ### ACCESS_CONTROL_ALLOW_CREDENTIALS 17 | 18 | **Type:** String = "Access-Control-Allow-Credentials" 19 | 20 | An allowed header name constant for Access-Control-Allow-Credentials 21 | 22 | ### ACCESS_CONTROL_ALLOW_HEADERS 23 | 24 | **Type:** String = "Access-Control-Allow-Headers" 25 | 26 | An allowed header name constant for Access-Control-Allow-Headers 27 | 28 | ### ACCESS_CONTROL_ALLOW_METHODS 29 | 30 | **Type:** String = "Access-Control-Allow-Methods" 31 | 32 | An allowed header name constant for Access-Control-Allow-Methods 33 | 34 | ### ACCESS_CONTROL_ALLOW_ORIGIN 35 | 36 | **Type:** String = "Access-Control-Allow-Origin" 37 | 38 | An allowed header name constant for Access-Control-Allow-Origin 39 | 40 | ### ACCESS_CONTROL_EXPOSE_HEADERS 41 | 42 | **Type:** String = "Access-Control-Expose-Headers" 43 | 44 | An allowed header name constant for Access-Control-Expose-Headers 45 | 46 | ### ALLOW 47 | 48 | **Type:** String = "Allow" 49 | 50 | An allowed header name constant for Allow 51 | 52 | ### CONTENT_DISPOSITION 53 | 54 | **Type:** String = "Content-Disposition" 55 | 56 | An allowed header name constant for Content-Disposition 57 | 58 | ### CONTENT_LANGUAGE 59 | 60 | **Type:** String = "Content-Language" 61 | 62 | An allowed header name constant for Content-Language 63 | 64 | ### CONTENT_LOCATION 65 | 66 | **Type:** String = "Content-Location" 67 | 68 | An allowed header name constant for Content-Location 69 | 70 | ### CONTENT_MD5 71 | 72 | **Type:** String = "Content-MD5" 73 | 74 | An allowed header name constant for Content-MD5 75 | 76 | ### CONTENT_SECURITY_POLICY 77 | 78 | **Type:** String = "Content-Security-Policy" 79 | 80 | An allowed header name constant for Content-Security-Policy. Note: The Commerce Cloud platform can override this header for tools like the Storefront Toolkit. 81 | 82 | ### CONTENT_SECURITY_POLICY_REPORT_ONLY 83 | 84 | **Type:** String = "Content-Security-Policy-Report-Only" 85 | 86 | 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. 87 | 88 | ### CONTENT_TYPE 89 | 90 | **Type:** String = "Content-Type" 91 | 92 | An allowed header name constant for Content-Type 93 | 94 | ### CROSS_ORIGIN_EMBEDDER_POLICY 95 | 96 | **Type:** String = "Cross-Origin-Embedder-Policy" 97 | 98 | An allowed header name constant for Cross-Origin-Embedder-Policy 99 | 100 | ### CROSS_ORIGIN_EMBEDDER_POLICY_REPORT_ONLY 101 | 102 | **Type:** String = "Cross-Origin-Embedder-Policy-Report-Only" 103 | 104 | 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. 105 | 106 | ### CROSS_ORIGIN_OPENER_POLICY 107 | 108 | **Type:** String = "Cross-Origin-Opener-Policy" 109 | 110 | An allowed header name constant for Cross-Origin-Opener-Policy 111 | 112 | ### CROSS_ORIGIN_OPENER_POLICY_REPORT_ONLY 113 | 114 | **Type:** String = "Cross-Origin-Opener-Policy-Report-Only" 115 | 116 | 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. 117 | 118 | ### CROSS_ORIGIN_RESOURCE_POLICY 119 | 120 | **Type:** String = "Cross-Origin-Resource-Policy" 121 | 122 | An allowed header name constant for Cross-Origin-Resource-Policy 123 | 124 | ### LINK 125 | 126 | **Type:** String = "Link" 127 | 128 | An allowed header name constant for Link 129 | 130 | ### LOCATION 131 | 132 | **Type:** String = "Location" 133 | 134 | An allowed header name constant for Location 135 | 136 | ### PERMISSIONS_POLICY 137 | 138 | **Type:** String = "Permissions-Policy" 139 | 140 | An allowed header name constant for Permissions-Policy 141 | 142 | ### PLATFORM_FOR_PRIVACY_PREFERENCES_PROJECT 143 | 144 | **Type:** String = "P3P" 145 | 146 | An allowed header name constant for Platform for Privacy Preferences Project 147 | 148 | ### REFERRER_POLICY 149 | 150 | **Type:** String = "Referrer-Policy" 151 | 152 | An allowed header name constant for Referrer-Policy 153 | 154 | ### REFRESH 155 | 156 | **Type:** String = "Refresh" 157 | 158 | An allowed header name constant for Refresh 159 | 160 | ### RETRY_AFTER 161 | 162 | **Type:** String = "Retry-After" 163 | 164 | An allowed header name constant for Retry-After 165 | 166 | ### SERVICE_WORKER_ALLOWED 167 | 168 | **Type:** String = "service-worker-allowed" 169 | 170 | An allowed header name constant for service-worker-allowed 171 | 172 | ### VARY 173 | 174 | **Type:** String = "Vary" 175 | 176 | An allowed header name constant for Vary 177 | 178 | ### X_CONTENT_TYPE_OPTIONS 179 | 180 | **Type:** String = "X-Content-Type-Options" 181 | 182 | An allowed header name constant for X-Content-Type-Options 183 | 184 | ### X_FRAME_OPTIONS 185 | 186 | **Type:** String = "X-FRAME-OPTIONS" 187 | 188 | An allowed header name constant for X-FRAME-OPTIONS. Note: The Commerce Cloud platform can override this header for tools like the Storefront Toolkit. 189 | 190 | ### X_FRAME_OPTIONS_ALLOW_FROM 191 | 192 | **Type:** String = "ALLOW-FROM" 193 | 194 | An allowed value ALLOW-FROM for X-FRAME-OPTIONS 195 | 196 | ### X_FRAME_OPTIONS_DENY_VALUE 197 | 198 | **Type:** String = "DENY" 199 | 200 | An allowed value DENY for X-FRAME-OPTIONS 201 | 202 | ### X_FRAME_OPTIONS_SAMEORIGIN_VALUE 203 | 204 | **Type:** String = "SAMEORIGIN" 205 | 206 | An allowed value SAME-ORIGIN value for X-FRAME-OPTIONS 207 | 208 | ### X_ROBOTS_TAG 209 | 210 | **Type:** String = "X-Robots-Tag" 211 | 212 | An allowed header name constant for X-Robots-Tag 213 | 214 | ### X_XSS_PROTECTION 215 | 216 | **Type:** String = "X-XSS-Protection" 217 | 218 | An allowed header name constant for X-XSS-Protection 219 | 220 | ## Properties 221 | 222 | ### writer 223 | 224 | **Type:** PrintWriter (Read Only) 225 | 226 | A print writer which can be used to print content directly to the response. 227 | 228 | ## Constructor Summary 229 | 230 | ## Method Summary 231 | 232 | ### addHttpCookie 233 | 234 | **Signature:** `addHttpCookie(cookie : Cookie) : void` 235 | 236 | Adds the specified cookie to the outgoing response. 237 | 238 | ### addHttpHeader 239 | 240 | **Signature:** `addHttpHeader(name : String, value : String) : void` 241 | 242 | Adds a response header with the given name and value. 243 | 244 | ### containsHttpHeader 245 | 246 | **Signature:** `containsHttpHeader(name : String) : boolean` 247 | 248 | Checks whether the response message header has a field with the specified name. 249 | 250 | ### getWriter 251 | 252 | **Signature:** `getWriter() : PrintWriter` 253 | 254 | Returns a print writer which can be used to print content directly to the response. 255 | 256 | ### redirect 257 | 258 | **Signature:** `redirect(url : URL) : void` 259 | 260 | Sends a temporary redirect response (HTTP status 302) to the client for the specified redirect location URL. 261 | 262 | ### redirect 263 | 264 | **Signature:** `redirect(url : URL, status : Number) : void` 265 | 266 | Sends a redirect response with the given status to the client for the specified redirect location URL. 267 | 268 | ### redirect 269 | 270 | **Signature:** `redirect(location : String) : void` 271 | 272 | Sends a temporary redirect response (HTTP status 302) to the client for the specified redirect location URL. 273 | 274 | ### redirect 275 | 276 | **Signature:** `redirect(location : String, status : Number) : void` 277 | 278 | Sends a redirect response with the given status to the client for the specified redirect location URL. 279 | 280 | ### redirect 281 | 282 | **Signature:** `redirect(redirect : URLRedirect) : void` 283 | 284 | Sends a redirect response with the given status to the client for the specified redirect location URL. 285 | 286 | ### setBuffered 287 | 288 | **Signature:** `setBuffered(buffered : boolean) : void` 289 | 290 | Sets whether the output should be buffered or streamed directly to the client. 291 | 292 | ### setContentType 293 | 294 | **Signature:** `setContentType(contentType : String) : void` 295 | 296 | Sets the content type for this response. 297 | 298 | ### setExpires 299 | 300 | **Signature:** `setExpires(expires : Number) : void` 301 | 302 | Sets the cache expiration time for the response. 303 | 304 | ### setExpires 305 | 306 | **Signature:** `setExpires(expires : Date) : void` 307 | 308 | Convenience method for setExpires(Number) which takes a Date object. 309 | 310 | ### setHttpHeader 311 | 312 | **Signature:** `setHttpHeader(name : String, value : String) : void` 313 | 314 | Adds a response header with the given name and value. 315 | 316 | ### setStatus 317 | 318 | **Signature:** `setStatus(status : Number) : void` 319 | 320 | Sets the HTTP response code. 321 | 322 | ### setVaryBy 323 | 324 | **Signature:** `setVaryBy(varyBy : String) : void` 325 | 326 | Marks the response as personalized with the given variant identifier. 327 | 328 | ## Method Detail 329 | 330 | ## Method Details 331 | 332 | ### addHttpCookie 333 | 334 | **Signature:** `addHttpCookie(cookie : Cookie) : void` 335 | 336 | **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. 337 | 338 | **Parameters:** 339 | 340 | - `cookie`: a Cookie object 341 | 342 | --- 343 | 344 | ### addHttpHeader 345 | 346 | **Signature:** `addHttpHeader(name : String, value : String) : void` 347 | 348 | **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. 349 | 350 | **Parameters:** 351 | 352 | - `name`: the name to use for the response header. 353 | - `value`: the value to use. 354 | 355 | --- 356 | 357 | ### containsHttpHeader 358 | 359 | **Signature:** `containsHttpHeader(name : String) : boolean` 360 | 361 | **Description:** Checks whether the response message header has a field with the specified name. 362 | 363 | **Parameters:** 364 | 365 | - `name`: the name to use. 366 | 367 | --- 368 | 369 | ### getWriter 370 | 371 | **Signature:** `getWriter() : PrintWriter` 372 | 373 | **Description:** Returns a print writer which can be used to print content directly to the response. 374 | 375 | --- 376 | 377 | ### redirect 378 | 379 | **Signature:** `redirect(url : URL) : void` 380 | 381 | **Description:** Sends a temporary redirect response (HTTP status 302) to the client for the specified redirect location URL. 382 | 383 | **Parameters:** 384 | 385 | - `url`: the URL object for the target location, must be not null 386 | 387 | --- 388 | 389 | ### redirect 390 | 391 | **Signature:** `redirect(url : URL, status : Number) : void` 392 | 393 | **Description:** Sends a redirect response with the given status to the client for the specified redirect location URL. 394 | 395 | **Parameters:** 396 | 397 | - `url`: the URL object with the redirect location, must be not null 398 | - `status`: the status code for this redirect, must be 301, 302 or 307 399 | 400 | --- 401 | 402 | ### redirect 403 | 404 | **Signature:** `redirect(location : String) : void` 405 | 406 | **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. 407 | 408 | **Parameters:** 409 | 410 | - `location`: the target location as a string, must be not empty 411 | 412 | --- 413 | 414 | ### redirect 415 | 416 | **Signature:** `redirect(location : String, status : Number) : void` 417 | 418 | **Description:** Sends a redirect response with the given status to the client for the specified redirect location URL. 419 | 420 | **Parameters:** 421 | 422 | - `location`: the redirect location, must be not empty 423 | - `status`: the status code for this redirect, must be 301, 302 or 307 424 | 425 | --- 426 | 427 | ### redirect 428 | 429 | **Signature:** `redirect(redirect : URLRedirect) : void` 430 | 431 | **Description:** Sends a redirect response with the given status to the client for the specified redirect location URL. 432 | 433 | **Parameters:** 434 | 435 | - `redirect`: the URLRedirect object with the location and status, must be not null 436 | 437 | --- 438 | 439 | ### setBuffered 440 | 441 | **Signature:** `setBuffered(buffered : boolean) : void` 442 | 443 | **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. 444 | 445 | **Parameters:** 446 | 447 | - `buffered`: if true, buffering is used, if false the response will be streamed 448 | 449 | --- 450 | 451 | ### setContentType 452 | 453 | **Signature:** `setContentType(contentType : String) : void` 454 | 455 | **Description:** Sets the content type for this response. This method may only be called before any output is written to the response. 456 | 457 | **Parameters:** 458 | 459 | - `contentType`: the MIME type of the content, like "text/html", "application/json" etc. 460 | 461 | --- 462 | 463 | ### setExpires 464 | 465 | **Signature:** `setExpires(expires : Number) : void` 466 | 467 | **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. 468 | 469 | **Parameters:** 470 | 471 | - `expires`: the expiration time in milliseconds since January 1, 1970, 00:00:00 GMT 472 | 473 | --- 474 | 475 | ### setExpires 476 | 477 | **Signature:** `setExpires(expires : Date) : void` 478 | 479 | **Description:** Convenience method for setExpires(Number) which takes a Date object. 480 | 481 | **Parameters:** 482 | 483 | - `expires`: a Date object. 484 | 485 | --- 486 | 487 | ### setHttpHeader 488 | 489 | **Signature:** `setHttpHeader(name : String, value : String) : void` 490 | 491 | **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. 492 | 493 | **Parameters:** 494 | 495 | - `name`: the name to use for the response header. 496 | - `value`: the value to use. 497 | 498 | --- 499 | 500 | ### setStatus 501 | 502 | **Signature:** `setStatus(status : Number) : void` 503 | 504 | **Description:** Sets the HTTP response code. 505 | 506 | **Parameters:** 507 | 508 | - `status`: a standard-conform HTTP status code, for example 200 for "OK" 509 | 510 | --- 511 | 512 | ### setVaryBy 513 | 514 | **Signature:** `setVaryBy(varyBy : String) : void` 515 | 516 | **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. 517 | 518 | **Parameters:** 519 | 520 | - `varyBy`: the variation criteria, currently only "price_promotion" is supported, any other value has no effect 521 | 522 | --- ``` -------------------------------------------------------------------------------- /docs/best-practices/ocapi_hooks.md: -------------------------------------------------------------------------------- ```markdown 1 | # Quick Guide: Salesforce B2C Commerce OCAPI Hooks 2 | 3 | This guide provides best practices and examples for implementing OCAPI hooks in Salesforce B2C Commerce Cloud. 4 | 5 | **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. 6 | 7 | ## 1. Core Concepts 8 | 9 | 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. 10 | 11 | ### Hook Types & Execution Order 12 | 13 | There are three main hook types, executed in a specific order for state-changing requests (POST, PATCH, etc.): 14 | 15 | #### `before<HTTP_Method>` 16 | Executes before core platform logic. 17 | 18 | - **Use Case**: Input validation, data preprocessing, custom authorization checks. 19 | - **Context**: Runs within the database transaction. Can modify the incoming request document. 20 | 21 | #### `after<HTTP_Method>` 22 | Executes after core platform logic succeeds but before the response is generated. 23 | 24 | - **Use Case**: Business logic side effects, such as calling an external ERP/OMS, triggering basket recalculation, or saving data to custom objects. 25 | - **Context**: Runs within the same database transaction. Operates on persistent Script API objects (e.g., `dw.order.Basket`). 26 | 27 | #### `modify<HTTP_Method>Response` 28 | Executes last, after the platform generates the JSON response. 29 | 30 | - **Use Case**: Final formatting of the JSON response. Add, remove, or reformat attributes (especially `c_` custom attributes) before sending to the client. 31 | - **Context**: NOT transactional. Attempting to modify persistent data (e.g., `basket.setCustomerEmail()`) will throw an `ORMTransactionException`. 32 | 33 | | Hook Type | Transactional? | Can Modify Persistent Data? | Primary Purpose | 34 | |-----------|---------------|----------------------------|-----------------| 35 | | before | Yes | Yes | Validation & Preprocessing | 36 | | after | Yes | Yes | Business Logic & Side Effects | 37 | | modifyResponse | No | No | Formatting the JSON Response | 38 | 39 | ## 2. Registration 40 | 41 | Hooks are registered in a custom cartridge via two files. 42 | 43 | ### `package.json` (Cartridge Root) 44 | 45 | This file points to your hooks configuration file. 46 | 47 | ```json 48 | { 49 | "name": "my-hooks-cartridge", 50 | "hooks": "./cartridge/hooks.json" 51 | } 52 | ``` 53 | 54 | ### `hooks.json` (e.g., `/cartridge/hooks.json`) 55 | 56 | This file maps the official hook extension point name to your script file. 57 | 58 | ```json 59 | { 60 | "hooks": [ 61 | { 62 | "name": "dw.ocapi.shop.customer.address.beforePATCH", 63 | "script": "./customer/addressValidation.js" 64 | }, 65 | { 66 | "name": "dw.ocapi.shop.customer.modifyGETResponse", 67 | "script": "./customer/enrichCustomerResponse.js" 68 | }, 69 | { 70 | "name": "dw.ocapi.shop.order.afterPOST", 71 | "script": "./order/notifyOms.js" 72 | } 73 | ] 74 | } 75 | ``` 76 | 77 | ### Recommended Cartridge Structure 78 | 79 | Organize hook scripts by the resource they modify for better maintainability. 80 | 81 | ``` 82 | /cartridge 83 | └──hooks.json 84 | └──/scripts 85 | └──/hooks 86 | ├──/customer 87 | │ ├── addressValidation.js 88 | │ └── enrichCustomerResponse.js 89 | └──/order 90 | └── notifyOms.js 91 | ``` 92 | 93 | ## 3. Core Implementation Patterns 94 | 95 | ### Script Structure (CommonJS) 96 | 97 | Hook scripts are CommonJS modules. The function name must be exported and match the hook's method name (e.g., `afterPOST`). 98 | 99 | ```javascript 100 | 'use strict'; 101 | var Status = require('dw/system/Status'); 102 | 103 | /** 104 | * @param {dw.customer.Customer} customer - The customer object. 105 | * @param {Object} customerResponse - The response document to be modified. 106 | * @returns {dw.system.Status} - A status object. 107 | */ 108 | exports.modifyGETResponse = function (customer, customerResponse) { 109 | // Your logic here 110 | return new Status(Status.OK); 111 | }; 112 | ``` 113 | 114 | ### Signaling Success vs. Failure (`dw.system.Status`) 115 | 116 | Use the Status object to control the execution flow. 117 | 118 | - **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). 119 | - **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. 120 | 121 | ### Data Integrity (`dw.system.Transaction`) 122 | 123 | All modifications to persistent data in `before` or `after` hooks must be wrapped in a transaction. 124 | 125 | ```javascript 126 | var Transaction = require('dw/system/Transaction'); 127 | 128 | Transaction.wrap(function () { 129 | customer.getProfile().custom.lastAddressChange = new Date(); 130 | }); 131 | ``` 132 | 133 | ## 4. Code Examples 134 | 135 | ### Example 1: Custom Validation (before hook) 136 | 137 | Reject an address update if the US postal code format is invalid. 138 | 139 | **Hook**: `dw.ocapi.shop.customer.address.beforePATCH` 140 | **Script**: `cartridge/scripts/hooks/customer/addressValidation.js` 141 | 142 | ```javascript 143 | 'use strict'; 144 | 145 | var Status = require('dw/system/Status'); 146 | 147 | exports.beforePATCH = function (customer, addressName, customerAddress) { 148 | var countryCode = customerAddress.country_code; 149 | var postalCode = customerAddress.postal_code; 150 | 151 | if (countryCode === 'US' && postalCode) { 152 | var postalCodeRegex = /^\d{5}(-\d{4})?$/; 153 | if (!postalCodeRegex.test(postalCode)) { 154 | // Reject the request with a specific error 155 | return new Status(Status.ERROR, 'INVALID_POSTAL_CODE', 'The postal code format is invalid for the United States.'); 156 | } 157 | } 158 | return new Status(Status.OK); 159 | }; 160 | ``` 161 | 162 | ### Example 2: Enriching a Response (modifyResponse hook) 163 | 164 | Add a custom flag `c_isPreferredCustomer` to the customer GET response. 165 | 166 | **Hook**: `dw.ocapi.shop.customer.modifyGETResponse` 167 | **Script**: `cartridge/scripts/hooks/customer/enrichCustomerResponse.js` 168 | 169 | ```javascript 170 | 'use strict'; 171 | 172 | var Status = require('dw/system/Status'); 173 | 174 | exports.modifyGETResponse = function (customer, customerResponse) { 175 | // Logic to determine if the customer is preferred 176 | var isPreferred = customer.isMemberOfCustomerGroup('Preferred'); 177 | 178 | // Add a custom attribute directly to the response document. 179 | // This does NOT save anything to the database. 180 | customerResponse.c_isPreferredCustomer = isPreferred; 181 | 182 | return new Status(Status.OK); 183 | }; 184 | ``` 185 | 186 | ### Example 3: External Service Integration (after hook) 187 | 188 | Notify an external Order Management System (OMS) after an order is created. 189 | 190 | **Hook**: `dw.ocapi.shop.order.afterPOST` 191 | **Script**: `cartridge/scripts/hooks/order/notifyOms.js` 192 | 193 | ```javascript 194 | 'use strict'; 195 | 196 | var Status = require('dw/system/Status'); 197 | var LocalServiceRegistry = require('dw/svc/LocalServiceRegistry'); 198 | var Logger = require('dw/system/Logger').getLogger('OmsIntegrationHook'); 199 | 200 | exports.afterPOST = function (order) { 201 | try { 202 | var omsService = LocalServiceRegistry.createService('oms.http.service', { /* service config */ }); 203 | var payload = { orderNo: order.getOrderNo(), total: order.getTotalGrossPrice().getValue() }; 204 | var result = omsService.call({ payload: payload }); 205 | 206 | if (!result.isOk()) { 207 | // Log the error for monitoring, but don't return Status.ERROR. 208 | // The order is already created; returning an error here would be misleading to the client. 209 | Logger.error('Failed to notify OMS for order {0}. Error: {1}', order.getOrderNo(), result.getErrorMessage()); 210 | } 211 | } catch (e) { 212 | Logger.error('Exception notifying OMS for order {0}. Exception: {1}', order.getOrderNo(), e.toString()); 213 | } 214 | 215 | // Always return OK because the primary operation (order creation) was successful. 216 | return new Status(Status.OK); 217 | }; 218 | ``` 219 | 220 | ## 5. Key Best Practices 221 | 222 | ### Performance 223 | 224 | - **DON'T** perform expensive database lookups inside a hook (e.g., `ProductMgr.getProduct()`). 225 | - **DO** be aware of caching. Hooks on cacheable GET endpoints only run on a cache miss. 226 | - **DO** keep hook logic simple and efficient. 227 | 228 | ### Security 229 | 230 | - **DO** treat all client input as untrusted. Sanitize and validate data in before hooks. 231 | - **DO** re-authorize sensitive actions within the hook. For example, use `OrderMgr.getOrder(orderNumber, orderToken)` instead of just `OrderMgr.getOrder(orderNumber)`. 232 | 233 | ### Error Handling & Resilience 234 | 235 | - **DO** wrap all hook logic in `try/catch` blocks. 236 | - **DO** use `dw.system.Logger` with custom categories and include the `request.requestID` for easy tracing. 237 | - **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. 238 | 239 | ### Testing 240 | 241 | - **DO** use the `dw-api-mock` library to unit test hook logic locally in a Node.js environment. 242 | - **DO** use API clients like Postman for integration testing on a sandbox. 243 | 244 | ## 6. Comprehensive Hook Reference 245 | 246 | ### Shop API Hooks 247 | 248 | | API Endpoint (Method & Path) | Available Hook Extension Points | 249 | |------------------------------|----------------------------------| 250 | | **Authentication** | | 251 | | `POST /customers/auth` | `dw.ocapi.shop.auth.beforePOST`, `dw.ocapi.shop.auth.afterPOST`, `dw.ocapi.shop.auth.modifyPOSTResponse` | 252 | | **Basket** | | 253 | | `POST /baskets` | `dw.ocapi.shop.basket.beforePOST_v2`, `dw.ocapi.shop.basket.afterPOST`, `dw.ocapi.shop.basket.modifyPOSTResponse`, `dw.ocapi.shop.basket.validateBasket` | 254 | | `GET /baskets/{basket_id}` | `dw.ocapi.shop.basket.beforeGET`, `dw.ocapi.shop.basket.modifyGETResponse`, `dw.ocapi.shop.basket.validateBasket` | 255 | | `PATCH /baskets/{basket_id}` | `dw.ocapi.shop.basket.beforePATCH`, `dw.ocapi.shop.basket.afterPATCH`, `dw.ocapi.shop.basket.modifyPATCHResponse`, `dw.ocapi.shop.basket.validateBasket` | 256 | | `DELETE /baskets/{basket_id}` | `dw.ocapi.shop.basket.beforeDELETE`, `dw.ocapi.shop.basket.afterDELETE` | 257 | | `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` | 258 | | `POST /baskets/{basket_id}/coupons` | `dw.ocapi.shop.basket.coupon.beforePOST`, `dw.ocapi.shop.basket.coupon.afterPOST`, `dw.ocapi.shop.basket.coupon.modifyPOSTResponse` | 259 | | `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` | 260 | | `POST /baskets/{basket_id}/items` | `dw.ocapi.shop.basket.items.beforePOST`, `dw.ocapi.shop.basket.items.afterPOST`, `dw.ocapi.shop.basket.items.modifyPOSTResponse` | 261 | | `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` | 262 | | **Customer** | | 263 | | `POST /customers` | `dw.ocapi.shop.customer.beforePOST`, `dw.ocapi.shop.customer.afterPOST`, `dw.ocapi.shop.customer.modifyPOSTResponse` | 264 | | `GET /customers/{customer_id}` | `dw.ocapi.shop.customer.beforeGET`, `dw.ocapi.shop.customer.modifyGETResponse` | 265 | | `PATCH /customers/{customer_id}` | `dw.ocapi.shop.customer.beforePATCH`, `dw.ocapi.shop.customer.afterPATCH`, `dw.ocapi.shop.customer.modifyPATCHResponse` | 266 | | `POST /customers/{customer_id}/addresses` | `dw.ocapi.shop.customer.addresses.beforePOST`, `dw.ocapi.shop.customer.addresses.afterPOST`, `dw.ocapi.shop.customer.address.modifyPOSTResponse` | 267 | | `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` | 268 | | `DELETE /customers/{customer_id}/addresses/{address_name}` | `dw.ocapi.shop.customer.address.beforeDELETE`, `dw.ocapi.shop.customer.address.afterDELETE` | 269 | | **Order** | | 270 | | `POST /orders` | `dw.ocapi.shop.order.beforePOST`, `dw.ocapi.shop.order.afterPOST`, `dw.ocapi.shop.order.modifyPOSTResponse` | 271 | | `GET /orders/{order_no}` | `dw.ocapi.shop.order.beforeGET`, `dw.ocapi.shop.order.modifyGETResponse` | 272 | | `PATCH /orders/{order_no}` | `dw.ocapi.shop.order.beforePATCH`, `dw.ocapi.shop.order.afterPATCH`, `dw.ocapi.shop.order.modifyPATCHResponse` | 273 | | `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` | 274 | | **Product & Catalog** | | 275 | | `GET /products/{id}` | `dw.ocapi.shop.product.beforeGET`, `dw.ocapi.shop.product.modifyGETResponse` | 276 | | `GET /product_search` | `dw.ocapi.shop.product_search.beforeGET`, `dw.ocapi.shop.product_search.modifyGETResponse` | 277 | | `GET /categories/{id}` | `dw.ocapi.shop.category.beforeGET`, `dw.ocapi.shop.category.modifyGETResponse` | 278 | | `GET /content/{id}` | `dw.ocapi.shop.content.beforeGET`, `dw.ocapi.shop.content.modifyGETResponse` | 279 | 280 | ### Data API Hooks 281 | 282 | | API Endpoint (Method & Path) | Available Hook Extension Points | 283 | |------------------------------|----------------------------------| 284 | | **Custom Object** | | 285 | | `PUT /custom_objects/{object_type}/{key}` | `dw.ocapi.data.object.beforePut`, `dw.ocapi.data.object.afterPut` | 286 | | `PATCH /custom_objects/{object_type}/{key}` | `dw.ocapi.data.object.beforePatch`, `dw.ocapi.data.object.afterPatch` | 287 | | `DELETE /custom_objects/{object_type}/{key}` | `dw.ocapi.data.object.beforeDelete`, `dw.ocapi.data.object.afterDelete` | 288 | | **Customer** | | 289 | | `POST /customer_lists/{list_id}/customers` | `dw.ocapi.data.customer_list.customers.beforePOST`, `dw.ocapi.data.customer_list.customers.afterPOST` | 290 | | `PATCH /customer_lists/{list_id}/customers/{customer_no}` | `dw.ocapi.data.customer_list.customer.beforePATCH`, `dw.ocapi.data.customer_list.customer.afterPATCH` | 291 | | `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` | 292 | | **Content** | | 293 | | `PUT /libraries/{library_id}/content/{content_id}` | `dw.ocapi.data.content.content.beforeCreate`, `dw.ocapi.data.content.content.afterCreate` | 294 | | `PATCH /libraries/{library_id}/content/{content_id}` | `dw.ocapi.data.content.content.beforeUpdate`, `dw.ocapi.data.content.content.afterUpdate` | 295 | | `DELETE /libraries/{library_id}/content/{content_id}` | `dw.ocapi.data.content.content.beforeDelete`, `dw.ocapi.data.content.content.afterDelete` | 296 | | **User** | | 297 | | `PATCH /users/this/password` | `dw.ocapi.data.users.afterPATCH` | 298 | 299 | ## Troubleshooting Hook Registration 300 | 301 | **If OCAPI hooks are not executing after deployment:** 302 | 303 | 1. **Check Code Version**: If hooks don't execute after upload: 304 | - **Check Available Versions**: Use MCP `get_code_versions` tool to see all code versions on the instance 305 | - **Activate Different Version**: Use MCP `activate_code_version` tool to switch code versions 306 | - **Alternative Manual Method**: Switch code versions in Business Manager (Administration > Site Development > Code Deployment > Activate) 307 | 2. **Verify Hook Registration**: Check logs for hook registration confirmations after version activation 308 | 3. **Test Hook Execution**: Make OCAPI calls to endpoints that should trigger your hooks and verify they execute 309 | 4. **Verify API Settings**: Ensure OCAPI settings in Business Manager allow your endpoints and include proper hook configurations 310 | 311 | **Common Hook Issues:** 312 | - Hooks not triggering → Check code version activation and OCAPI settings 313 | - Hook scripts not found → Verify file paths match registration in hooks.json 314 | - Runtime errors in hooks → Check logs for specific error messages during hook execution 315 | - Data API hooks → Ensure proper authentication and permissions are configured 316 | ``` -------------------------------------------------------------------------------- /docs/dw_extensions.pinterest/PinterestProduct.md: -------------------------------------------------------------------------------- ```markdown 1 | ## Package: dw.extensions.pinterest 2 | 3 | # Class PinterestProduct 4 | 5 | ## Inheritance Hierarchy 6 | 7 | - Object 8 | - dw.extensions.pinterest.PinterestProduct 9 | 10 | ## Description 11 | 12 | Represents a row in the Pinterest catalog feed export. 13 | 14 | ## Constants 15 | 16 | ### AVAILABILITY_IN_STOCK 17 | 18 | **Type:** String = "in 19 | 20 | Indicates that the product is in stock. 21 | 22 | ### AVAILABILITY_OUT_OF_STOCK 23 | 24 | **Type:** String = "out 25 | 26 | Indicates that the product is not in stock. 27 | 28 | ### AVAILABILITY_PREORDER 29 | 30 | **Type:** String = "preorder" 31 | 32 | Indicates that the product is availabile in preorder. 33 | 34 | ### CONDITION_NEW 35 | 36 | **Type:** String = "new" 37 | 38 | Indicates that the product has never been used. 39 | 40 | ### CONDITION_REFURBISHED 41 | 42 | **Type:** String = "refurbished" 43 | 44 | Indicates that the product has been used but refurbished. 45 | 46 | ### CONDITION_USED 47 | 48 | **Type:** String = "used" 49 | 50 | Indicates that the product has been used. 51 | 52 | ## Properties 53 | 54 | ### availability 55 | 56 | **Type:** String 57 | 58 | The availability of the Pinterest product. Possible values are 59 | AVAILABILITY_IN_STOCK or 60 | AVAILABILITY_OUT_OF_STOCK. 61 | 62 | ### brand 63 | 64 | **Type:** String 65 | 66 | The Pinterest brand of the product. 67 | 68 | ### color 69 | 70 | **Type:** String 71 | 72 | The Pinterest color value label of the product. 73 | 74 | ### colorHex 75 | 76 | **Type:** String 77 | 78 | The Pinterest color hex value of the product. 79 | 80 | ### colorImage 81 | 82 | **Type:** URL 83 | 84 | The URL of the image to show in Pinterest for the product color (swatch). 85 | 86 | ### condition 87 | 88 | **Type:** String 89 | 90 | The condition of the Pinterest product. Possible values are 91 | CONDITION_NEW, 92 | CONDITION_REFURBISHED, or 93 | CONDITION_USED. 94 | 95 | ### description 96 | 97 | **Type:** String 98 | 99 | The Pinterest description of the product. 100 | 101 | ### googleProductCategory 102 | 103 | **Type:** String 104 | 105 | The category of this product in the Google category taxonomy. 106 | 107 | ### gtin 108 | 109 | **Type:** String 110 | 111 | The Pinterest GTIN of the product. 112 | 113 | ### ID 114 | 115 | **Type:** String (Read Only) 116 | 117 | The ID of the Pinterest product. This is the same as the ID of the Demandware product. 118 | 119 | ### imageLinks 120 | 121 | **Type:** List 122 | 123 | A list containing the URLs of the image to show in Pinterest for the product. 124 | 125 | ### itemGroupID 126 | 127 | **Type:** String 128 | 129 | The ID of the Pinterest item group for the product, that is, its master product. 130 | 131 | ### itemGroupLink 132 | 133 | **Type:** URL 134 | 135 | The URL of the Pinterest item group for the product, that is, the link to its master product in the 136 | Demandware storefront. 137 | 138 | ### link 139 | 140 | **Type:** URL 141 | 142 | The URL of the Demandware storefront link to the product. 143 | 144 | ### maxPrice 145 | 146 | **Type:** Money 147 | 148 | The maximum price to show in Pinterest for the product. 149 | 150 | ### minPrice 151 | 152 | **Type:** Money 153 | 154 | The minimum price to show in Pinterest for the product. 155 | 156 | ### price 157 | 158 | **Type:** Money 159 | 160 | The price to show in Pinterest for the product. 161 | 162 | ### productCategory 163 | 164 | **Type:** String 165 | 166 | The Pinterest category path of the product. 167 | 168 | ### returnPolicy 169 | 170 | **Type:** String 171 | 172 | The Pinterest return policy of the product. 173 | 174 | ### size 175 | 176 | **Type:** String 177 | 178 | The Pinterest size value label of the product. 179 | 180 | ### title 181 | 182 | **Type:** String 183 | 184 | The Pinterest title of the product. 185 | 186 | ## Constructor Summary 187 | 188 | ## Method Summary 189 | 190 | ### getAvailability 191 | 192 | **Signature:** `getAvailability() : String` 193 | 194 | Returns the availability of the Pinterest product. 195 | 196 | ### getBrand 197 | 198 | **Signature:** `getBrand() : String` 199 | 200 | Returns the Pinterest brand of the product. 201 | 202 | ### getColor 203 | 204 | **Signature:** `getColor() : String` 205 | 206 | Returns the Pinterest color value label of the product. 207 | 208 | ### getColorHex 209 | 210 | **Signature:** `getColorHex() : String` 211 | 212 | Returns the Pinterest color hex value of the product. 213 | 214 | ### getColorImage 215 | 216 | **Signature:** `getColorImage() : URL` 217 | 218 | Returns the URL of the image to show in Pinterest for the product color (swatch). 219 | 220 | ### getCondition 221 | 222 | **Signature:** `getCondition() : String` 223 | 224 | Returns the condition of the Pinterest product. 225 | 226 | ### getDescription 227 | 228 | **Signature:** `getDescription() : String` 229 | 230 | Returns the Pinterest description of the product. 231 | 232 | ### getGoogleProductCategory 233 | 234 | **Signature:** `getGoogleProductCategory() : String` 235 | 236 | Returns the category of this product in the Google category taxonomy. 237 | 238 | ### getGtin 239 | 240 | **Signature:** `getGtin() : String` 241 | 242 | Returns the Pinterest GTIN of the product. 243 | 244 | ### getID 245 | 246 | **Signature:** `getID() : String` 247 | 248 | Returns the ID of the Pinterest product. 249 | 250 | ### getImageLinks 251 | 252 | **Signature:** `getImageLinks() : List` 253 | 254 | Returns a list containing the URLs of the image to show in Pinterest for the product. 255 | 256 | ### getItemGroupID 257 | 258 | **Signature:** `getItemGroupID() : String` 259 | 260 | Returns the ID of the Pinterest item group for the product, that is, its master product. 261 | 262 | ### getItemGroupLink 263 | 264 | **Signature:** `getItemGroupLink() : URL` 265 | 266 | Returns the URL of the Pinterest item group for the product, that is, the link to its master product in the Demandware storefront. 267 | 268 | ### getLink 269 | 270 | **Signature:** `getLink() : URL` 271 | 272 | Returns the URL of the Demandware storefront link to the product. 273 | 274 | ### getMaxPrice 275 | 276 | **Signature:** `getMaxPrice() : Money` 277 | 278 | Returns the maximum price to show in Pinterest for the product. 279 | 280 | ### getMinPrice 281 | 282 | **Signature:** `getMinPrice() : Money` 283 | 284 | Returns the minimum price to show in Pinterest for the product. 285 | 286 | ### getPrice 287 | 288 | **Signature:** `getPrice() : Money` 289 | 290 | Returns the price to show in Pinterest for the product. 291 | 292 | ### getProductCategory 293 | 294 | **Signature:** `getProductCategory() : String` 295 | 296 | Returns the Pinterest category path of the product. 297 | 298 | ### getReturnPolicy 299 | 300 | **Signature:** `getReturnPolicy() : String` 301 | 302 | Returns the Pinterest return policy of the product. 303 | 304 | ### getSize 305 | 306 | **Signature:** `getSize() : String` 307 | 308 | Returns the Pinterest size value label of the product. 309 | 310 | ### getTitle 311 | 312 | **Signature:** `getTitle() : String` 313 | 314 | Returns the Pinterest title of the product. 315 | 316 | ### setAvailability 317 | 318 | **Signature:** `setAvailability(availability : String) : void` 319 | 320 | Sets the availability of the Pinterest product. 321 | 322 | ### setBrand 323 | 324 | **Signature:** `setBrand(brand : String) : void` 325 | 326 | Sets the Pinterest brand of the product. 327 | 328 | ### setColor 329 | 330 | **Signature:** `setColor(color : String) : void` 331 | 332 | Sets the Pinterest color value label of the product. 333 | 334 | ### setColorHex 335 | 336 | **Signature:** `setColorHex(colorHex : String) : void` 337 | 338 | Sets the Pinterest color hex value of the product. 339 | 340 | ### setColorImage 341 | 342 | **Signature:** `setColorImage(colorImage : URL) : void` 343 | 344 | Sets the URL of the image to show in Pinterest for the product color (swatch). 345 | 346 | ### setCondition 347 | 348 | **Signature:** `setCondition(condition : String) : void` 349 | 350 | Sets the condition of the Pinterest product. 351 | 352 | ### setDescription 353 | 354 | **Signature:** `setDescription(description : String) : void` 355 | 356 | Sets the Pinterest description of the product. 357 | 358 | ### setGoogleProductCategory 359 | 360 | **Signature:** `setGoogleProductCategory(googleProductCategory : String) : void` 361 | 362 | Sets the category of this product in the Google category taxonomy. 363 | 364 | ### setGtin 365 | 366 | **Signature:** `setGtin(gtin : String) : void` 367 | 368 | Sets the Pinterest GTIN of the product. 369 | 370 | ### setImageLinks 371 | 372 | **Signature:** `setImageLinks(imageLinks : List) : void` 373 | 374 | Sets the list of URLs of images to show in Pinterest for the product. 375 | 376 | ### setItemGroupID 377 | 378 | **Signature:** `setItemGroupID(itemGroupID : String) : void` 379 | 380 | Sets the ID of the Pinterest item group for the product, that is, its master product. 381 | 382 | ### setItemGroupLink 383 | 384 | **Signature:** `setItemGroupLink(itemGroupLink : URL) : void` 385 | 386 | Sets the URL of the Pinterest item group for the product, that is, the link to its master product in the Demandware storefront. 387 | 388 | ### setLink 389 | 390 | **Signature:** `setLink(link : URL) : void` 391 | 392 | Sets the URL of the Demandware storefront link to the product. 393 | 394 | ### setMaxPrice 395 | 396 | **Signature:** `setMaxPrice(maxPrice : Money) : void` 397 | 398 | Sets the maximum price to show in Pinterest for the product. 399 | 400 | ### setMinPrice 401 | 402 | **Signature:** `setMinPrice(minPrice : Money) : void` 403 | 404 | Sets the minimum price to show in Pinterest for the product. 405 | 406 | ### setPrice 407 | 408 | **Signature:** `setPrice(price : Money) : void` 409 | 410 | Sets the price to show in Pinterest for the product. 411 | 412 | ### setProductCategory 413 | 414 | **Signature:** `setProductCategory(productCategory : String) : void` 415 | 416 | Sets the Pinterest category path of the product. 417 | 418 | ### setReturnPolicy 419 | 420 | **Signature:** `setReturnPolicy(returnPolicy : String) : void` 421 | 422 | Sets the Pinterest return policy of the product. 423 | 424 | ### setSize 425 | 426 | **Signature:** `setSize(size : String) : void` 427 | 428 | Sets the Pinterest size value label of the product. 429 | 430 | ### setTitle 431 | 432 | **Signature:** `setTitle(title : String) : void` 433 | 434 | Sets the Pinterest title of the product. 435 | 436 | ## Method Detail 437 | 438 | ## Method Details 439 | 440 | ### getAvailability 441 | 442 | **Signature:** `getAvailability() : String` 443 | 444 | **Description:** Returns the availability of the Pinterest product. Possible values are AVAILABILITY_IN_STOCK or AVAILABILITY_OUT_OF_STOCK. 445 | 446 | --- 447 | 448 | ### getBrand 449 | 450 | **Signature:** `getBrand() : String` 451 | 452 | **Description:** Returns the Pinterest brand of the product. 453 | 454 | --- 455 | 456 | ### getColor 457 | 458 | **Signature:** `getColor() : String` 459 | 460 | **Description:** Returns the Pinterest color value label of the product. 461 | 462 | --- 463 | 464 | ### getColorHex 465 | 466 | **Signature:** `getColorHex() : String` 467 | 468 | **Description:** Returns the Pinterest color hex value of the product. 469 | 470 | --- 471 | 472 | ### getColorImage 473 | 474 | **Signature:** `getColorImage() : URL` 475 | 476 | **Description:** Returns the URL of the image to show in Pinterest for the product color (swatch). 477 | 478 | --- 479 | 480 | ### getCondition 481 | 482 | **Signature:** `getCondition() : String` 483 | 484 | **Description:** Returns the condition of the Pinterest product. Possible values are CONDITION_NEW, CONDITION_REFURBISHED, or CONDITION_USED. 485 | 486 | --- 487 | 488 | ### getDescription 489 | 490 | **Signature:** `getDescription() : String` 491 | 492 | **Description:** Returns the Pinterest description of the product. 493 | 494 | --- 495 | 496 | ### getGoogleProductCategory 497 | 498 | **Signature:** `getGoogleProductCategory() : String` 499 | 500 | **Description:** Returns the category of this product in the Google category taxonomy. 501 | 502 | --- 503 | 504 | ### getGtin 505 | 506 | **Signature:** `getGtin() : String` 507 | 508 | **Description:** Returns the Pinterest GTIN of the product. 509 | 510 | --- 511 | 512 | ### getID 513 | 514 | **Signature:** `getID() : String` 515 | 516 | **Description:** Returns the ID of the Pinterest product. This is the same as the ID of the Demandware product. 517 | 518 | **Returns:** 519 | 520 | product ID 521 | 522 | --- 523 | 524 | ### getImageLinks 525 | 526 | **Signature:** `getImageLinks() : List` 527 | 528 | **Description:** Returns a list containing the URLs of the image to show in Pinterest for the product. 529 | 530 | --- 531 | 532 | ### getItemGroupID 533 | 534 | **Signature:** `getItemGroupID() : String` 535 | 536 | **Description:** Returns the ID of the Pinterest item group for the product, that is, its master product. 537 | 538 | --- 539 | 540 | ### getItemGroupLink 541 | 542 | **Signature:** `getItemGroupLink() : URL` 543 | 544 | **Description:** Returns the URL of the Pinterest item group for the product, that is, the link to its master product in the Demandware storefront. 545 | 546 | --- 547 | 548 | ### getLink 549 | 550 | **Signature:** `getLink() : URL` 551 | 552 | **Description:** Returns the URL of the Demandware storefront link to the product. 553 | 554 | --- 555 | 556 | ### getMaxPrice 557 | 558 | **Signature:** `getMaxPrice() : Money` 559 | 560 | **Description:** Returns the maximum price to show in Pinterest for the product. 561 | 562 | --- 563 | 564 | ### getMinPrice 565 | 566 | **Signature:** `getMinPrice() : Money` 567 | 568 | **Description:** Returns the minimum price to show in Pinterest for the product. 569 | 570 | --- 571 | 572 | ### getPrice 573 | 574 | **Signature:** `getPrice() : Money` 575 | 576 | **Description:** Returns the price to show in Pinterest for the product. 577 | 578 | --- 579 | 580 | ### getProductCategory 581 | 582 | **Signature:** `getProductCategory() : String` 583 | 584 | **Description:** Returns the Pinterest category path of the product. 585 | 586 | --- 587 | 588 | ### getReturnPolicy 589 | 590 | **Signature:** `getReturnPolicy() : String` 591 | 592 | **Description:** Returns the Pinterest return policy of the product. 593 | 594 | --- 595 | 596 | ### getSize 597 | 598 | **Signature:** `getSize() : String` 599 | 600 | **Description:** Returns the Pinterest size value label of the product. 601 | 602 | --- 603 | 604 | ### getTitle 605 | 606 | **Signature:** `getTitle() : String` 607 | 608 | **Description:** Returns the Pinterest title of the product. 609 | 610 | --- 611 | 612 | ### setAvailability 613 | 614 | **Signature:** `setAvailability(availability : String) : void` 615 | 616 | **Description:** Sets the availability of the Pinterest product. Possible values are AVAILABILITY_IN_STOCK or AVAILABILITY_OUT_OF_STOCK. 617 | 618 | **Parameters:** 619 | 620 | - `availability`: the availability status to set for this product 621 | 622 | --- 623 | 624 | ### setBrand 625 | 626 | **Signature:** `setBrand(brand : String) : void` 627 | 628 | **Description:** Sets the Pinterest brand of the product. 629 | 630 | **Parameters:** 631 | 632 | - `brand`: Pinterest brand 633 | 634 | --- 635 | 636 | ### setColor 637 | 638 | **Signature:** `setColor(color : String) : void` 639 | 640 | **Description:** Sets the Pinterest color value label of the product. 641 | 642 | **Parameters:** 643 | 644 | - `color`: Pinterest color value label 645 | 646 | --- 647 | 648 | ### setColorHex 649 | 650 | **Signature:** `setColorHex(colorHex : String) : void` 651 | 652 | **Description:** Sets the Pinterest color hex value of the product. 653 | 654 | **Parameters:** 655 | 656 | - `colorHex`: Pinterest color hex value 657 | 658 | --- 659 | 660 | ### setColorImage 661 | 662 | **Signature:** `setColorImage(colorImage : URL) : void` 663 | 664 | **Description:** Sets the URL of the image to show in Pinterest for the product color (swatch). 665 | 666 | **Parameters:** 667 | 668 | - `colorImage`: link to Pinterest color image 669 | 670 | --- 671 | 672 | ### setCondition 673 | 674 | **Signature:** `setCondition(condition : String) : void` 675 | 676 | **Description:** Sets the condition of the Pinterest product. Possible values are CONDITION_NEW, CONDITION_REFURBISHED, or CONDITION_USED. 677 | 678 | **Parameters:** 679 | 680 | - `condition`: the condition status to set for this product 681 | 682 | --- 683 | 684 | ### setDescription 685 | 686 | **Signature:** `setDescription(description : String) : void` 687 | 688 | **Description:** Sets the Pinterest description of the product. 689 | 690 | **Parameters:** 691 | 692 | - `description`: Pinterest description 693 | 694 | --- 695 | 696 | ### setGoogleProductCategory 697 | 698 | **Signature:** `setGoogleProductCategory(googleProductCategory : String) : void` 699 | 700 | **Description:** Sets the category of this product in the Google category taxonomy. 701 | 702 | **Parameters:** 703 | 704 | - `googleProductCategory`: Google product category 705 | 706 | --- 707 | 708 | ### setGtin 709 | 710 | **Signature:** `setGtin(gtin : String) : void` 711 | 712 | **Description:** Sets the Pinterest GTIN of the product. 713 | 714 | **Parameters:** 715 | 716 | - `gtin`: Pinterest GTIN 717 | 718 | --- 719 | 720 | ### setImageLinks 721 | 722 | **Signature:** `setImageLinks(imageLinks : List) : void` 723 | 724 | **Description:** Sets the list of URLs of images to show in Pinterest for the product. 725 | 726 | **Parameters:** 727 | 728 | - `imageLinks`: links to the product images 729 | 730 | --- 731 | 732 | ### setItemGroupID 733 | 734 | **Signature:** `setItemGroupID(itemGroupID : String) : void` 735 | 736 | **Description:** Sets the ID of the Pinterest item group for the product, that is, its master product. 737 | 738 | **Parameters:** 739 | 740 | - `itemGroupID`: ID of Pinterest item group 741 | 742 | --- 743 | 744 | ### setItemGroupLink 745 | 746 | **Signature:** `setItemGroupLink(itemGroupLink : URL) : void` 747 | 748 | **Description:** Sets the URL of the Pinterest item group for the product, that is, the link to its master product in the Demandware storefront. 749 | 750 | **Parameters:** 751 | 752 | - `itemGroupLink`: link to the Pinterest item group 753 | 754 | --- 755 | 756 | ### setLink 757 | 758 | **Signature:** `setLink(link : URL) : void` 759 | 760 | **Description:** Sets the URL of the Demandware storefront link to the product. 761 | 762 | **Parameters:** 763 | 764 | - `link`: Demandware storefront link to the product 765 | 766 | --- 767 | 768 | ### setMaxPrice 769 | 770 | **Signature:** `setMaxPrice(maxPrice : Money) : void` 771 | 772 | **Description:** Sets the maximum price to show in Pinterest for the product. 773 | 774 | **Parameters:** 775 | 776 | - `maxPrice`: Pinterest maximum price 777 | 778 | --- 779 | 780 | ### setMinPrice 781 | 782 | **Signature:** `setMinPrice(minPrice : Money) : void` 783 | 784 | **Description:** Sets the minimum price to show in Pinterest for the product. 785 | 786 | **Parameters:** 787 | 788 | - `minPrice`: Pinterest minimum price 789 | 790 | --- 791 | 792 | ### setPrice 793 | 794 | **Signature:** `setPrice(price : Money) : void` 795 | 796 | **Description:** Sets the price to show in Pinterest for the product. 797 | 798 | **Parameters:** 799 | 800 | - `price`: Pinterest price 801 | 802 | --- 803 | 804 | ### setProductCategory 805 | 806 | **Signature:** `setProductCategory(productCategory : String) : void` 807 | 808 | **Description:** Sets the Pinterest category path of the product. 809 | 810 | **Parameters:** 811 | 812 | - `productCategory`: Pinterest category path 813 | 814 | --- 815 | 816 | ### setReturnPolicy 817 | 818 | **Signature:** `setReturnPolicy(returnPolicy : String) : void` 819 | 820 | **Description:** Sets the Pinterest return policy of the product. 821 | 822 | **Parameters:** 823 | 824 | - `returnPolicy`: Pinterest return policy 825 | 826 | --- 827 | 828 | ### setSize 829 | 830 | **Signature:** `setSize(size : String) : void` 831 | 832 | **Description:** Sets the Pinterest size value label of the product. 833 | 834 | **Parameters:** 835 | 836 | - `size`: Pinterest size value label 837 | 838 | --- 839 | 840 | ### setTitle 841 | 842 | **Signature:** `setTitle(title : String) : void` 843 | 844 | **Description:** Sets the Pinterest title of the product. 845 | 846 | **Parameters:** 847 | 848 | - `title`: Pinterest title 849 | 850 | --- ```