This is page 19 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 -------------------------------------------------------------------------------- /docs/dw_content/Folder.md: -------------------------------------------------------------------------------- ```markdown 1 | ## Package: dw.content 2 | 3 | # Class Folder 4 | 5 | ## Inheritance Hierarchy 6 | 7 | - Object 8 | - dw.object.PersistentObject 9 | - dw.object.ExtensibleObject 10 | - dw.content.Folder 11 | 12 | ## Description 13 | 14 | Class representing a folder for organizing content assets in Commerce Cloud Digital. 15 | 16 | ## Properties 17 | 18 | ### content 19 | 20 | **Type:** Collection (Read Only) 21 | 22 | The content objects for this folder, sorted by position. 23 | 24 | ### description 25 | 26 | **Type:** String (Read Only) 27 | 28 | The description for the folder as known in the current 29 | locale or null if it cannot be found. 30 | 31 | ### displayName 32 | 33 | **Type:** String (Read Only) 34 | 35 | The display name for the folder as known in the current 36 | locale or null if it cannot be found. 37 | 38 | ### ID 39 | 40 | **Type:** String (Read Only) 41 | 42 | The ID of the folder. The ID can be used to uniquely 43 | identify a folder within any given library. This folder ID provides 44 | an alternative lookup mechanism for folders frequently used in 45 | the storefront. 46 | 47 | ### online 48 | 49 | **Type:** boolean (Read Only) 50 | 51 | Indicates if the folder is set online or 52 | offline. Initially, all folders are set online. 53 | 54 | ### onlineContent 55 | 56 | **Type:** Collection (Read Only) 57 | 58 | The online content objects for this folder, sorted by position. 59 | 60 | ### onlineSubFolders 61 | 62 | **Type:** Collection (Read Only) 63 | 64 | The online subfolders of this folder, sorted by position. 65 | 66 | ### pageDescription 67 | 68 | **Type:** String (Read Only) 69 | 70 | The page description for this folder using the value in 71 | the current locale, or returns null if no value was found. 72 | 73 | ### pageKeywords 74 | 75 | **Type:** String (Read Only) 76 | 77 | The page keywords for this folder using the value in 78 | the current locale, or returns null if no value was found. 79 | 80 | ### pageTitle 81 | 82 | **Type:** String (Read Only) 83 | 84 | The page title for this folder using the value in 85 | the current locale, or returns null if no value was found. 86 | 87 | ### pageURL 88 | 89 | **Type:** String (Read Only) 90 | 91 | The page URL for this folder using the value in 92 | the current locale, or returns null if no value was found. 93 | 94 | ### parent 95 | 96 | **Type:** Folder (Read Only) 97 | 98 | The parent folder of this folder. 99 | 100 | ### root 101 | 102 | **Type:** boolean (Read Only) 103 | 104 | Indicates if this is the root folder. 105 | 106 | ### siteMapChangeFrequency 107 | 108 | **Type:** String (Read Only) 109 | 110 | The folder's sitemap change frequency. 111 | 112 | ### siteMapIncluded 113 | 114 | **Type:** Number (Read Only) 115 | 116 | The folder's sitemap inclusion. 117 | 118 | ### siteMapPriority 119 | 120 | **Type:** Number (Read Only) 121 | 122 | The folder's sitemap priority. 123 | 124 | ### subFolders 125 | 126 | **Type:** Collection (Read Only) 127 | 128 | The subfolders of this folder, sorted by position. 129 | 130 | ### template 131 | 132 | **Type:** String (Read Only) 133 | 134 | The name of the template used to render the folder 135 | in the store front. 136 | 137 | ## Constructor Summary 138 | 139 | ## Method Summary 140 | 141 | ### getContent 142 | 143 | **Signature:** `getContent() : Collection` 144 | 145 | Returns the content objects for this folder, sorted by position. 146 | 147 | ### getDescription 148 | 149 | **Signature:** `getDescription() : String` 150 | 151 | Returns the description for the folder as known in the current locale or null if it cannot be found. 152 | 153 | ### getDisplayName 154 | 155 | **Signature:** `getDisplayName() : String` 156 | 157 | Returns the display name for the folder as known in the current locale or null if it cannot be found. 158 | 159 | ### getID 160 | 161 | **Signature:** `getID() : String` 162 | 163 | Returns the ID of the folder. 164 | 165 | ### getOnlineContent 166 | 167 | **Signature:** `getOnlineContent() : Collection` 168 | 169 | Returns the online content objects for this folder, sorted by position. 170 | 171 | ### getOnlineSubFolders 172 | 173 | **Signature:** `getOnlineSubFolders() : Collection` 174 | 175 | Returns the online subfolders of this folder, sorted by position. 176 | 177 | ### getPageDescription 178 | 179 | **Signature:** `getPageDescription() : String` 180 | 181 | Returns the page description for this folder using the value in the current locale, or returns null if no value was found. 182 | 183 | ### getPageKeywords 184 | 185 | **Signature:** `getPageKeywords() : String` 186 | 187 | Returns the page keywords for this folder using the value in the current locale, or returns null if no value was found. 188 | 189 | ### getPageTitle 190 | 191 | **Signature:** `getPageTitle() : String` 192 | 193 | Returns the page title for this folder using the value in the current locale, or returns null if no value was found. 194 | 195 | ### getPageURL 196 | 197 | **Signature:** `getPageURL() : String` 198 | 199 | Returns the page URL for this folder using the value in the current locale, or returns null if no value was found. 200 | 201 | ### getParent 202 | 203 | **Signature:** `getParent() : Folder` 204 | 205 | Returns the parent folder of this folder. 206 | 207 | ### getSiteMapChangeFrequency 208 | 209 | **Signature:** `getSiteMapChangeFrequency() : String` 210 | 211 | Returns the folder's sitemap change frequency. 212 | 213 | ### getSiteMapIncluded 214 | 215 | **Signature:** `getSiteMapIncluded() : Number` 216 | 217 | Returns the folder's sitemap inclusion. 218 | 219 | ### getSiteMapPriority 220 | 221 | **Signature:** `getSiteMapPriority() : Number` 222 | 223 | Returns the folder's sitemap priority. 224 | 225 | ### getSubFolders 226 | 227 | **Signature:** `getSubFolders() : Collection` 228 | 229 | Returns the subfolders of this folder, sorted by position. 230 | 231 | ### getTemplate 232 | 233 | **Signature:** `getTemplate() : String` 234 | 235 | Returns the name of the template used to render the folder in the store front. 236 | 237 | ### isOnline 238 | 239 | **Signature:** `isOnline() : boolean` 240 | 241 | Indicates if the folder is set online or offline. 242 | 243 | ### isRoot 244 | 245 | **Signature:** `isRoot() : boolean` 246 | 247 | Indicates if this is the root folder. 248 | 249 | ## Method Detail 250 | 251 | ## Method Details 252 | 253 | ### getContent 254 | 255 | **Signature:** `getContent() : Collection` 256 | 257 | **Description:** Returns the content objects for this folder, sorted by position. 258 | 259 | **Returns:** 260 | 261 | the content objects for this folder, sorted by position. 262 | 263 | --- 264 | 265 | ### getDescription 266 | 267 | **Signature:** `getDescription() : String` 268 | 269 | **Description:** Returns the description for the folder as known in the current locale or null if it cannot be found. 270 | 271 | **Returns:** 272 | 273 | the description for the folder as known in the current locale or null if it cannot be found. 274 | 275 | --- 276 | 277 | ### getDisplayName 278 | 279 | **Signature:** `getDisplayName() : String` 280 | 281 | **Description:** Returns the display name for the folder as known in the current locale or null if it cannot be found. 282 | 283 | **Returns:** 284 | 285 | the display name for the folder as known in the current locale or null if it cannot be found. 286 | 287 | --- 288 | 289 | ### getID 290 | 291 | **Signature:** `getID() : String` 292 | 293 | **Description:** Returns the ID of the folder. The ID can be used to uniquely identify a folder within any given library. This folder ID provides an alternative lookup mechanism for folders frequently used in the storefront. 294 | 295 | **Returns:** 296 | 297 | the ID of the folder. 298 | 299 | --- 300 | 301 | ### getOnlineContent 302 | 303 | **Signature:** `getOnlineContent() : Collection` 304 | 305 | **Description:** Returns the online content objects for this folder, sorted by position. 306 | 307 | **Returns:** 308 | 309 | the online content objects for this folder, sorted by position. 310 | 311 | --- 312 | 313 | ### getOnlineSubFolders 314 | 315 | **Signature:** `getOnlineSubFolders() : Collection` 316 | 317 | **Description:** Returns the online subfolders of this folder, sorted by position. 318 | 319 | **Returns:** 320 | 321 | the online subfolders of this folder, sorted by position. 322 | 323 | --- 324 | 325 | ### getPageDescription 326 | 327 | **Signature:** `getPageDescription() : String` 328 | 329 | **Description:** Returns the page description for this folder using the value in the current locale, or returns null if no value was found. 330 | 331 | **Returns:** 332 | 333 | the page description for this folder using the value in the current locale, or returns null if no value was found. 334 | 335 | --- 336 | 337 | ### getPageKeywords 338 | 339 | **Signature:** `getPageKeywords() : String` 340 | 341 | **Description:** Returns the page keywords for this folder using the value in the current locale, or returns null if no value was found. 342 | 343 | **Returns:** 344 | 345 | the page keywords for this folder using the value in the current locale, or returns null if no value was found. 346 | 347 | --- 348 | 349 | ### getPageTitle 350 | 351 | **Signature:** `getPageTitle() : String` 352 | 353 | **Description:** Returns the page title for this folder using the value in the current locale, or returns null if no value was found. 354 | 355 | **Returns:** 356 | 357 | the page title for this folder using the value in the current locale, or returns null if no value was found. 358 | 359 | --- 360 | 361 | ### getPageURL 362 | 363 | **Signature:** `getPageURL() : String` 364 | 365 | **Description:** Returns the page URL for this folder using the value in the current locale, or returns null if no value was found. 366 | 367 | **Returns:** 368 | 369 | the page URL for this folder using the value in the current locale, or returns null if no value was found. 370 | 371 | --- 372 | 373 | ### getParent 374 | 375 | **Signature:** `getParent() : Folder` 376 | 377 | **Description:** Returns the parent folder of this folder. 378 | 379 | **Returns:** 380 | 381 | the parent folder of this folder. 382 | 383 | --- 384 | 385 | ### getSiteMapChangeFrequency 386 | 387 | **Signature:** `getSiteMapChangeFrequency() : String` 388 | 389 | **Description:** Returns the folder's sitemap change frequency. 390 | 391 | **Returns:** 392 | 393 | the value of the attribute 'siteMapChangeFrequency'. 394 | 395 | --- 396 | 397 | ### getSiteMapIncluded 398 | 399 | **Signature:** `getSiteMapIncluded() : Number` 400 | 401 | **Description:** Returns the folder's sitemap inclusion. 402 | 403 | **Returns:** 404 | 405 | the value of the attribute 'siteMapIncluded'. 406 | 407 | --- 408 | 409 | ### getSiteMapPriority 410 | 411 | **Signature:** `getSiteMapPriority() : Number` 412 | 413 | **Description:** Returns the folder's sitemap priority. 414 | 415 | **Returns:** 416 | 417 | the value of the attribute 'siteMapPriority'. 418 | 419 | --- 420 | 421 | ### getSubFolders 422 | 423 | **Signature:** `getSubFolders() : Collection` 424 | 425 | **Description:** Returns the subfolders of this folder, sorted by position. 426 | 427 | **Returns:** 428 | 429 | the subfolders of this folder, sorted by position. 430 | 431 | --- 432 | 433 | ### getTemplate 434 | 435 | **Signature:** `getTemplate() : String` 436 | 437 | **Description:** Returns the name of the template used to render the folder in the store front. 438 | 439 | **Returns:** 440 | 441 | the name of the template used to render the folder. 442 | 443 | --- 444 | 445 | ### isOnline 446 | 447 | **Signature:** `isOnline() : boolean` 448 | 449 | **Description:** Indicates if the folder is set online or offline. Initially, all folders are set online. 450 | 451 | **Returns:** 452 | 453 | true if the folder is online, false otherwise. 454 | 455 | --- 456 | 457 | ### isRoot 458 | 459 | **Signature:** `isRoot() : boolean` 460 | 461 | **Description:** Indicates if this is the root folder. 462 | 463 | **Returns:** 464 | 465 | true if this is the root folder, false otherwise. 466 | 467 | --- ``` -------------------------------------------------------------------------------- /src/clients/docs/class-content-parser.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Class Content Parser 3 | * 4 | * Responsible for parsing markdown documentation content and extracting 5 | * structured class information including constants, properties, methods, and inheritance. 6 | * 7 | * Single Responsibility: Converting markdown content to structured data 8 | */ 9 | 10 | import { Logger } from '../../utils/logger.js'; 11 | 12 | export interface SFCCMethod { 13 | name: string; 14 | signature: string; 15 | description: string; 16 | parameters?: string[]; 17 | returnType?: string; 18 | deprecated?: boolean; 19 | deprecationMessage?: string; 20 | } 21 | 22 | export interface SFCCProperty { 23 | name: string; 24 | type: string; 25 | description: string; 26 | modifiers?: string[]; 27 | deprecated?: boolean; 28 | deprecationMessage?: string; 29 | } 30 | 31 | export interface SFCCConstant { 32 | name: string; 33 | type: string; 34 | value?: string; 35 | description: string; 36 | deprecated?: boolean; 37 | deprecationMessage?: string; 38 | } 39 | 40 | export interface SFCCClassDetails { 41 | className: string; 42 | packageName: string; 43 | description: string; 44 | constants: SFCCConstant[]; 45 | properties: SFCCProperty[]; 46 | methods: SFCCMethod[]; 47 | inheritance?: string[]; 48 | constructorInfo?: string; 49 | } 50 | 51 | export class ClassContentParser { 52 | private logger: Logger; 53 | 54 | constructor() { 55 | this.logger = Logger.getChildLogger('ClassContentParser'); 56 | } 57 | 58 | /** 59 | * Parse markdown content and extract structured class information 60 | */ 61 | parseClassContent(content: string): SFCCClassDetails { 62 | const lines = content.split('\n'); 63 | 64 | let currentSection = ''; 65 | let className = ''; 66 | let packageName = ''; 67 | let description = ''; 68 | const constants: SFCCConstant[] = []; 69 | const properties: SFCCProperty[] = []; 70 | const methods: SFCCMethod[] = []; 71 | const inheritance: string[] = []; 72 | let constructorInfo = ''; 73 | 74 | for (let i = 0; i < lines.length; i++) { 75 | const line = lines[i].trim(); 76 | 77 | // Extract package name 78 | if (line.startsWith('## Package:')) { 79 | packageName = line.replace('## Package:', '').trim(); 80 | } 81 | 82 | // Extract class name 83 | if (line.startsWith('# ') && !line.startsWith('## ')) { 84 | className = line.replace('# ', '').replace('Class ', '').trim(); 85 | } 86 | 87 | // Track current section 88 | if (line.startsWith('## ')) { 89 | currentSection = line.replace('## ', '').trim(); 90 | } 91 | 92 | // Extract description 93 | if (currentSection === 'Description' && line && !line.startsWith('#')) { 94 | description += `${line} `; 95 | } 96 | 97 | // Extract inheritance hierarchy 98 | if (currentSection === 'Inheritance Hierarchy' && line.includes('-')) { 99 | const hierarchyItem = line.replace(/^[\s-]*/, '').trim(); 100 | if (hierarchyItem) { 101 | inheritance.push(hierarchyItem); 102 | } 103 | } 104 | 105 | // Extract constants 106 | if (currentSection === 'Constants' && line.startsWith('### ')) { 107 | const constant = this.parseConstant(line, lines, i); 108 | if (constant) { 109 | constants.push(constant); 110 | } 111 | } 112 | 113 | // Extract properties 114 | if (currentSection === 'Properties' && line.startsWith('### ')) { 115 | const property = this.parseProperty(line, lines, i); 116 | if (property) { 117 | properties.push(property); 118 | } 119 | } 120 | 121 | // Extract methods 122 | if ((currentSection === 'Method Summary' || currentSection === 'Method Details') && line.startsWith('### ')) { 123 | const method = this.parseMethod(line, lines, i); 124 | if (method) { 125 | methods.push(method); 126 | } 127 | } 128 | 129 | // Extract constructor info 130 | if (currentSection === 'Constructor Summary' && line && !line.startsWith('#')) { 131 | constructorInfo += `${line} `; 132 | } 133 | } 134 | 135 | return { 136 | className: className.trim(), 137 | packageName: packageName.trim(), 138 | description: description.trim(), 139 | constants, 140 | properties, 141 | methods, 142 | inheritance: inheritance.length > 0 ? inheritance : undefined, 143 | constructorInfo: constructorInfo.trim() || undefined, 144 | }; 145 | } 146 | 147 | /** 148 | * Parse a constant definition from markdown content 149 | */ 150 | private parseConstant(headerLine: string, lines: string[], startIndex: number): SFCCConstant | null { 151 | const constName = headerLine.replace('### ', '').trim(); 152 | let constType = ''; 153 | let constValue = ''; 154 | let constDesc = ''; 155 | let deprecated = false; 156 | let deprecationMessage = ''; 157 | 158 | // Look for type, value and description in following lines 159 | for (let j = startIndex + 1; j < lines.length && !lines[j].startsWith('#'); j++) { 160 | const nextLine = lines[j].trim(); 161 | 162 | if (nextLine.startsWith('**Type:**')) { 163 | const typeMatch = nextLine.match(/\*\*Type:\*\*\s*(.+)/); 164 | if (typeMatch) { 165 | const typeInfo = typeMatch[1]; 166 | // Extract type and value if present (e.g., "String = 'COMPLETED'" or "Number = 8") 167 | const valueMatch = typeInfo.match(/^(\w+)\s*=\s*(.+)$/); 168 | if (valueMatch) { 169 | constType = valueMatch[1]; 170 | constValue = valueMatch[2]; 171 | } else { 172 | constType = typeInfo.trim(); 173 | } 174 | } 175 | } else if (nextLine.startsWith('**Deprecated:**')) { 176 | const deprecationInfo = this.parseDeprecationInfo(nextLine, lines, j); 177 | deprecated = true; 178 | deprecationMessage = deprecationInfo.message; 179 | } else if (nextLine && !nextLine.startsWith('**') && !nextLine.startsWith('#')) { 180 | constDesc += `${nextLine} `; 181 | } 182 | } 183 | 184 | return { 185 | name: constName, 186 | type: constType, 187 | value: constValue || undefined, 188 | description: constDesc.trim(), 189 | deprecated: deprecated || undefined, 190 | deprecationMessage: deprecationMessage || undefined, 191 | }; 192 | } 193 | 194 | /** 195 | * Parse a property definition from markdown content 196 | */ 197 | private parseProperty(headerLine: string, lines: string[], startIndex: number): SFCCProperty | null { 198 | const propName = headerLine.replace('### ', '').trim(); 199 | let propType = ''; 200 | let propDesc = ''; 201 | const modifiers: string[] = []; 202 | let deprecated = false; 203 | let deprecationMessage = ''; 204 | 205 | // Look for type and description in following lines 206 | for (let j = startIndex + 1; j < lines.length && !lines[j].startsWith('#'); j++) { 207 | const nextLine = lines[j].trim(); 208 | 209 | if (nextLine.startsWith('**Type:**')) { 210 | const typeMatch = nextLine.match(/\*\*Type:\*\*\s*(.+)/); 211 | if (typeMatch) { 212 | const typeInfo = typeMatch[1]; 213 | propType = typeInfo.split(' ')[0]; 214 | if (typeInfo.includes('(Read Only)')) { 215 | modifiers.push('Read Only'); 216 | } 217 | if (typeInfo.includes('(Static)')) { 218 | modifiers.push('Static'); 219 | } 220 | } 221 | } else if (nextLine.startsWith('**Deprecated:**')) { 222 | const deprecationInfo = this.parseDeprecationInfo(nextLine, lines, j); 223 | deprecated = true; 224 | deprecationMessage = deprecationInfo.message; 225 | } else if (nextLine && !nextLine.startsWith('**') && !nextLine.startsWith('#')) { 226 | propDesc += `${nextLine} `; 227 | } 228 | } 229 | 230 | return { 231 | name: propName, 232 | type: propType, 233 | description: propDesc.trim(), 234 | modifiers: modifiers.length > 0 ? modifiers : undefined, 235 | deprecated: deprecated || undefined, 236 | deprecationMessage: deprecationMessage || undefined, 237 | }; 238 | } 239 | 240 | /** 241 | * Parse a method definition from markdown content 242 | */ 243 | private parseMethod(headerLine: string, lines: string[], startIndex: number): SFCCMethod | null { 244 | const methodName = headerLine.replace('### ', '').trim(); 245 | let signature = ''; 246 | let methodDesc = ''; 247 | let deprecated = false; 248 | let deprecationMessage = ''; 249 | 250 | // Look for signature and description in following lines 251 | for (let j = startIndex + 1; j < lines.length && !lines[j].startsWith('#'); j++) { 252 | const nextLine = lines[j].trim(); 253 | 254 | if (nextLine.startsWith('**Signature:**')) { 255 | const sigMatch = nextLine.match(/\*\*Signature:\*\*\s*`(.+)`/); 256 | if (sigMatch) { 257 | signature = sigMatch[1]; 258 | } 259 | } else if (nextLine.startsWith('**Description:**')) { 260 | methodDesc = nextLine.replace('**Description:**', '').trim(); 261 | } else if (nextLine.startsWith('**Deprecated:**')) { 262 | const deprecationInfo = this.parseDeprecationInfo(nextLine, lines, j); 263 | deprecated = true; 264 | deprecationMessage = deprecationInfo.message; 265 | } else if (nextLine && !nextLine.startsWith('**') && !nextLine.startsWith('#') && !nextLine.startsWith('---')) { 266 | if (!methodDesc && !nextLine.includes('Signature:')) { 267 | methodDesc += `${nextLine} `; 268 | } 269 | } 270 | } 271 | 272 | return { 273 | name: methodName, 274 | signature: signature || methodName, 275 | description: methodDesc.trim(), 276 | deprecated: deprecated || undefined, 277 | deprecationMessage: deprecationMessage || undefined, 278 | }; 279 | } 280 | 281 | /** 282 | * Parse deprecation information from markdown content 283 | */ 284 | private parseDeprecationInfo(deprecationLine: string, lines: string[], startIndex: number): { message: string } { 285 | let deprecationMessage = ''; 286 | 287 | // Check if there's a message on the same line 288 | const sameLineMessage = deprecationLine.replace('**Deprecated:**', '').trim(); 289 | if (sameLineMessage) { 290 | deprecationMessage = sameLineMessage; 291 | } else { 292 | // Look for the deprecation message on subsequent lines until next ** marker 293 | const depLines: string[] = []; 294 | for (let k = startIndex + 1; k < lines.length && !lines[k].startsWith('#'); k++) { 295 | const depLine = lines[k].trim(); 296 | if (depLine.startsWith('**') && !depLine.startsWith('**Deprecated:**')) { 297 | break; // Stop at next ** marker 298 | } 299 | if (depLine && !depLine.startsWith('---')) { 300 | depLines.push(depLine); 301 | } 302 | } 303 | deprecationMessage = depLines.join(' ').trim(); 304 | } 305 | 306 | return { message: deprecationMessage }; 307 | } 308 | } 309 | ``` -------------------------------------------------------------------------------- /tests/servers/sfcc-mock-server/server.js: -------------------------------------------------------------------------------- ```javascript 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * SFCC Mock Server 5 | * 6 | * Unified mock server combining WebDAV and OCAPI functionality for SFCC development testing. 7 | * Provides a single endpoint for both log file access and OCAPI simulation. 8 | * 9 | * Usage: 10 | * node server.js [options] 11 | * 12 | * Options: 13 | * --port <number> Server port (default: 3000) 14 | * --host <string> Server host (default: localhost) 15 | * --dev Enable development mode with verbose logging 16 | * --no-webdav Disable WebDAV functionality 17 | * --no-ocapi Disable OCAPI functionality 18 | * --no-cors Disable CORS headers 19 | * --mock-data <path> Custom path to mock data directory 20 | * --help Show this help message 21 | */ 22 | 23 | const ServerConfig = require('./src/config/server-config'); 24 | const SFCCMockApp = require('./src/app'); 25 | 26 | class SFCCMockServer { 27 | constructor(options = {}) { 28 | this.config = new ServerConfig(options); 29 | this.app = new SFCCMockApp(this.config); 30 | this.server = null; 31 | } 32 | 33 | async start() { 34 | try { 35 | // Validate configuration 36 | const configErrors = this.config.validate(); 37 | if (configErrors.length > 0) { 38 | throw new Error(`Configuration errors: ${configErrors.join(', ')}`); 39 | } 40 | 41 | const expressApp = this.app.getExpressApp(); 42 | 43 | return new Promise((resolve, reject) => { 44 | this.server = expressApp.listen(this.config.port, this.config.host, (err) => { 45 | if (err) { 46 | reject(err); 47 | return; 48 | } 49 | 50 | this.printStartupMessage(); 51 | resolve(this.server); 52 | }); 53 | 54 | this.server.on('error', (err) => { 55 | if (err.code === 'EADDRINUSE') { 56 | console.error(`❌ Port ${this.config.port} is already in use`); 57 | console.error(' Try using a different port with --port <number>'); 58 | } else { 59 | console.error('❌ Server error:', err.message); 60 | } 61 | reject(err); 62 | }); 63 | }); 64 | } catch (error) { 65 | console.error('❌ Failed to start server:', error.message); 66 | throw error; 67 | } 68 | } 69 | 70 | async stop() { 71 | if (this.server) { 72 | return new Promise((resolve) => { 73 | this.server.close(() => { 74 | console.log('🛑 SFCC Mock Server stopped'); 75 | resolve(); 76 | }); 77 | }); 78 | } 79 | } 80 | 81 | printStartupMessage() { 82 | const summary = this.config.getSummary(); 83 | 84 | console.log('🚀 SFCC Mock Server started successfully!'); 85 | console.log(''); 86 | console.log(`📊 Server Info:`); 87 | console.log(` Host: ${summary.server}`); 88 | console.log(` Mode: ${summary.mode}`); 89 | console.log(` Features: ${summary.features.join(', ')}`); 90 | console.log(''); 91 | console.log('📋 Available Endpoints:'); 92 | 93 | Object.entries(summary.endpoints).forEach(([name, url]) => { 94 | if (typeof url === 'string') { 95 | console.log(` ${name}: ${url}`); 96 | } else if (typeof url === 'object') { 97 | console.log(` ${name}:`); 98 | Object.entries(url).forEach(([subName, subUrl]) => { 99 | console.log(` ${subName}: ${subUrl}`); 100 | }); 101 | } 102 | }); 103 | 104 | if (this.config.features.webdav) { 105 | console.log(''); 106 | console.log('📁 WebDAV Endpoints:'); 107 | console.log(` Logs (SFCC path): ${this.config.getWebdavLogsUrl()}`); 108 | console.log(` Logs (direct): ${this.config.getServerUrl()}/Logs/`); 109 | console.log(` Job Logs: ${this.config.getServerUrl()}/Logs/jobs/`); 110 | } 111 | 112 | if (this.config.features.ocapi) { 113 | console.log(''); 114 | console.log('🔐 OCAPI Endpoints:'); 115 | console.log(` OAuth: ${this.config.getServerUrl()}/dw/oauth2/access_token`); 116 | console.log(` System Objects: ${this.config.getOcapiBaseUrl()}/system_object_definitions`); 117 | console.log(` Code Versions: ${this.config.getOcapiBaseUrl()}/code_versions`); 118 | console.log(''); 119 | console.log('🔑 Test Credentials:'); 120 | console.log(` Client ID: ${this.config.validCredentials.clientId}`); 121 | console.log(` Client Secret: ${this.config.validCredentials.clientSecret}`); 122 | } 123 | 124 | if (this.config.isDevMode) { 125 | console.log(''); 126 | console.log('🔧 Development mode enabled - verbose logging active'); 127 | } 128 | 129 | console.log(''); 130 | console.log('✅ Server is ready for connections'); 131 | } 132 | 133 | getConfig() { 134 | return this.config; 135 | } 136 | 137 | getApp() { 138 | return this.app; 139 | } 140 | } 141 | 142 | /** 143 | * Parse command line arguments 144 | */ 145 | function parseArgs() { 146 | const args = process.argv.slice(2); 147 | const options = {}; 148 | 149 | for (let i = 0; i < args.length; i++) { 150 | const arg = args[i]; 151 | 152 | switch (arg) { 153 | case '--help': 154 | printHelp(); 155 | process.exit(0); 156 | break; 157 | case '--dev': 158 | options.dev = true; 159 | break; 160 | case '--no-webdav': 161 | options.enableWebdav = false; 162 | break; 163 | case '--no-ocapi': 164 | options.enableOcapi = false; 165 | break; 166 | case '--no-cors': 167 | options.enableCors = false; 168 | break; 169 | case '--enable-random-errors': 170 | options.enableRandomErrors = true; 171 | break; 172 | case '--port': 173 | if (i + 1 < args.length) { 174 | options.port = parseInt(args[i + 1]); 175 | i++; 176 | } else { 177 | console.error('❌ --port requires a value'); 178 | process.exit(1); 179 | } 180 | break; 181 | case '--host': 182 | if (i + 1 < args.length) { 183 | options.host = args[i + 1]; 184 | i++; 185 | } else { 186 | console.error('❌ --host requires a value'); 187 | process.exit(1); 188 | } 189 | break; 190 | case '--mock-data': 191 | if (i + 1 < args.length) { 192 | options.mockDataPath = args[i + 1]; 193 | i++; 194 | } else { 195 | console.error('❌ --mock-data requires a path'); 196 | process.exit(1); 197 | } 198 | break; 199 | default: 200 | if (arg.startsWith('--port=')) { 201 | options.port = parseInt(arg.split('=')[1]); 202 | } else if (arg.startsWith('--host=')) { 203 | options.host = arg.split('=')[1]; 204 | } else if (arg.startsWith('--mock-data=')) { 205 | options.mockDataPath = arg.split('=')[1]; 206 | } else { 207 | console.warn(`⚠️ Unknown argument: ${arg}`); 208 | } 209 | } 210 | } 211 | 212 | return options; 213 | } 214 | 215 | /** 216 | * Print help message 217 | */ 218 | function printHelp() { 219 | console.log('SFCC Mock Server - Unified WebDAV and OCAPI mock server'); 220 | console.log(''); 221 | console.log('Usage:'); 222 | console.log(' node server.js [options]'); 223 | console.log(''); 224 | console.log('Options:'); 225 | console.log(' --port <number> Server port (default: 3000)'); 226 | console.log(' --host <string> Server host (default: localhost)'); 227 | console.log(' --dev Enable development mode with verbose logging'); 228 | console.log(' --no-webdav Disable WebDAV functionality'); 229 | console.log(' --no-ocapi Disable OCAPI functionality'); 230 | console.log(' --no-cors Disable CORS headers'); 231 | console.log(' --enable-random-errors Enable random 500 errors (1% chance) for error handling testing'); 232 | console.log(' --mock-data <path> Custom path to mock data directory'); 233 | console.log(' --help Show this help message'); 234 | console.log(''); 235 | console.log('Examples:'); 236 | console.log(' node server.js --dev # Start in development mode'); 237 | console.log(' node server.js --port 4000 # Start on port 4000'); 238 | console.log(' node server.js --no-webdav # Only OCAPI endpoints'); 239 | console.log(' node server.js --host 0.0.0.0 --port 3001 # Bind to all interfaces'); 240 | } 241 | 242 | // CLI execution 243 | if (require.main === module) { 244 | const options = parseArgs(); 245 | const server = new SFCCMockServer(options); 246 | 247 | // Graceful shutdown handlers 248 | const gracefulShutdown = async (signal) => { 249 | console.log(`\\n🔄 Received ${signal}, shutting down gracefully...`); 250 | try { 251 | await server.stop(); 252 | process.exit(0); 253 | } catch (error) { 254 | console.error('❌ Error during shutdown:', error); 255 | process.exit(1); 256 | } 257 | }; 258 | 259 | process.on('SIGINT', () => gracefulShutdown('SIGINT')); 260 | process.on('SIGTERM', () => gracefulShutdown('SIGTERM')); 261 | 262 | // Handle uncaught exceptions 263 | process.on('uncaughtException', (error) => { 264 | console.error('❌ Uncaught Exception:', error); 265 | process.exit(1); 266 | }); 267 | 268 | process.on('unhandledRejection', (reason, promise) => { 269 | console.error('❌ Unhandled Rejection at:', promise, 'reason:', reason); 270 | process.exit(1); 271 | }); 272 | 273 | // Start the server 274 | server.start().catch((error) => { 275 | console.error('💥 Failed to start server:', error.message); 276 | process.exit(1); 277 | }); 278 | } 279 | 280 | module.exports = SFCCMockServer; ``` -------------------------------------------------------------------------------- /docs/dw_order/ShippingOrderItem.md: -------------------------------------------------------------------------------- ```markdown 1 | ## Package: dw.order 2 | 3 | # Class ShippingOrderItem 4 | 5 | ## Inheritance Hierarchy 6 | 7 | - Object 8 | - dw.object.Extensible 9 | - dw.order.AbstractItem 10 | - dw.order.ShippingOrderItem 11 | 12 | ## Description 13 | 14 | One or more ShippingOrderItems are contained in a ShippingOrder, created using ShippingOrder.createShippingOrderItem(OrderItem, Quantity) and can be retrieved by ShippingOrder.getItems(). A ShippingOrderItem references a single OrderItem which in turn references a LineItem associated with an Order. 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 | ### STATUS_CANCELLED 19 | 20 | **Type:** String = "CANCELLED" 21 | 22 | Constant for Order Item Status CANCELLED 23 | 24 | ### STATUS_CONFIRMED 25 | 26 | **Type:** String = "CONFIRMED" 27 | 28 | Constant for Order Item Status CONFIRMED 29 | 30 | ### STATUS_SHIPPED 31 | 32 | **Type:** String = "SHIPPED" 33 | 34 | Constant for Order Item Status SHIPPED 35 | 36 | ### STATUS_WAREHOUSE 37 | 38 | **Type:** String = "WAREHOUSE" 39 | 40 | Constant for Order Item Status WAREHOUSE 41 | 42 | ## Properties 43 | 44 | ### basePrice 45 | 46 | **Type:** Money (Read Only) 47 | 48 | Price of a single unit before discount application. 49 | 50 | ### parentItem 51 | 52 | **Type:** ShippingOrderItem 53 | 54 | Returns null or the parent item. 55 | 56 | ### quantity 57 | 58 | **Type:** Quantity (Read Only) 59 | 60 | The quantity of the shipping order item. 61 | 62 | The Quantity is equal to the related line item quantity. 63 | 64 | ### shippingOrderNumber 65 | 66 | **Type:** String (Read Only) 67 | 68 | The mandatory shipping order number of the related 69 | ShippingOrder. 70 | 71 | ### status 72 | 73 | **Type:** EnumValue 74 | 75 | Gets the order item status. 76 | 77 | The possible values are STATUS_CONFIRMED, 78 | STATUS_WAREHOUSE, STATUS_SHIPPED, 79 | STATUS_CANCELLED. 80 | 81 | ### trackingRefs 82 | 83 | **Type:** FilteringCollection (Read Only) 84 | 85 | Gets the tracking refs (tracking infos) the shipping order item is 86 | assigned to. 87 | 88 | ## Constructor Summary 89 | 90 | ## Method Summary 91 | 92 | ### addTrackingRef 93 | 94 | **Signature:** `addTrackingRef(trackingInfoID : String, quantity : Quantity) : TrackingRef` 95 | 96 | A shipping order item can be assigned to one or many tracking infos with different quantities. 97 | 98 | ### applyPriceRate 99 | 100 | **Signature:** `applyPriceRate(factor : Decimal, divisor : Decimal, roundUp : boolean) : void` 101 | 102 | Apply a rate of (factor / divisor) to the prices in this item, with the option to half round up or half round down to the nearest cent if necessary. 103 | 104 | ### getBasePrice 105 | 106 | **Signature:** `getBasePrice() : Money` 107 | 108 | Price of a single unit before discount application. 109 | 110 | ### getParentItem 111 | 112 | **Signature:** `getParentItem() : ShippingOrderItem` 113 | 114 | Returns null or the parent item. 115 | 116 | ### getQuantity 117 | 118 | **Signature:** `getQuantity() : Quantity` 119 | 120 | The quantity of the shipping order item. 121 | 122 | ### getShippingOrderNumber 123 | 124 | **Signature:** `getShippingOrderNumber() : String` 125 | 126 | The mandatory shipping order number of the related ShippingOrder. 127 | 128 | ### getStatus 129 | 130 | **Signature:** `getStatus() : EnumValue` 131 | 132 | Gets the order item status. 133 | 134 | ### getTrackingRefs 135 | 136 | **Signature:** `getTrackingRefs() : FilteringCollection` 137 | 138 | Gets the tracking refs (tracking infos) the shipping order item is assigned to. 139 | 140 | ### setParentItem 141 | 142 | **Signature:** `setParentItem(parentItem : ShippingOrderItem) : void` 143 | 144 | Set a parent item. 145 | 146 | ### setStatus 147 | 148 | **Signature:** `setStatus(status : String) : void` 149 | 150 | Sets the status. 151 | 152 | ### split 153 | 154 | **Signature:** `split(quantity : Quantity) : ShippingOrderItem` 155 | 156 | Split the shipping order item. 157 | 158 | ### split 159 | 160 | **Signature:** `split(quantity : Quantity, splitOrderItem : boolean) : ShippingOrderItem` 161 | 162 | Split the shipping order item. 163 | 164 | ## Method Detail 165 | 166 | ## Method Details 167 | 168 | ### addTrackingRef 169 | 170 | **Signature:** `addTrackingRef(trackingInfoID : String, quantity : Quantity) : TrackingRef` 171 | 172 | **Description:** A shipping order item can be assigned to one or many tracking infos with different quantities. For example an item with quantity 3 may have been shipped in 2 packages, each represented by its own tracking info - 2 TrackingRefs would exist with quantities 1 and 2. This method creates and adds a new tracking reference to this shipping order item for a given tracking info and quantity. The new instance is returned. 173 | 174 | **Parameters:** 175 | 176 | - `trackingInfoID`: the id of the tracking info 177 | - `quantity`: the quantity the which is assigned to the tracking info for this shipping order item. Optional (null is allowed). 178 | 179 | **Returns:** 180 | 181 | the new tracking reference 182 | 183 | **See Also:** 184 | 185 | TrackingRef 186 | 187 | --- 188 | 189 | ### applyPriceRate 190 | 191 | **Signature:** `applyPriceRate(factor : Decimal, divisor : Decimal, roundUp : boolean) : void` 192 | 193 | **Description:** Apply a rate of (factor / divisor) to the prices in this item, with the option to half round up or half round down to the nearest cent if necessary. Examples: TaxBasis beforefactordivisorroundupCalculationTaxBasis after $10.0012true10*1/2=$5.00 $10.00910true10*9/10=$9.00 $10.0013true10*1/3=3.3333=$3.33 $2.4712true2.47*1/2=1.235=$1.24 $2.4712false2.47*1/2=1.235=$1.23 Which prices are updated?: The rate described above is applied to tax-basis and tax then the net-price and gross-price are recalculated by adding / subtracting depending on whether the order is based on net price. Example (order based on net price) New TaxBasis:$10.00, Tax:$1.00, NetPrice=TaxBasis=$10.00, GrossPrice=TaxBasis+Tax=$11.00 Example (order based on gross price) New TaxBasis:$10.00, Tax:$1.00, NetPrice=TaxBasis-tax=$9.00, GrossPrice=TaxBasis=$10.00 194 | 195 | **Parameters:** 196 | 197 | - `factor`: factor used to calculate rate 198 | - `divisor`: divisor used to calculate rate 199 | - `roundUp`: whether to round up or down on 0.5 200 | 201 | **See Also:** 202 | 203 | AbstractItem.getTaxBasis() 204 | AbstractItem.getTax() 205 | AbstractItem.getNetPrice() 206 | AbstractItem.getGrossPrice() 207 | TaxMgr.getTaxationPolicy() 208 | 209 | --- 210 | 211 | ### getBasePrice 212 | 213 | **Signature:** `getBasePrice() : Money` 214 | 215 | **Description:** Price of a single unit before discount application. 216 | 217 | **Returns:** 218 | 219 | Price of a single unit before discount application. 220 | 221 | --- 222 | 223 | ### getParentItem 224 | 225 | **Signature:** `getParentItem() : ShippingOrderItem` 226 | 227 | **Description:** Returns null or the parent item. 228 | 229 | **Returns:** 230 | 231 | null or the parent item. 232 | 233 | --- 234 | 235 | ### getQuantity 236 | 237 | **Signature:** `getQuantity() : Quantity` 238 | 239 | **Description:** The quantity of the shipping order item. The Quantity is equal to the related line item quantity. 240 | 241 | **Returns:** 242 | 243 | the quantity 244 | 245 | --- 246 | 247 | ### getShippingOrderNumber 248 | 249 | **Signature:** `getShippingOrderNumber() : String` 250 | 251 | **Description:** The mandatory shipping order number of the related ShippingOrder. 252 | 253 | **Returns:** 254 | 255 | the shipping order number. 256 | 257 | --- 258 | 259 | ### getStatus 260 | 261 | **Signature:** `getStatus() : EnumValue` 262 | 263 | **Description:** Gets the order item status. The possible values are STATUS_CONFIRMED, STATUS_WAREHOUSE, STATUS_SHIPPED, STATUS_CANCELLED. 264 | 265 | **Returns:** 266 | 267 | the status 268 | 269 | --- 270 | 271 | ### getTrackingRefs 272 | 273 | **Signature:** `getTrackingRefs() : FilteringCollection` 274 | 275 | **Description:** Gets the tracking refs (tracking infos) the shipping order item is assigned to. 276 | 277 | **Returns:** 278 | 279 | the tracking refs ( tracking infos - TrackingRef ) the shipping order item is assigned to. 280 | 281 | **See Also:** 282 | 283 | TrackingRef 284 | 285 | --- 286 | 287 | ### setParentItem 288 | 289 | **Signature:** `setParentItem(parentItem : ShippingOrderItem) : void` 290 | 291 | **Description:** Set a parent item. The parent item must belong to the same ShippingOrder. An infinite parent-child loop is disallowed as is a parent-child depth greater than 10. Setting a parent item indicates a dependency of the child item on the parent item, and can be used to form a parallel structure to that accessed using ProductLineItem.getParent(). 292 | 293 | **Parameters:** 294 | 295 | - `parentItem`: The parent item, null is allowed 296 | 297 | --- 298 | 299 | ### setStatus 300 | 301 | **Signature:** `setStatus(status : String) : void` 302 | 303 | **Description:** Sets the status. See ShippingOrder for details of shipping order status transitions. Do not use this method to set a shipping order to status WAREHOUSE, instead use ShippingOrder.setStatusWarehouse() This also triggers the setting of the status of the LineItem when appropriate. Setting this status can also have an impact on the order status, accessed using Order.getStatus() and the shipping order status, accessed using ShippingOrder.getStatus(). 304 | 305 | **Parameters:** 306 | 307 | - `status`: the status 308 | 309 | **Throws:** 310 | 311 | NullPointerException - if status is null 312 | IllegalArgumentException - if the status transition to the status is not allowed 313 | 314 | --- 315 | 316 | ### split 317 | 318 | **Signature:** `split(quantity : Quantity) : ShippingOrderItem` 319 | 320 | **Description:** Split the shipping order item. This will also lead to a split of the related LineItem. Split means that for the passed quantity a new item is created with this quantity as an exact copy of this item. The remaining amount will stay in this item. If quantity is equal to getQuantity() no split is done and this item is returned itself. This method is equivalent to split(Quantity, Boolean) called with splitOrderItem equals to true. 321 | 322 | **Parameters:** 323 | 324 | - `quantity`: the quantity for the newly created item 325 | 326 | **Returns:** 327 | 328 | the newly created item or this item 329 | 330 | **Throws:** 331 | 332 | IllegalArgumentException - if quantity is greater than getQuantity() 333 | 334 | --- 335 | 336 | ### split 337 | 338 | **Signature:** `split(quantity : Quantity, splitOrderItem : boolean) : ShippingOrderItem` 339 | 340 | **Description:** Split the shipping order item. This will also lead to a split of the related LineItem when splitOrderItem is true. Split means that for the passed quantity a new item is created with this quantity as an exact copy of this item. The remaining amount will stay in this item. If quantity is equal to getQuantity() no split is done and this item is returned itself. 341 | 342 | **Parameters:** 343 | 344 | - `quantity`: the quantity for the newly created item 345 | - `splitOrderItem`: true the related LineItem will be splitted too false the related LineItem will not be splitted 346 | 347 | **Returns:** 348 | 349 | the newly created item or this item 350 | 351 | **Throws:** 352 | 353 | IllegalArgumentException - if quantity is greater than getQuantity() 354 | 355 | --- ``` -------------------------------------------------------------------------------- /docs/dw_system/RESTResponseMgr.md: -------------------------------------------------------------------------------- ```markdown 1 | ## Package: dw.system 2 | 3 | # Class RESTResponseMgr 4 | 5 | ## Inheritance Hierarchy 6 | 7 | - Object 8 | - dw.system.RESTResponseMgr 9 | 10 | ## Description 11 | 12 | This class provides helper methods for creating REST error and success responses. It is mainly intended to be used to build Custom REST APIs. But, any controller implementation planning to provide REST-like responses can use these methods. If these methods are being used in the controllers, note that a few defaults like URL prefix for type in createError methods will correspond to Custom REST APIs. 13 | 14 | ## Constructor Summary 15 | 16 | RESTResponseMgr() 17 | 18 | ## Method Summary 19 | 20 | ### createEmptySuccess 21 | 22 | **Signature:** `static createEmptySuccess(statusCode : Number) : RESTSuccessResponse` 23 | 24 | Constructs a new RESTSuccessResponse object. 25 | 26 | ### createError 27 | 28 | **Signature:** `static createError(statusCode : Number) : RESTErrorResponse` 29 | 30 | Constructs a new RESTErrorResponse object. 31 | 32 | ### createError 33 | 34 | **Signature:** `static createError(statusCode : Number, type : String) : RESTErrorResponse` 35 | 36 | Constructs a new RESTErrorResponse object. 37 | 38 | ### createError 39 | 40 | **Signature:** `static createError(statusCode : Number, type : String, title : String) : RESTErrorResponse` 41 | 42 | Constructs a new RESTErrorResponse object. 43 | 44 | ### createError 45 | 46 | **Signature:** `static createError(statusCode : Number, type : String, title : String, detail : String) : RESTErrorResponse` 47 | 48 | Constructs a new RESTErrorResponse object. 49 | 50 | ### createScapiRemoteInclude 51 | 52 | **Signature:** `static createScapiRemoteInclude(apiFamily : String, apiName : String, apiVersion : String, resourcePath : String, params : URLParameter...) : RemoteInclude` 53 | 54 | Constructs a new RemoteInclude object specific for the SCAPI include path. 55 | 56 | ### createStorefrontControllerRemoteInclude 57 | 58 | **Signature:** `static createStorefrontControllerRemoteInclude(action : URLAction, params : URLParameter...) : RemoteInclude` 59 | 60 | Constructs a new RemoteInclude object specific for the Storefront Controller include path. 61 | 62 | ### createSuccess 63 | 64 | **Signature:** `static createSuccess(body : Object, statusCode : Number) : RESTSuccessResponse` 65 | 66 | Constructs a new RESTSuccessResponse object. 67 | 68 | ### createSuccess 69 | 70 | **Signature:** `static createSuccess(body : Object) : RESTSuccessResponse` 71 | 72 | Constructs a new RESTSuccessResponse object. 73 | 74 | ## Constructor Detail 75 | 76 | ## Method Detail 77 | 78 | ## Method Details 79 | 80 | ### createEmptySuccess 81 | 82 | **Signature:** `static createEmptySuccess(statusCode : Number) : RESTSuccessResponse` 83 | 84 | **Description:** Constructs a new RESTSuccessResponse object. This method is to be used in scenarios where response body is not expected (e.g. statusCode is 204). 85 | 86 | **Parameters:** 87 | 88 | - `statusCode`: The http status code of the response. The statusCode parameter should conform to RFC standards for a success. 89 | 90 | **Returns:** 91 | 92 | A new RESTSuccessResponse object. 93 | 94 | **Throws:** 95 | 96 | IllegalArgumentException - If the statusCode is not in the (100..299) range. 97 | 98 | --- 99 | 100 | ### createError 101 | 102 | **Signature:** `static createError(statusCode : Number) : RESTErrorResponse` 103 | 104 | **Description:** Constructs a new RESTErrorResponse object. This method should be used when you have just the statusCode of the error and want the type of error to be inferred. 'type' of the error is inferred from the status code as follows: 400 - bad-request 401 - unauthorized 403 - forbidden 404 - resource-not-found 409 - conflict 412 - precondition-failed 429 - too-many-requests 500 - internal-server-error default - about:blank 105 | 106 | **Parameters:** 107 | 108 | - `statusCode`: The error code of the response. The statusCode parameter should conform to RFC standards for an error. 109 | 110 | **Returns:** 111 | 112 | A new RESTErrorResponse object. 113 | 114 | **Throws:** 115 | 116 | IllegalArgumentException - If the statusCode is not in the (400..599) range. 117 | 118 | --- 119 | 120 | ### createError 121 | 122 | **Signature:** `static createError(statusCode : Number, type : String) : RESTErrorResponse` 123 | 124 | **Description:** Constructs a new RESTErrorResponse object. This method should be used when you want to omit 'title' and 'detail' of the error. With this method, custom error codes and types apart from the standard ones can be constructed. 125 | 126 | **Parameters:** 127 | 128 | - `statusCode`: The error code of the response. The statusCode parameter should conform to RFC standards for an error. 129 | - `type`: Type of the error according to RFC 9457. We enforce the following restrictions on top of the RFC: If the provided type is not an absolute URL, it will be prepended with https://api.commercecloud.salesforce.com/documentation/error/v1/custom-errors/. Custom error types are not allowed to have SYSTEM error type prefix: https://api.commercecloud.salesforce.com/documentation/error/v1/errors/. 130 | 131 | **Returns:** 132 | 133 | A new RESTErrorResponse object. 134 | 135 | **Throws:** 136 | 137 | IllegalArgumentException - If the statusCode is not in the (400..599) range or if the error type is not a valid URI or conflicts with the SYSTEM error type namespace. 138 | 139 | --- 140 | 141 | ### createError 142 | 143 | **Signature:** `static createError(statusCode : Number, type : String, title : String) : RESTErrorResponse` 144 | 145 | **Description:** Constructs a new RESTErrorResponse object. This method should be used when you want to omit 'detail' of the error but want to have valid 'statusCode', 'type' and 'title'. 146 | 147 | **Parameters:** 148 | 149 | - `statusCode`: The error code of the response. The statusCode parameter should conform to RFC standards for an error. 150 | - `type`: Type of the error according to RFC 9457. We enforce the following restrictions on top of the RFC: If the provided type is not an absolute URL, it will be prepended with https://api.commercecloud.salesforce.com/documentation/error/v1/custom-errors/. Custom error types are not allowed to have SYSTEM error type prefix: https://api.commercecloud.salesforce.com/documentation/error/v1/errors/. 151 | - `title`: Human-readable summary of the error type. 152 | 153 | **Returns:** 154 | 155 | A new RESTErrorResponse object. 156 | 157 | **Throws:** 158 | 159 | IllegalArgumentException - If the statusCode is not in the (400..599) range or if the error type is not a valid URI or conflicts with SYSTEM error type namespace. 160 | 161 | --- 162 | 163 | ### createError 164 | 165 | **Signature:** `static createError(statusCode : Number, type : String, title : String, detail : String) : RESTErrorResponse` 166 | 167 | **Description:** Constructs a new RESTErrorResponse object. This method can be used to construct error responses with valid 'statusCode', 'type', 'title' and 'detail'. If you want to omit title or detail, you can pass in null. 168 | 169 | **Parameters:** 170 | 171 | - `statusCode`: The error code of the response. The statusCode parameter should conform to RFC standards for an error. 172 | - `type`: Type of the error according to RFC 9457. We enforce the following restrictions on top of the RFC: If the provided type is not an absolute URL, it will be prepended with https://api.commercecloud.salesforce.com/documentation/error/v1/custom-errors/. Custom error types are not allowed to have SYSTEM error type prefix: https://api.commercecloud.salesforce.com/documentation/error/v1/errors/. 173 | - `title`: Human-readable summary of the error type. 174 | - `detail`: Human-readable explanation of the specific occurrence of the error. 175 | 176 | **Returns:** 177 | 178 | A new RESTErrorResponse object. 179 | 180 | **Throws:** 181 | 182 | IllegalArgumentException - If the statusCode is not in the (400..599) range or if the error type is not a valid URI or conflicts with SYSTEM error type namespace. 183 | 184 | --- 185 | 186 | ### createScapiRemoteInclude 187 | 188 | **Signature:** `static createScapiRemoteInclude(apiFamily : String, apiName : String, apiVersion : String, resourcePath : String, params : URLParameter...) : RemoteInclude` 189 | 190 | **Description:** Constructs a new RemoteInclude object specific for the SCAPI include path. Usage: SCAPI remote include URL have following form: BASE_PATH/{apiFamily}/{apiName}/{apiVersion}/organizations/ORG_ID/{resourcePath}[?params] For the given SCAPI resource path: BASE_PATH/product/shopper-products/v1/organizations/ORG_ID/categories/root?siteId=YourShopHere RemoteInclude object can be constructed in a script like following: let include = dw.system.RESTResponseMgr.createScapiRemoteInclude("product", "shopper-products", "v1", "categories/root", dw.web.URLParameter("siteId", "YourShopHere")); Please notice that 'BASE_PATH' and 'ORG_ID' are automatically resolved. 191 | 192 | **Parameters:** 193 | 194 | - `apiFamily`: an API Family name. Example: 'product'. 195 | - `apiName`: an API Name. Example: 'shopper-products'. 196 | - `apiVersion`: an API Version. Example: 'v1'. 197 | - `resourcePath`: a Resource path. Example: 'categories/root' 198 | - `params`: a query parameters (optional) 199 | 200 | **Returns:** 201 | 202 | a new instance of RemoteInclude. 203 | 204 | --- 205 | 206 | ### createStorefrontControllerRemoteInclude 207 | 208 | **Signature:** `static createStorefrontControllerRemoteInclude(action : URLAction, params : URLParameter...) : RemoteInclude` 209 | 210 | **Description:** Constructs a new RemoteInclude object specific for the Storefront Controller include path. 211 | 212 | **Parameters:** 213 | 214 | - `action`: a container to specify target controller. Hostnames in URL actions are ignored. 215 | - `params`: a query parameters (optional). 216 | 217 | **Returns:** 218 | 219 | a new instance of RemoteInclude. 220 | 221 | --- 222 | 223 | ### createSuccess 224 | 225 | **Signature:** `static createSuccess(body : Object, statusCode : Number) : RESTSuccessResponse` 226 | 227 | **Description:** Constructs a new RESTSuccessResponse object. 228 | 229 | **Parameters:** 230 | 231 | - `body`: The body of the successful response. This should always be a valid JavaScript JSON object. 232 | - `statusCode`: The http status code of the response. The statusCode parameter should conform to RFC standards for a success. 233 | 234 | **Returns:** 235 | 236 | A new RESTSuccessResponse object. 237 | 238 | **Throws:** 239 | 240 | IllegalArgumentException - If the statusCode is not in the (100..299) range. 241 | 242 | --- 243 | 244 | ### createSuccess 245 | 246 | **Signature:** `static createSuccess(body : Object) : RESTSuccessResponse` 247 | 248 | **Description:** Constructs a new RESTSuccessResponse object. HTTP status code of the response will be defaulted to 200. 249 | 250 | **Parameters:** 251 | 252 | - `body`: The body of the successful response. This should always be a valid JavaScript JSON object. 253 | 254 | **Returns:** 255 | 256 | A new RESTSuccessResponse object. 257 | 258 | --- ``` -------------------------------------------------------------------------------- /tests/cartridge-handler.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { CartridgeToolHandler } from '../src/core/handlers/cartridge-handler.js'; 2 | import { HandlerContext } from '../src/core/handlers/base-handler.js'; 3 | import { Logger } from '../src/utils/logger.js'; 4 | 5 | // Mock cartridge client 6 | const mockCartridgeClient = { 7 | generateCartridgeStructure: jest.fn(), 8 | }; 9 | 10 | // Mock the ClientFactory to return our mock client 11 | jest.mock('../src/core/handlers/client-factory.js', () => ({ 12 | ClientFactory: jest.fn().mockImplementation(() => ({ 13 | createCartridgeClient: jest.fn(() => mockCartridgeClient), 14 | })), 15 | })); 16 | 17 | describe('CartridgeToolHandler', () => { 18 | let mockLogger: jest.Mocked<Logger>; 19 | let mockClient: typeof mockCartridgeClient; 20 | let context: HandlerContext; 21 | let handler: CartridgeToolHandler; 22 | 23 | beforeEach(() => { 24 | mockLogger = { 25 | debug: jest.fn(), 26 | log: jest.fn(), 27 | error: jest.fn(), 28 | timing: jest.fn(), 29 | methodEntry: jest.fn(), 30 | methodExit: jest.fn(), 31 | } as any; 32 | 33 | // Reset mocks 34 | jest.clearAllMocks(); 35 | 36 | // Use the mock client directly and reset it 37 | mockClient = mockCartridgeClient; 38 | mockClient.generateCartridgeStructure.mockReset(); 39 | 40 | jest.spyOn(Logger, 'getChildLogger').mockReturnValue(mockLogger); 41 | 42 | context = { 43 | logger: mockLogger, 44 | config: null as any, 45 | capabilities: { canAccessLogs: false, canAccessOCAPI: false }, 46 | }; 47 | 48 | handler = new CartridgeToolHandler(context, 'Cartridge'); 49 | }); 50 | 51 | afterEach(() => { 52 | jest.restoreAllMocks(); 53 | }); 54 | 55 | // Helper function to initialize handler for tests that need it 56 | const initializeHandler = async () => { 57 | await (handler as any).initialize(); 58 | }; 59 | 60 | describe('canHandle', () => { 61 | it('should handle cartridge tools', () => { 62 | expect(handler.canHandle('generate_cartridge_structure')).toBe(true); 63 | }); 64 | 65 | it('should not handle non-cartridge tools', () => { 66 | expect(handler.canHandle('get_latest_error')).toBe(false); 67 | expect(handler.canHandle('unknown_tool')).toBe(false); 68 | }); 69 | }); 70 | 71 | describe('initialization', () => { 72 | it('should initialize cartridge generation client', async () => { 73 | await initializeHandler(); 74 | 75 | const MockedClientFactory = jest.requireMock('../src/core/handlers/client-factory.js').ClientFactory; 76 | const mockFactoryInstance = MockedClientFactory.mock.results[0].value; 77 | expect(mockFactoryInstance.createCartridgeClient).toHaveBeenCalled(); 78 | expect(mockLogger.debug).toHaveBeenCalledWith('Cartridge generation client initialized'); 79 | }); 80 | }); 81 | 82 | describe('disposal', () => { 83 | it('should dispose cartridge generation client properly', async () => { 84 | await initializeHandler(); 85 | await (handler as any).dispose(); 86 | 87 | expect(mockLogger.debug).toHaveBeenCalledWith('Cartridge generation client disposed'); 88 | }); 89 | }); 90 | 91 | describe('generate_cartridge_structure tool', () => { 92 | beforeEach(async () => { 93 | await initializeHandler(); 94 | }); 95 | 96 | it('should handle generate_cartridge_structure with cartridgeName', async () => { 97 | mockClient.generateCartridgeStructure.mockResolvedValue({ 98 | success: true, 99 | cartridgeName: 'plugin_example', 100 | targetPath: '/path/to/project', 101 | filesCreated: [ 102 | 'cartridges/plugin_example/cartridge/controllers/Product.js', 103 | 'cartridges/plugin_example/cartridge/scripts/helpers/ProductHelper.js', 104 | 'package.json', 105 | ], 106 | }); 107 | 108 | const args = { cartridgeName: 'plugin_example' }; 109 | const result = await handler.handle('generate_cartridge_structure', args, Date.now()); 110 | 111 | expect(mockClient.generateCartridgeStructure).toHaveBeenCalledWith({ 112 | cartridgeName: 'plugin_example', 113 | targetPath: undefined, 114 | fullProjectSetup: true, 115 | }); 116 | expect(result.content[0].text).toContain('plugin_example'); 117 | }); 118 | 119 | it('should handle generate_cartridge_structure with all options', async () => { 120 | mockClient.generateCartridgeStructure.mockResolvedValue({ 121 | success: true, 122 | cartridgeName: 'custom_cartridge', 123 | targetPath: '/custom/path', 124 | filesCreated: [ 125 | 'cartridges/custom_cartridge/cartridge/controllers/Product.js', 126 | ], 127 | }); 128 | 129 | const args = { 130 | cartridgeName: 'custom_cartridge', 131 | targetPath: '/custom/path', 132 | fullProjectSetup: false, 133 | }; 134 | const result = await handler.handle('generate_cartridge_structure', args, Date.now()); 135 | 136 | expect(mockClient.generateCartridgeStructure).toHaveBeenCalledWith({ 137 | cartridgeName: 'custom_cartridge', 138 | targetPath: '/custom/path', 139 | fullProjectSetup: false, 140 | }); 141 | expect(result.content[0].text).toContain('custom_cartridge'); 142 | }); 143 | 144 | it('should use default fullProjectSetup when not provided', async () => { 145 | mockClient.generateCartridgeStructure.mockResolvedValue({ 146 | success: true, 147 | cartridgeName: 'test_cartridge', 148 | filesCreated: [], 149 | }); 150 | 151 | const args = { 152 | cartridgeName: 'test_cartridge', 153 | targetPath: '/test/path', 154 | }; 155 | await handler.handle('generate_cartridge_structure', args, Date.now()); 156 | 157 | expect(mockClient.generateCartridgeStructure).toHaveBeenCalledWith({ 158 | cartridgeName: 'test_cartridge', 159 | targetPath: '/test/path', 160 | fullProjectSetup: true, 161 | }); 162 | }); 163 | 164 | it('should throw error when cartridgeName is missing', async () => { 165 | const result = await handler.handle('generate_cartridge_structure', {}, Date.now()); 166 | expect(result.isError).toBe(true); 167 | expect(result.content[0].text).toContain('cartridgeName must be a valid identifier'); 168 | }); 169 | 170 | it('should throw error when cartridgeName is empty', async () => { 171 | const result = await handler.handle('generate_cartridge_structure', { cartridgeName: '' }, Date.now()); 172 | expect(result.isError).toBe(true); 173 | expect(result.content[0].text).toContain('cartridgeName must be a valid identifier'); 174 | }); 175 | 176 | it('should throw error when cartridgeName is not a string', async () => { 177 | const result = await handler.handle('generate_cartridge_structure', { cartridgeName: 123 }, Date.now()); 178 | expect(result.isError).toBe(true); 179 | expect(result.content[0].text).toContain('cartridgeName must be a valid identifier'); 180 | }); 181 | }); 182 | 183 | describe('error handling', () => { 184 | beforeEach(async () => { 185 | await initializeHandler(); 186 | }); 187 | 188 | it('should handle client errors gracefully', async () => { 189 | mockClient.generateCartridgeStructure.mockRejectedValue(new Error('Directory already exists')); 190 | 191 | const result = await handler.handle('generate_cartridge_structure', { cartridgeName: 'existing_cartridge' }, Date.now()); 192 | expect(result.isError).toBe(true); 193 | expect(result.content[0].text).toContain('Directory already exists'); 194 | }); 195 | 196 | it('should throw error for unsupported tools', async () => { 197 | await expect(handler.handle('unsupported_tool', {}, Date.now())) 198 | .rejects.toThrow('Unsupported tool'); 199 | }); 200 | }); 201 | 202 | describe('timing and logging', () => { 203 | beforeEach(async () => { 204 | await initializeHandler(); 205 | mockClient.generateCartridgeStructure.mockResolvedValue({ 206 | success: true, 207 | cartridgeName: 'test_cartridge', 208 | filesCreated: [], 209 | }); 210 | }); 211 | 212 | it('should log timing information', async () => { 213 | const startTime = Date.now(); 214 | await handler.handle('generate_cartridge_structure', { cartridgeName: 'test_cartridge' }, startTime); 215 | 216 | expect(mockLogger.timing).toHaveBeenCalledWith('generate_cartridge_structure', startTime); 217 | }); 218 | 219 | it('should log execution details', async () => { 220 | await handler.handle('generate_cartridge_structure', { cartridgeName: 'test_cartridge' }, Date.now()); 221 | 222 | expect(mockLogger.debug).toHaveBeenCalledWith( 223 | 'generate_cartridge_structure completed successfully', 224 | expect.any(Object), 225 | ); 226 | }); 227 | 228 | it('should create appropriate log message', async () => { 229 | mockClient.generateCartridgeStructure.mockResolvedValue({ 230 | success: true, 231 | cartridgeName: 'my_cartridge', 232 | filesCreated: [], 233 | }); 234 | 235 | await handler.handle('generate_cartridge_structure', { cartridgeName: 'my_cartridge' }, Date.now()); 236 | 237 | // Check that the debug log contains the execution details 238 | expect(mockLogger.debug).toHaveBeenCalledWith( 239 | 'generate_cartridge_structure completed successfully', 240 | expect.any(Object), 241 | ); 242 | }); 243 | }); 244 | 245 | describe('client integration', () => { 246 | beforeEach(async () => { 247 | await initializeHandler(); 248 | }); 249 | 250 | it('should pass correct parameters to client for minimal request', async () => { 251 | mockClient.generateCartridgeStructure.mockResolvedValue({ 252 | success: true, 253 | cartridgeName: 'minimal_cartridge', 254 | filesCreated: ['cartridges/minimal_cartridge/cartridge/controllers/Default.js'], 255 | }); 256 | 257 | const args = { cartridgeName: 'minimal_cartridge' }; 258 | 259 | await handler.handle('generate_cartridge_structure', args, Date.now()); 260 | 261 | expect(mockClient.generateCartridgeStructure).toHaveBeenCalledWith({ 262 | cartridgeName: 'minimal_cartridge', 263 | targetPath: undefined, 264 | fullProjectSetup: true, 265 | }); 266 | }); 267 | 268 | it('should pass correct parameters to client for full request', async () => { 269 | mockClient.generateCartridgeStructure.mockResolvedValue({ 270 | success: true, 271 | cartridgeName: 'full_cartridge', 272 | filesCreated: ['cartridges/full_cartridge/cartridge/controllers/Default.js'], 273 | }); 274 | 275 | const args = { 276 | cartridgeName: 'full_cartridge', 277 | targetPath: '/workspace/my-project', 278 | fullProjectSetup: false, 279 | }; 280 | 281 | await handler.handle('generate_cartridge_structure', args, Date.now()); 282 | 283 | expect(mockClient.generateCartridgeStructure).toHaveBeenCalledWith({ 284 | cartridgeName: 'full_cartridge', 285 | targetPath: '/workspace/my-project', 286 | fullProjectSetup: false, 287 | }); 288 | }); 289 | }); 290 | }); 291 | ``` -------------------------------------------------------------------------------- /docs/dw_svc/ServiceCallback.md: -------------------------------------------------------------------------------- ```markdown 1 | ## Package: dw.svc 2 | 3 | # Class ServiceCallback 4 | 5 | ## Inheritance Hierarchy 6 | 7 | - Object 8 | - dw.svc.ServiceCallback 9 | 10 | ## Description 11 | 12 | Defines callbacks for use with the LocalServiceRegistry. Note this class itself is not used directly, and is present only for documentation of the available callback methods. These methods are called in sequence when a service is called: initServiceClient(Service) -- Creates the underlying client that will be used to make the call. This is intended for SOAP Services. Other client types will be created automatically. createRequest(Service, Object...) -- Given arguments to the Service.call(Object...), configure the actual service request. This may include setting request headers, defining the message body, etc. execute(Service, Object) -- Perform the actual request. At this point the client has been configured with the relevant credentials, so the call should be made. This is required for SOAP services. parseResponse(Service, Object) -- Convert the result of the call into an object to be returned from the Service.call(Object...) method. If the service is mocked (see Service.isMock()), then mockFull(Service, Object...) takes the place of this entire sequence. If that is not implemented, then mockCall(Service, Object) takes the place of just the execute(Service, Object) method. The URL, request, and response objects may be logged. To avoid logging sensitive data, filterLogMessage(String) and/or getRequestLogMessage(Object) and getResponseLogMessage(Object) must be implemented. If they are not implemented then this logging will not be done on Production environments. There are some special considerations for the combination of service type and callback: Service Type initServiceClient createRequest execute parseResponse HTTP Not normally implemented. Must return a HTTPClient Required unless execute is provided. The return value is expected to be either a String or array of HTTPRequestPart, which will be used as the request body Not called unless a boolean "executeOverride:true" is set on the callback. This is a temporary limitation, a future release will always call this callback if it is present Required unless execute is provided. HTTPForm Not normally implemented. Must return a HTTPClient Not normally implemented. Default behavior constructs an "application/x-www-form-urlencoded" request based on a Map given as an argument. Not normally implemented. The same limitations as HTTP regarding the "executeOverride" flag apply here. Optional. Default behavior is to return the response body as a String. SOAP Optional. This must return the Webservice stub or port Required. If initServiceClient was not provided, then this function must call SOAPService.setServiceClient(Object) with the stub or port Required. A typical implementation will call the webservice via a method on the service client Optional. Default behavior returns the output of execute FTP Not normally implemented. Must return a FTPClient or SFTPClient Required unless execute is defined. If present, it should call FTPService.setOperation(String, Object...) Optional. An implementation may call any required methods on the given client. The default implementation calls the Operation that was set up and returns the result. Optional. Default behavior returns the output of execute GENERIC Optional. Optional. Required. The GENERIC type allows any code to be wrapped in the service framework layer, and it's up to this execute method to define what that logic is. Optional. 13 | 14 | ## Properties 15 | 16 | ### URL 17 | 18 | **Type:** String (Read Only) 19 | 20 | Allows overriding the URL provided by the service configuration. 21 | 22 | It is usually better to call Service.setURL(String) within createRequest(Service, Object...) 23 | because that allows you to modify the existing URL based on call parameters. 24 | 25 | ## Constructor Summary 26 | 27 | ## Method Summary 28 | 29 | ### createRequest 30 | 31 | **Signature:** `createRequest(service : Service, params : Object...) : Object` 32 | 33 | Creates a request object to be used when calling the service. 34 | 35 | ### execute 36 | 37 | **Signature:** `execute(service : Service, request : Object) : Object` 38 | 39 | Provides service-specific execution logic. 40 | 41 | ### filterLogMessage 42 | 43 | **Signature:** `filterLogMessage(msg : String) : String` 44 | 45 | Allows filtering communication URL, request, and response log messages. 46 | 47 | ### getRequestLogMessage 48 | 49 | **Signature:** `getRequestLogMessage(request : Object) : String` 50 | 51 | Creates a communication log message for the given request. 52 | 53 | ### getResponseLogMessage 54 | 55 | **Signature:** `getResponseLogMessage(response : Object) : String` 56 | 57 | Creates a response log message for the given request. 58 | 59 | ### getURL 60 | 61 | **Signature:** `getURL() : String` 62 | 63 | Allows overriding the URL provided by the service configuration. 64 | 65 | ### initServiceClient 66 | 67 | **Signature:** `initServiceClient(service : Service) : Object` 68 | 69 | Creates a protocol-specific client object. 70 | 71 | ### mockCall 72 | 73 | **Signature:** `mockCall(service : Service, requestObj : Object) : Object` 74 | 75 | Override this method to mock the remote portion of the service call. 76 | 77 | ### mockFull 78 | 79 | **Signature:** `mockFull(service : Service, args : Object...) : Object` 80 | 81 | Override this method to mock the entire service call, including the createRequest, execute, and parseResponse phases. 82 | 83 | ### parseResponse 84 | 85 | **Signature:** `parseResponse(service : Service, response : Object) : Object` 86 | 87 | Creates a response object from a successful service call. 88 | 89 | ## Method Detail 90 | 91 | ## Method Details 92 | 93 | ### createRequest 94 | 95 | **Signature:** `createRequest(service : Service, params : Object...) : Object` 96 | 97 | **Description:** Creates a request object to be used when calling the service. The type of the object expected is dependent on the service. For example, the HTTPService expects the HTTP request body to be returned. This is required unless the execute method is implemented. It is not recommended to have a service accept a single array or list as a parameter, since doing so requires some extra work when actually calling the service. See Service.call(Object...) for more details. 98 | 99 | **Parameters:** 100 | 101 | - `service`: Service being executed. 102 | - `params`: Parameters given to the call method. 103 | 104 | **Returns:** 105 | 106 | Request object to give to the execute method. 107 | 108 | --- 109 | 110 | ### execute 111 | 112 | **Signature:** `execute(service : Service, request : Object) : Object` 113 | 114 | **Description:** Provides service-specific execution logic. This can be overridden to execute a chain of FTP commands in the FTPService, or perform the actual remote call on a webservice stub in the SOAPService. 115 | 116 | **Parameters:** 117 | 118 | - `service`: Service being executed. 119 | - `request`: Request object returned by createRequest(Service, Object...). 120 | 121 | **Returns:** 122 | 123 | Response from the underlying call, to be sent to parseResponse(Service, Object). 124 | 125 | **Throws:** 126 | 127 | - Exception 128 | 129 | --- 130 | 131 | ### filterLogMessage 132 | 133 | **Signature:** `filterLogMessage(msg : String) : String` 134 | 135 | **Description:** Allows filtering communication URL, request, and response log messages. If not implemented, then no filtering will be performed and the message will be logged as-is. 136 | 137 | **Parameters:** 138 | 139 | - `msg`: Original log message. 140 | 141 | **Returns:** 142 | 143 | Message to be logged. 144 | 145 | --- 146 | 147 | ### getRequestLogMessage 148 | 149 | **Signature:** `getRequestLogMessage(request : Object) : String` 150 | 151 | **Description:** Creates a communication log message for the given request. If not implemented then the default logic will be used to convert the request into a log message. 152 | 153 | **Parameters:** 154 | 155 | - `request`: Request object. 156 | 157 | **Returns:** 158 | 159 | Log message, or null to create and use the default message. 160 | 161 | --- 162 | 163 | ### getResponseLogMessage 164 | 165 | **Signature:** `getResponseLogMessage(response : Object) : String` 166 | 167 | **Description:** Creates a response log message for the given request. If not implemented then the default logic will be used to convert the response into a log message. 168 | 169 | **Parameters:** 170 | 171 | - `response`: Response object. 172 | 173 | **Returns:** 174 | 175 | Log message, or null to create and use the default message. 176 | 177 | --- 178 | 179 | ### getURL 180 | 181 | **Signature:** `getURL() : String` 182 | 183 | **Description:** Allows overriding the URL provided by the service configuration. It is usually better to call Service.setURL(String) within createRequest(Service, Object...) because that allows you to modify the existing URL based on call parameters. 184 | 185 | **Returns:** 186 | 187 | URL to use. The default behavior is to use the URL from the service configuration. 188 | 189 | --- 190 | 191 | ### initServiceClient 192 | 193 | **Signature:** `initServiceClient(service : Service) : Object` 194 | 195 | **Description:** Creates a protocol-specific client object. This does not normally need to be implemented, except in the case of SOAP services. Example declaration: initServiceClient: function( svc:SOAPService ) { } 196 | 197 | **Parameters:** 198 | 199 | - `service`: the Service object. 200 | 201 | **Returns:** 202 | 203 | Client object 204 | 205 | **Throws:** 206 | 207 | - Exception 208 | 209 | --- 210 | 211 | ### mockCall 212 | 213 | **Signature:** `mockCall(service : Service, requestObj : Object) : Object` 214 | 215 | **Description:** Override this method to mock the remote portion of the service call. Other callbacks like createRequest and parseResponse are still called. 216 | 217 | **Parameters:** 218 | 219 | - `service`: Service being executed. 220 | - `requestObj`: Request object returned by createRequest(Service, Object...). 221 | 222 | **Returns:** 223 | 224 | Mock response, to be sent to parseResponse(Service, Object). 225 | 226 | **Throws:** 227 | 228 | - Exception 229 | 230 | --- 231 | 232 | ### mockFull 233 | 234 | **Signature:** `mockFull(service : Service, args : Object...) : Object` 235 | 236 | **Description:** Override this method to mock the entire service call, including the createRequest, execute, and parseResponse phases. 237 | 238 | **Parameters:** 239 | 240 | - `service`: Service being executed. 241 | - `args`: Arguments from the Service call method. 242 | 243 | **Returns:** 244 | 245 | Object to return in the service call's Result. 246 | 247 | **Throws:** 248 | 249 | - Exception 250 | 251 | --- 252 | 253 | ### parseResponse 254 | 255 | **Signature:** `parseResponse(service : Service, response : Object) : Object` 256 | 257 | **Description:** Creates a response object from a successful service call. This response object will be the output object of the call method's Result. 258 | 259 | **Parameters:** 260 | 261 | - `service`: Service being executed. 262 | - `response`: Service-specific response object. For example, the HTTPService service provides the underlying HTTPClient object that made the HTTP call. 263 | 264 | **Returns:** 265 | 266 | Object to return in the service call's Result. 267 | 268 | --- ``` -------------------------------------------------------------------------------- /docs/dw_order/BonusDiscountLineItem.md: -------------------------------------------------------------------------------- ```markdown 1 | ## Package: dw.order 2 | 3 | # Class BonusDiscountLineItem 4 | 5 | ## Inheritance Hierarchy 6 | 7 | - Object 8 | - dw.object.PersistentObject 9 | - dw.object.ExtensibleObject 10 | - dw.order.BonusDiscountLineItem 11 | 12 | ## Description 13 | 14 | Line item representing an applied BonusChoiceDiscount in a LineItemCtnr. This type of line item can only be created by the B2C Commerce promotions engine when applying a BonusChoiceDiscount. A BonusDiscountLineItem is basically a placeholder in the cart which entitles a customer to add one or more bonus products to his basket from a configured list of products. Merchants typically display this type of line item in the cart by showing the corresponding promotion callout message. They typically provide links to the bonus products that the customer can choose from. This line item can be removed from the cart but will be re-added each time the promotions engine re-applies discounts. Merchants may however add custom logic to show/hide this line item since it just a placeholder and not an actual product line item. The number of products that a customer is allowed to choose from is determined by getMaxBonusItems(). The collection of products the customer can choose from is determined by getBonusProducts(). When a customer chooses a bonus product in the storefront, it is necessary to use the AddBonusProductToBasket pipelet instead of the usual AddProductToBasket pipelet, in order to associate this BonusDiscountLineItem with the newly created bonus ProductLineItem. Alternatively, the API method LineItemCtnr.createBonusProductLineItem(BonusDiscountLineItem, Product, ProductOptionModel, Shipment) can be used instead. The system does proper validations in order to prevent incorrect or too many bonus products from being associated with this BonusDiscountLineItem. Once a customer has selected bonus products, the product line items representing the chosen bonus products can be retrieved with getBonusProductLineItems(). 15 | 16 | ## Properties 17 | 18 | ### bonusChoiceRuleBased 19 | 20 | **Type:** getBonusProducts() (Read Only) 21 | 22 | Returns whether the promotion that triggered the creation of this line item uses a rule to determine the list of 23 | bonus products. 24 | 25 | If the promotion is rule based, then a ProductSearchModel should be used to return the bonus products the 26 | customer may choose from, as the methods that return lists will return nothing. See getBonusProducts() 27 | 28 | ### bonusProductLineItems 29 | 30 | **Type:** List (Read Only) 31 | 32 | Get the product line items in the current LineItemCtnr representing the 33 | bonus products that the customer has selected for this discount. 34 | 35 | ### bonusProducts 36 | 37 | **Type:** List (Read Only) 38 | 39 | Get the list of bonus products which the customer is allowed to choose 40 | from for this discount. This list is configured by a merchant entering a 41 | list of SKUs for the discount. Products which do not exist in the system, 42 | or are offline, or are not assigned to a category in the site catalog are 43 | filtered out. Unavailable (i.e. out-of-stock) products are NOT filtered 44 | out. This allows merchants to display out-of-stock bonus products with 45 | appropriate messaging. 46 | 47 | If the promotion which triggered this discount does not exist, or this 48 | promotion is rule based, then this method returns an empty list. 49 | 50 | If the promotion is rule based, then this method will return an empty list. 51 | A ProductSearchModel should be used to return the bonus products the 52 | customer may choose from instead. See 53 | ProductSearchModel.PROMOTION_PRODUCT_TYPE_BONUS and 54 | ProductSearchModel.setPromotionID(String) 55 | 56 | If a returned product is a master product, the customer is entitled to 57 | choose from any variant. If the product is an option product, the 58 | customer is entitled to choose any value for each option. Since the 59 | promotions engine does not touch the value of the product option line 60 | items, it is the responsibility of custom code to set option prices. 61 | 62 | ### couponLineItem 63 | 64 | **Type:** CouponLineItem (Read Only) 65 | 66 | Get the coupon line item associated with this discount. 67 | 68 | ### maxBonusItems 69 | 70 | **Type:** Number (Read Only) 71 | 72 | Get the maximum number of bonus items that the customer is permitted to 73 | select for this bonus discount. 74 | 75 | If the promotion which triggered this discount does not exist, then this 76 | method returns 0. 77 | 78 | ### promotion 79 | 80 | **Type:** Promotion (Read Only) 81 | 82 | Get the promotion associated with this discount. 83 | 84 | ### promotionID 85 | 86 | **Type:** String (Read Only) 87 | 88 | Get the promotion ID associated with this discount. 89 | 90 | ## Constructor Summary 91 | 92 | ## Method Summary 93 | 94 | ### getBonusProductLineItems 95 | 96 | **Signature:** `getBonusProductLineItems() : List` 97 | 98 | Get the product line items in the current LineItemCtnr representing the bonus products that the customer has selected for this discount. 99 | 100 | ### getBonusProductPrice 101 | 102 | **Signature:** `getBonusProductPrice(product : Product) : Money` 103 | 104 | Get the effective price for the passed bonus product. 105 | 106 | ### getBonusProducts 107 | 108 | **Signature:** `getBonusProducts() : List` 109 | 110 | Get the list of bonus products which the customer is allowed to choose from for this discount. 111 | 112 | ### getCouponLineItem 113 | 114 | **Signature:** `getCouponLineItem() : CouponLineItem` 115 | 116 | Get the coupon line item associated with this discount. 117 | 118 | ### getMaxBonusItems 119 | 120 | **Signature:** `getMaxBonusItems() : Number` 121 | 122 | Get the maximum number of bonus items that the customer is permitted to select for this bonus discount. 123 | 124 | ### getPromotion 125 | 126 | **Signature:** `getPromotion() : Promotion` 127 | 128 | Get the promotion associated with this discount. 129 | 130 | ### getPromotionID 131 | 132 | **Signature:** `getPromotionID() : String` 133 | 134 | Get the promotion ID associated with this discount. 135 | 136 | ### isBonusChoiceRuleBased 137 | 138 | **Signature:** `isBonusChoiceRuleBased() : boolean` 139 | 140 | Returns whether the promotion that triggered the creation of this line item uses a rule to determine the list of bonus products. 141 | 142 | ## Method Detail 143 | 144 | ## Method Details 145 | 146 | ### getBonusProductLineItems 147 | 148 | **Signature:** `getBonusProductLineItems() : List` 149 | 150 | **Description:** Get the product line items in the current LineItemCtnr representing the bonus products that the customer has selected for this discount. 151 | 152 | **Returns:** 153 | 154 | The selected product line items, never null. 155 | 156 | **See Also:** 157 | 158 | LineItemCtnr.createBonusProductLineItem(BonusDiscountLineItem, Product, ProductOptionModel, Shipment) 159 | 160 | --- 161 | 162 | ### getBonusProductPrice 163 | 164 | **Signature:** `getBonusProductPrice(product : Product) : Money` 165 | 166 | **Description:** Get the effective price for the passed bonus product. This is expected to be one of the products returned by getBonusProducts() with one exception: If a master product is configured as a bonus product, this implies that a customer may choose from any of its variants. In this case, it is allowed to pass in a variant to this method and a price will be returned. If the passed product is not a valid bonus product, this method throws an exception. 167 | 168 | **Parameters:** 169 | 170 | - `product`: The bonus product to retrieve a price for, must not be null. 171 | 172 | **Returns:** 173 | 174 | The price of the passed bonus product as a Number. 175 | 176 | --- 177 | 178 | ### getBonusProducts 179 | 180 | **Signature:** `getBonusProducts() : List` 181 | 182 | **Description:** Get the list of bonus products which the customer is allowed to choose from for this discount. This list is configured by a merchant entering a list of SKUs for the discount. Products which do not exist in the system, or are offline, or are not assigned to a category in the site catalog are filtered out. Unavailable (i.e. out-of-stock) products are NOT filtered out. This allows merchants to display out-of-stock bonus products with appropriate messaging. If the promotion which triggered this discount does not exist, or this promotion is rule based, then this method returns an empty list. If the promotion is rule based, then this method will return an empty list. A ProductSearchModel should be used to return the bonus products the customer may choose from instead. See ProductSearchModel.PROMOTION_PRODUCT_TYPE_BONUS and ProductSearchModel.setPromotionID(String) If a returned product is a master product, the customer is entitled to choose from any variant. If the product is an option product, the customer is entitled to choose any value for each option. Since the promotions engine does not touch the value of the product option line items, it is the responsibility of custom code to set option prices. 183 | 184 | **Returns:** 185 | 186 | An ordered list of bonus products that the customer may choose from for this discount. 187 | 188 | --- 189 | 190 | ### getCouponLineItem 191 | 192 | **Signature:** `getCouponLineItem() : CouponLineItem` 193 | 194 | **Description:** Get the coupon line item associated with this discount. 195 | 196 | **Returns:** 197 | 198 | The coupon line item associated with this discount, or null if it no longer exists or there is no one. 199 | 200 | --- 201 | 202 | ### getMaxBonusItems 203 | 204 | **Signature:** `getMaxBonusItems() : Number` 205 | 206 | **Description:** Get the maximum number of bonus items that the customer is permitted to select for this bonus discount. If the promotion which triggered this discount does not exist, then this method returns 0. 207 | 208 | **Returns:** 209 | 210 | The maximum number of bonus items that the customer is permitted to select for this bonus discount, or 0 if the promotion no longer exists. 211 | 212 | --- 213 | 214 | ### getPromotion 215 | 216 | **Signature:** `getPromotion() : Promotion` 217 | 218 | **Description:** Get the promotion associated with this discount. 219 | 220 | **Returns:** 221 | 222 | The promotion associated with this discount, or null if it no longer exists. 223 | 224 | --- 225 | 226 | ### getPromotionID 227 | 228 | **Signature:** `getPromotionID() : String` 229 | 230 | **Description:** Get the promotion ID associated with this discount. 231 | 232 | **Returns:** 233 | 234 | The promotion ID associated with this discount, never null. 235 | 236 | --- 237 | 238 | ### isBonusChoiceRuleBased 239 | 240 | **Signature:** `isBonusChoiceRuleBased() : boolean` 241 | 242 | **Description:** Returns whether the promotion that triggered the creation of this line item uses a rule to determine the list of bonus products. If the promotion is rule based, then a ProductSearchModel should be used to return the bonus products the customer may choose from, as the methods that return lists will return nothing. See getBonusProducts() 243 | 244 | **Returns:** 245 | 246 | If the promotion no longer exists, then null, otherwise, true if the promotion that triggered the creation of this line item uses a rule to determine the bonus products to choose from. 247 | 248 | --- ``` -------------------------------------------------------------------------------- /tests/servers/sfcc-mock-server/mock-data/ocapi/system-object-definitions-old.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "_v": "24.4", 3 | "_type": "system_object_definitions", 4 | "count": 25, 5 | "data": [ 6 | { 7 | "_type": "system_object_definition", 8 | "object_type": "Product", 9 | "displayName": { 10 | "default": "Product" 11 | }, 12 | "description": { 13 | "default": "Product" 14 | }, 15 | "content_object": false, 16 | "queryable": true, 17 | "read_only": false, 18 | "attribute_group_count": 35, 19 | "attribute_definition_count": 113, 20 | "_links": { 21 | "self": { 22 | "href": "https://{{hostname}}/s/-/dw/data/v24_4/system_object_definitions/Product" 23 | } 24 | } 25 | }, 26 | { 27 | "_type": "system_object_definition", 28 | "object_type": "Category", 29 | "displayName": { 30 | "default": "Category" 31 | }, 32 | "description": { 33 | "default": "Category" 34 | }, 35 | "content_object": false, 36 | "queryable": true, 37 | "read_only": false, 38 | "attribute_group_count": 12, 39 | "attribute_definition_count": 45, 40 | "_links": { 41 | "self": { 42 | "href": "https://{{hostname}}/s/-/dw/data/v24_4/system_object_definitions/Category" 43 | } 44 | } 45 | }, 46 | { 47 | "_type": "system_object_definition", 48 | "object_type": "CustomerAddress", 49 | "displayName": { 50 | "default": "Customer Address" 51 | }, 52 | "description": { 53 | "default": "Customer Address" 54 | }, 55 | "content_object": false, 56 | "queryable": true, 57 | "read_only": false, 58 | "attribute_group_count": 8, 59 | "attribute_definition_count": 28, 60 | "_links": { 61 | "self": { 62 | "href": "https://{{hostname}}/s/-/dw/data/v24_4/system_object_definitions/CustomerAddress" 63 | } 64 | } 65 | }, 66 | { 67 | "_type": "system_object_definition", 68 | "object_type": "Customer", 69 | "displayName": { 70 | "default": "Customer" 71 | }, 72 | "description": { 73 | "default": "Customer" 74 | }, 75 | "content_object": false, 76 | "queryable": true, 77 | "read_only": false, 78 | "attribute_group_count": 15, 79 | "attribute_definition_count": 67, 80 | "_links": { 81 | "self": { 82 | "href": "https://{{hostname}}/s/-/dw/data/v24_4/system_object_definitions/Customer" 83 | } 84 | } 85 | }, 86 | { 87 | "_type": "system_object_definition", 88 | "object_type": "Order", 89 | "displayName": { 90 | "default": "Order" 91 | }, 92 | "description": { 93 | "default": "Order" 94 | }, 95 | "content_object": false, 96 | "queryable": true, 97 | "read_only": false, 98 | "attribute_group_count": 18, 99 | "attribute_definition_count": 89, 100 | "_links": { 101 | "self": { 102 | "href": "https://{{hostname}}/s/-/dw/data/v24_4/system_object_definitions/Order" 103 | } 104 | } 105 | }, 106 | { 107 | "_type": "system_object_definition", 108 | "object_type": "SitePreferences", 109 | "displayName": { 110 | "default": "Site Preferences" 111 | }, 112 | "description": { 113 | "default": "Site Preferences" 114 | }, 115 | "content_object": false, 116 | "queryable": false, 117 | "read_only": false, 118 | "attribute_group_count": 8, 119 | "attribute_definition_count": 156, 120 | "_links": { 121 | "self": { 122 | "href": "https://{{hostname}}/s/-/dw/data/v24_4/system_object_definitions/SitePreferences" 123 | } 124 | } 125 | } 126 | "key_attribute_id": "order_no", 127 | "content_object": false, 128 | "queryable": true, 129 | "read_only": false, 130 | "creation_date": "2021-01-01T00:00:00.000Z", 131 | "last_modified": "2024-01-01T00:00:00.000Z" 132 | }, 133 | { 134 | "_type": "object_type_definition", 135 | "object_type": "Category", 136 | "display_name": { 137 | "default": "Category" 138 | }, 139 | "description": { 140 | "default": "Salesforce B2C Commerce category object definition" 141 | }, 142 | "attribute_definition_count": 20, 143 | "attribute_group_count": 4, 144 | "key_attribute_id": "id", 145 | "content_object": false, 146 | "queryable": true, 147 | "read_only": false, 148 | "creation_date": "2021-01-01T00:00:00.000Z", 149 | "last_modified": "2024-01-01T00:00:00.000Z" 150 | }, 151 | { 152 | "_type": "object_type_definition", 153 | "object_type": "Site", 154 | "display_name": { 155 | "default": "Site" 156 | }, 157 | "description": { 158 | "default": "Salesforce B2C Commerce site object definition" 159 | }, 160 | "attribute_definition_count": 15, 161 | "attribute_group_count": 3, 162 | "key_attribute_id": "id", 163 | "content_object": false, 164 | "queryable": true, 165 | "read_only": false, 166 | "creation_date": "2021-01-01T00:00:00.000Z", 167 | "last_modified": "2024-01-01T00:00:00.000Z" 168 | }, 169 | { 170 | "_type": "object_type_definition", 171 | "object_type": "Campaign", 172 | "display_name": { 173 | "default": "Campaign" 174 | }, 175 | "description": { 176 | "default": "Salesforce B2C Commerce campaign object definition" 177 | }, 178 | "attribute_definition_count": 12, 179 | "attribute_group_count": 3, 180 | "key_attribute_id": "id", 181 | "content_object": false, 182 | "queryable": true, 183 | "read_only": false, 184 | "creation_date": "2021-01-01T00:00:00.000Z", 185 | "last_modified": "2024-01-01T00:00:00.000Z" 186 | }, 187 | { 188 | "_type": "object_type_definition", 189 | "object_type": "Coupon", 190 | "display_name": { 191 | "default": "Coupon" 192 | }, 193 | "description": { 194 | "default": "Salesforce B2C Commerce coupon object definition" 195 | }, 196 | "attribute_definition_count": 18, 197 | "attribute_group_count": 4, 198 | "key_attribute_id": "id", 199 | "content_object": false, 200 | "queryable": true, 201 | "read_only": false, 202 | "creation_date": "2021-01-01T00:00:00.000Z", 203 | "last_modified": "2024-01-01T00:00:00.000Z" 204 | }, 205 | { 206 | "_type": "object_type_definition", 207 | "object_type": "Promotion", 208 | "display_name": { 209 | "default": "Promotion" 210 | }, 211 | "description": { 212 | "default": "Salesforce B2C Commerce promotion object definition" 213 | }, 214 | "attribute_definition_count": 25, 215 | "attribute_group_count": 5, 216 | "key_attribute_id": "id", 217 | "content_object": false, 218 | "queryable": true, 219 | "read_only": false, 220 | "creation_date": "2021-01-01T00:00:00.000Z", 221 | "last_modified": "2024-01-01T00:00:00.000Z" 222 | }, 223 | { 224 | "_type": "object_type_definition", 225 | "object_type": "PriceBook", 226 | "display_name": { 227 | "default": "Price Book" 228 | }, 229 | "description": { 230 | "default": "Salesforce B2C Commerce price book object definition" 231 | }, 232 | "attribute_definition_count": 10, 233 | "attribute_group_count": 2, 234 | "key_attribute_id": "id", 235 | "content_object": false, 236 | "queryable": true, 237 | "read_only": false, 238 | "creation_date": "2021-01-01T00:00:00.000Z", 239 | "last_modified": "2024-01-01T00:00:00.000Z" 240 | }, 241 | { 242 | "_type": "object_type_definition", 243 | "object_type": "CustomObject", 244 | "display_name": { 245 | "default": "Custom Object" 246 | }, 247 | "description": { 248 | "default": "Custom business object definition" 249 | }, 250 | "attribute_definition_count": 8, 251 | "attribute_group_count": 2, 252 | "key_attribute_id": "key_property", 253 | "content_object": true, 254 | "queryable": true, 255 | "read_only": false, 256 | "creation_date": "2021-01-01T00:00:00.000Z", 257 | "last_modified": "2024-01-01T00:00:00.000Z" 258 | }, 259 | { 260 | "_type": "object_type_definition", 261 | "object_type": "Content", 262 | "display_name": { 263 | "default": "Content" 264 | }, 265 | "description": { 266 | "default": "Salesforce B2C Commerce content object definition" 267 | }, 268 | "attribute_definition_count": 22, 269 | "attribute_group_count": 4, 270 | "key_attribute_id": "id", 271 | "content_object": true, 272 | "queryable": true, 273 | "read_only": false, 274 | "creation_date": "2021-01-01T00:00:00.000Z", 275 | "last_modified": "2024-01-01T00:00:00.000Z" 276 | }, 277 | { 278 | "_type": "object_type_definition", 279 | "object_type": "ContentFolder", 280 | "display_name": { 281 | "default": "Content Folder" 282 | }, 283 | "description": { 284 | "default": "Salesforce B2C Commerce content folder object definition" 285 | }, 286 | "attribute_definition_count": 12, 287 | "attribute_group_count": 3, 288 | "key_attribute_id": "id", 289 | "content_object": true, 290 | "queryable": true, 291 | "read_only": false, 292 | "creation_date": "2021-01-01T00:00:00.000Z", 293 | "last_modified": "2024-01-01T00:00:00.000Z" 294 | }, 295 | { 296 | "_type": "object_type_definition", 297 | "object_type": "CustomerGroup", 298 | "display_name": { 299 | "default": "Customer Group" 300 | }, 301 | "description": { 302 | "default": "Salesforce B2C Commerce customer group object definition" 303 | }, 304 | "attribute_definition_count": 8, 305 | "attribute_group_count": 2, 306 | "key_attribute_id": "id", 307 | "content_object": false, 308 | "queryable": true, 309 | "read_only": false, 310 | "creation_date": "2021-01-01T00:00:00.000Z", 311 | "last_modified": "2024-01-01T00:00:00.000Z" 312 | }, 313 | { 314 | "_type": "object_type_definition", 315 | "object_type": "SourceCodeInfo", 316 | "display_name": { 317 | "default": "Source Code Info" 318 | }, 319 | "description": { 320 | "default": "Salesforce B2C Commerce source code information object definition" 321 | }, 322 | "attribute_definition_count": 6, 323 | "attribute_group_count": 1, 324 | "key_attribute_id": "id", 325 | "content_object": false, 326 | "queryable": true, 327 | "read_only": true, 328 | "creation_date": "2021-01-01T00:00:00.000Z", 329 | "last_modified": "2024-01-01T00:00:00.000Z" 330 | }, 331 | { 332 | "_type": "object_type_definition", 333 | "object_type": "GiftCertificate", 334 | "display_name": { 335 | "default": "Gift Certificate" 336 | }, 337 | "description": { 338 | "default": "Salesforce B2C Commerce gift certificate object definition" 339 | }, 340 | "attribute_definition_count": 14, 341 | "attribute_group_count": 3, 342 | "key_attribute_id": "gift_certificate_code", 343 | "content_object": false, 344 | "queryable": true, 345 | "read_only": false, 346 | "creation_date": "2021-01-01T00:00:00.000Z", 347 | "last_modified": "2024-01-01T00:00:00.000Z" 348 | } 349 | ], 350 | "next": null, 351 | "previous": null, 352 | "start": 0, 353 | "total": 25 354 | } ``` -------------------------------------------------------------------------------- /docs/dw_catalog/ProductSearchRefinements.md: -------------------------------------------------------------------------------- ```markdown 1 | ## Package: dw.catalog 2 | 3 | # Class ProductSearchRefinements 4 | 5 | ## Inheritance Hierarchy 6 | 7 | - Object 8 | - dw.catalog.SearchRefinements 9 | - dw.catalog.ProductSearchRefinements 10 | 11 | ## Description 12 | 13 | This class provides an interface to refinement options for the product search. In a typical usage, the client application UI displays the search refinements along with the search results and allows customers to "refine" the results (i.e. limit the results that are shown) by specifying additional product criteria, or "relax" (i.e. broaden) the results after previously refining. The four types of product search refinements are: Refine By Category: Limit the products to those assigned to specific child/ancestor categories of the search category. Refine By Attribute: Limit the products to those with specific values for a given attribute. Values may be grouped into "buckets" so that a given set of values are represented as a single refinement option. Refine By Price: Limit the products to those whose prices fall in a specific range. Refine By Promotion: Limit the products to those which are related to a specific promotion. Rendering a product search refinement UI typically begins with iterating the refinement definitions for the search result. Call SearchRefinements.getRefinementDefinitions() or SearchRefinements.getAllRefinementDefinitions() to retrieve the appropriate collection of refinement definitions. For each definition, display the available refinement values by calling getAllRefinementValues(ProductSearchRefinementDefinition). Depending on the type of the refinement definition, the application must use slightly different logic to display the refinement widgets. For all 4 types, methods in ProductSearchModel are used to generate URLs to render hyperlinks in the UI. When clicked, these links trigger a call to the Search pipelet which in turn applies the appropriate filters to the native search result. 14 | 15 | ## Properties 16 | 17 | ### categoryRefinementDefinition 18 | 19 | **Type:** ProductSearchRefinementDefinition (Read Only) 20 | 21 | The appropriate category refinement definition based on the search 22 | result. The category refinement definition returned will be the first that 23 | can be found traversing the category tree upward starting at the deepest 24 | common category of the search result. 25 | 26 | ### priceRefinementDefinition 27 | 28 | **Type:** ProductSearchRefinementDefinition (Read Only) 29 | 30 | The appropriate price refinement definition based on the search 31 | result. The price refinement definition returned will be the first that 32 | can be found traversing the category tree upward starting at the deepest 33 | common category of the search result. 34 | 35 | ### promotionRefinementDefinition 36 | 37 | **Type:** ProductSearchRefinementDefinition (Read Only) 38 | 39 | The appropriate promotion refinement definition based on the search 40 | result. The promotion refinement definition returned will be the first that 41 | can be found traversing the category tree upward starting at the deepest 42 | common category of the search result. 43 | 44 | ## Constructor Summary 45 | 46 | ## Method Summary 47 | 48 | ### getAllRefinementValues 49 | 50 | **Signature:** `getAllRefinementValues(definition : ProductSearchRefinementDefinition) : Collection` 51 | 52 | Returns a sorted collection of refinement values for the passed refinement definition. 53 | 54 | ### getCategoryRefinementDefinition 55 | 56 | **Signature:** `getCategoryRefinementDefinition() : ProductSearchRefinementDefinition` 57 | 58 | Returns the appropriate category refinement definition based on the search result. 59 | 60 | ### getNextLevelCategoryRefinementValues 61 | 62 | **Signature:** `getNextLevelCategoryRefinementValues(category : Category) : Collection` 63 | 64 | Returns category refinement values based on the current search result filtered such that only category refinements representing children of the given category are present. 65 | 66 | ### getPriceRefinementDefinition 67 | 68 | **Signature:** `getPriceRefinementDefinition() : ProductSearchRefinementDefinition` 69 | 70 | Returns the appropriate price refinement definition based on the search result. 71 | 72 | ### getPromotionRefinementDefinition 73 | 74 | **Signature:** `getPromotionRefinementDefinition() : ProductSearchRefinementDefinition` 75 | 76 | Returns the appropriate promotion refinement definition based on the search result. 77 | 78 | ### getRefinementValue 79 | 80 | **Signature:** `getRefinementValue(definition : ProductSearchRefinementDefinition, value : String) : ProductSearchRefinementValue` 81 | 82 | Returns the refinement value (incl. 83 | 84 | ### getRefinementValue 85 | 86 | **Signature:** `getRefinementValue(name : String, value : String) : ProductSearchRefinementValue` 87 | 88 | Returns the refinement value (incl. 89 | 90 | ### getRefinementValues 91 | 92 | **Signature:** `getRefinementValues(definition : ProductSearchRefinementDefinition) : Collection` 93 | 94 | Returns a collection of refinement values for the given refinement definition. 95 | 96 | ## Method Detail 97 | 98 | ## Method Details 99 | 100 | ### getAllRefinementValues 101 | 102 | **Signature:** `getAllRefinementValues(definition : ProductSearchRefinementDefinition) : Collection` 103 | 104 | **Description:** Returns a sorted collection of refinement values for the passed refinement definition. The returned collection includes all refinement values for which the hit count is greater than 0 within the search result when the passed refinement definition is excluded from filtering the search hits but all other refinement filters are still applied. This method is useful for rendering broadening options for definitions that the search is currently refined by. If the search is not currently restricted by the passed refinement definition, then this method will return the same result as getRefinementValues(ProductSearchRefinementDefinition). For attribute-based refinement definitions, the returned collection depends upon how the "value set" property is configured. (Category and price refinement definitions do not have such a property.) If this property is set to "search result values", the behavior is as described above. If this property is set to "all values of category", then the returned collection will also include all refinement values for products in the category subtree rooted at the search definition's category. This setting is useful for refinements whose visualization is supposed to remain constant for a certain subtree of a catalog (e.g. color pickers or size charts). These additional values are independent of the search result and do not contribute towards the value hit counts. If the search result is further refined by one of these values, it is possible to get an empty search result. Except for this one case this method does NOT return refinement values independent of the search result. 105 | 106 | **Parameters:** 107 | 108 | - `definition`: The refinement definition to return refinement values for. Must not be null. 109 | 110 | **Returns:** 111 | 112 | The collection of ProductSearchRefinementValue instances, sorted according to the settings of the refinement definition. 113 | 114 | --- 115 | 116 | ### getCategoryRefinementDefinition 117 | 118 | **Signature:** `getCategoryRefinementDefinition() : ProductSearchRefinementDefinition` 119 | 120 | **Description:** Returns the appropriate category refinement definition based on the search result. The category refinement definition returned will be the first that can be found traversing the category tree upward starting at the deepest common category of the search result. 121 | 122 | **Returns:** 123 | 124 | The category refinement definition or null if none can be found. 125 | 126 | --- 127 | 128 | ### getNextLevelCategoryRefinementValues 129 | 130 | **Signature:** `getNextLevelCategoryRefinementValues(category : Category) : Collection` 131 | 132 | **Description:** Returns category refinement values based on the current search result filtered such that only category refinements representing children of the given category are present. If no category is given, the method uses the catalog's root category. The refinement value product counts represent all hits contained in the catalog tree starting at the corresponding child category. 133 | 134 | **Parameters:** 135 | 136 | - `category`: The category to return child category refinement values for. 137 | 138 | **Returns:** 139 | 140 | The refinement values for all child categories of the given category. 141 | 142 | --- 143 | 144 | ### getPriceRefinementDefinition 145 | 146 | **Signature:** `getPriceRefinementDefinition() : ProductSearchRefinementDefinition` 147 | 148 | **Description:** Returns the appropriate price refinement definition based on the search result. The price refinement definition returned will be the first that can be found traversing the category tree upward starting at the deepest common category of the search result. 149 | 150 | **Returns:** 151 | 152 | The price refinement definition or null if none can be found. 153 | 154 | --- 155 | 156 | ### getPromotionRefinementDefinition 157 | 158 | **Signature:** `getPromotionRefinementDefinition() : ProductSearchRefinementDefinition` 159 | 160 | **Description:** Returns the appropriate promotion refinement definition based on the search result. The promotion refinement definition returned will be the first that can be found traversing the category tree upward starting at the deepest common category of the search result. 161 | 162 | **Returns:** 163 | 164 | The promotion refinement definition or null if none can be found. 165 | 166 | --- 167 | 168 | ### getRefinementValue 169 | 170 | **Signature:** `getRefinementValue(definition : ProductSearchRefinementDefinition, value : String) : ProductSearchRefinementValue` 171 | 172 | **Description:** Returns the refinement value (incl. product hit count) for the given refinement definition and the given (selected) value. 173 | 174 | **Parameters:** 175 | 176 | - `definition`: The definition to return the refinement for. 177 | - `value`: The value to return the refinement for. 178 | 179 | **Returns:** 180 | 181 | The refinement value. 182 | 183 | --- 184 | 185 | ### getRefinementValue 186 | 187 | **Signature:** `getRefinementValue(name : String, value : String) : ProductSearchRefinementValue` 188 | 189 | **Description:** Returns the refinement value (incl. product hit count) for the given refinement attribute and the given (selected) value. 190 | 191 | **Parameters:** 192 | 193 | - `name`: The name of the refinement attribute. 194 | - `value`: The value to return the refinement for. 195 | 196 | **Returns:** 197 | 198 | The refinement value. 199 | 200 | --- 201 | 202 | ### getRefinementValues 203 | 204 | **Signature:** `getRefinementValues(definition : ProductSearchRefinementDefinition) : Collection` 205 | 206 | **Description:** Returns a collection of refinement values for the given refinement definition. The returned refinement values only include those that are part of the actual search result (i.e. hit count will always be > 0). 207 | 208 | **Parameters:** 209 | 210 | - `definition`: The refinement definition to return refinement values for. 211 | 212 | **Returns:** 213 | 214 | The collection of refinement values sorted according to the settings of the definition. 215 | 216 | --- ``` -------------------------------------------------------------------------------- /tests/code-version-handler.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { CodeVersionToolHandler } from '../src/core/handlers/code-version-handler.js'; 2 | import { HandlerContext } from '../src/core/handlers/base-handler.js'; 3 | import { Logger } from '../src/utils/logger.js'; 4 | 5 | // Mock the OCAPICodeVersionsClient 6 | const mockCodeVersionsClient = { 7 | getCodeVersions: jest.fn(), 8 | activateCodeVersion: jest.fn(), 9 | }; 10 | 11 | jest.mock('../src/clients/ocapi/code-versions-client.js', () => ({ 12 | OCAPICodeVersionsClient: jest.fn(() => mockCodeVersionsClient), 13 | })); 14 | 15 | describe('CodeVersionToolHandler', () => { 16 | let mockLogger: jest.Mocked<Logger>; 17 | let mockClient: typeof mockCodeVersionsClient; 18 | let context: HandlerContext; 19 | let handler: CodeVersionToolHandler; 20 | 21 | beforeEach(() => { 22 | mockLogger = { 23 | debug: jest.fn(), 24 | log: jest.fn(), 25 | error: jest.fn(), 26 | timing: jest.fn(), 27 | methodEntry: jest.fn(), 28 | methodExit: jest.fn(), 29 | } as any; 30 | 31 | // Reset mocks 32 | jest.clearAllMocks(); 33 | 34 | // Use the mock client directly and reset it 35 | mockClient = mockCodeVersionsClient; 36 | mockClient.getCodeVersions.mockReset(); 37 | mockClient.activateCodeVersion.mockReset(); 38 | 39 | jest.spyOn(Logger, 'getChildLogger').mockReturnValue(mockLogger); 40 | 41 | context = { 42 | logger: mockLogger, 43 | config: { 44 | hostname: 'test.commercecloud.salesforce.com', 45 | clientId: 'test-client-id', 46 | clientSecret: 'test-client-secret', 47 | siteId: 'test-site', 48 | }, 49 | capabilities: { canAccessLogs: false, canAccessOCAPI: true }, 50 | }; 51 | 52 | handler = new CodeVersionToolHandler(context, 'CodeVersion'); 53 | }); 54 | 55 | afterEach(() => { 56 | jest.restoreAllMocks(); 57 | }); 58 | 59 | // Helper function to initialize handler for tests that need it 60 | const initializeHandler = async () => { 61 | await (handler as any).initialize(); 62 | }; 63 | 64 | describe('canHandle', () => { 65 | it('should handle code version tools', () => { 66 | expect(handler.canHandle('get_code_versions')).toBe(true); 67 | expect(handler.canHandle('activate_code_version')).toBe(true); 68 | }); 69 | 70 | it('should not handle non-code-version tools', () => { 71 | expect(handler.canHandle('get_latest_error')).toBe(false); 72 | expect(handler.canHandle('unknown_tool')).toBe(false); 73 | }); 74 | }); 75 | 76 | describe('initialization', () => { 77 | it('should initialize code versions client when OCAPI access is available', async () => { 78 | await initializeHandler(); 79 | 80 | const MockedConstructor = jest.requireMock('../src/clients/ocapi/code-versions-client.js').OCAPICodeVersionsClient; 81 | expect(MockedConstructor).toHaveBeenCalledWith({ 82 | hostname: 'test.commercecloud.salesforce.com', 83 | clientId: 'test-client-id', 84 | clientSecret: 'test-client-secret', 85 | version: 'v23_2', 86 | }); 87 | expect(mockLogger.debug).toHaveBeenCalledWith('Code versions client initialized'); 88 | }); 89 | 90 | it('should not initialize client when OCAPI access is not available', async () => { 91 | context.capabilities = { canAccessLogs: false, canAccessOCAPI: false }; 92 | const handlerWithoutOCAPI = new CodeVersionToolHandler(context, 'CodeVersion'); 93 | 94 | await (handlerWithoutOCAPI as any).initialize(); 95 | 96 | const MockedConstructor = jest.requireMock('../src/clients/ocapi/code-versions-client.js').OCAPICodeVersionsClient; 97 | expect(MockedConstructor).not.toHaveBeenCalled(); 98 | }); 99 | 100 | it('should not initialize client when config is missing', async () => { 101 | context.config = null as any; 102 | const handlerWithoutConfig = new CodeVersionToolHandler(context, 'CodeVersion'); 103 | 104 | await (handlerWithoutConfig as any).initialize(); 105 | 106 | const MockedConstructor = jest.requireMock('../src/clients/ocapi/code-versions-client.js').OCAPICodeVersionsClient; 107 | expect(MockedConstructor).not.toHaveBeenCalled(); 108 | }); 109 | }); 110 | 111 | describe('disposal', () => { 112 | it('should dispose code versions client properly', async () => { 113 | await initializeHandler(); 114 | await (handler as any).dispose(); 115 | 116 | expect(mockLogger.debug).toHaveBeenCalledWith('Code versions client disposed'); 117 | }); 118 | }); 119 | 120 | describe('get_code_versions tool', () => { 121 | beforeEach(async () => { 122 | await initializeHandler(); 123 | mockClient.getCodeVersions.mockResolvedValue([ 124 | { id: 'version_1', active: true, lastModified: '2024-01-01T00:00:00Z' }, 125 | { id: 'version_2', active: false, lastModified: '2024-01-02T00:00:00Z' }, 126 | ]); 127 | }); 128 | 129 | it('should handle get_code_versions', async () => { 130 | const result = await handler.handle('get_code_versions', {}, Date.now()); 131 | 132 | expect(mockClient.getCodeVersions).toHaveBeenCalled(); 133 | expect(result.content[0].text).toContain('version_1'); 134 | expect(result.content[0].text).toContain('version_2'); 135 | }); 136 | }); 137 | 138 | describe('activate_code_version tool', () => { 139 | beforeEach(async () => { 140 | await initializeHandler(); 141 | mockClient.activateCodeVersion.mockResolvedValue({ 142 | success: true, 143 | message: 'Code version activated successfully', 144 | codeVersionId: 'version_2', 145 | }); 146 | }); 147 | 148 | it('should handle activate_code_version with codeVersionId', async () => { 149 | const args = { codeVersionId: 'version_2' }; 150 | const result = await handler.handle('activate_code_version', args, Date.now()); 151 | 152 | expect(mockClient.activateCodeVersion).toHaveBeenCalledWith('version_2'); 153 | expect(result.content[0].text).toContain('activated successfully'); 154 | }); 155 | 156 | it('should throw error when codeVersionId is missing', async () => { 157 | const result = await handler.handle('activate_code_version', {}, Date.now()); 158 | expect(result.isError).toBe(true); 159 | expect(result.content[0].text).toContain('codeVersionId must be a non-empty string'); 160 | }); 161 | 162 | it('should throw error when codeVersionId is empty', async () => { 163 | const result = await handler.handle('activate_code_version', { codeVersionId: '' }, Date.now()); 164 | expect(result.isError).toBe(true); 165 | expect(result.content[0].text).toContain('codeVersionId must be a non-empty string'); 166 | }); 167 | }); 168 | 169 | describe('error handling', () => { 170 | beforeEach(async () => { 171 | await initializeHandler(); 172 | }); 173 | 174 | it('should handle client errors gracefully', async () => { 175 | mockClient.getCodeVersions.mockRejectedValue(new Error('OCAPI connection failed')); 176 | 177 | const result = await handler.handle('get_code_versions', {}, Date.now()); 178 | expect(result.isError).toBe(true); 179 | expect(result.content[0].text).toContain('OCAPI connection failed'); 180 | }); 181 | 182 | it('should throw error for unsupported tools', async () => { 183 | await expect(handler.handle('unsupported_tool', {}, Date.now())) 184 | .rejects.toThrow('Unsupported tool'); 185 | }); 186 | 187 | it('should handle OCAPI client not configured error', async () => { 188 | // Create handler without proper configuration 189 | const contextWithoutOCAPI = { 190 | ...context, 191 | capabilities: { canAccessLogs: false, canAccessOCAPI: false }, 192 | }; 193 | const handlerWithoutOCAPI = new CodeVersionToolHandler(contextWithoutOCAPI, 'CodeVersion'); 194 | 195 | const result = await handlerWithoutOCAPI.handle('get_code_versions', {}, Date.now()); 196 | expect(result.isError).toBe(true); 197 | expect(result.content[0].text).toContain('OCAPI client not configured'); 198 | }); 199 | }); 200 | 201 | describe('timing and logging', () => { 202 | beforeEach(async () => { 203 | await initializeHandler(); 204 | mockClient.getCodeVersions.mockResolvedValue([]); 205 | }); 206 | 207 | it('should log timing information', async () => { 208 | const startTime = Date.now(); 209 | await handler.handle('get_code_versions', {}, startTime); 210 | 211 | expect(mockLogger.timing).toHaveBeenCalledWith('get_code_versions', startTime); 212 | }); 213 | 214 | it('should log execution details', async () => { 215 | await handler.handle('get_code_versions', {}, Date.now()); 216 | 217 | expect(mockLogger.debug).toHaveBeenCalledWith( 218 | 'get_code_versions completed successfully', 219 | expect.any(Object), 220 | ); 221 | }); 222 | }); 223 | 224 | describe('client integration', () => { 225 | beforeEach(async () => { 226 | await initializeHandler(); 227 | }); 228 | 229 | it('should call getCodeVersions correctly', async () => { 230 | mockClient.getCodeVersions.mockResolvedValue([ 231 | { id: 'test_version', active: false }, 232 | ]); 233 | 234 | await handler.handle('get_code_versions', {}, Date.now()); 235 | 236 | expect(mockClient.getCodeVersions).toHaveBeenCalledWith(); 237 | }); 238 | 239 | it('should call activateCodeVersion correctly', async () => { 240 | mockClient.activateCodeVersion.mockResolvedValue({ 241 | success: true, 242 | codeVersionId: 'test_version', 243 | }); 244 | 245 | await handler.handle('activate_code_version', { codeVersionId: 'test_version' }, Date.now()); 246 | 247 | expect(mockClient.activateCodeVersion).toHaveBeenCalledWith('test_version'); 248 | }); 249 | }); 250 | 251 | describe('configuration variations', () => { 252 | it('should not initialize without hostname', async () => { 253 | const contextWithoutHostname = { 254 | ...context, 255 | config: { ...context.config, hostname: undefined }, 256 | }; 257 | const handlerWithoutHostname = new CodeVersionToolHandler(contextWithoutHostname, 'CodeVersion'); 258 | 259 | await (handlerWithoutHostname as any).initialize(); 260 | 261 | const MockedConstructor = jest.requireMock('../src/clients/ocapi/code-versions-client.js').OCAPICodeVersionsClient; 262 | expect(MockedConstructor).not.toHaveBeenCalled(); 263 | }); 264 | 265 | it('should not initialize without clientId', async () => { 266 | const contextWithoutClientId = { 267 | ...context, 268 | config: { ...context.config, clientId: undefined }, 269 | }; 270 | const handlerWithoutClientId = new CodeVersionToolHandler(contextWithoutClientId, 'CodeVersion'); 271 | 272 | await (handlerWithoutClientId as any).initialize(); 273 | 274 | const MockedConstructor = jest.requireMock('../src/clients/ocapi/code-versions-client.js').OCAPICodeVersionsClient; 275 | expect(MockedConstructor).not.toHaveBeenCalled(); 276 | }); 277 | 278 | it('should not initialize without clientSecret', async () => { 279 | const contextWithoutClientSecret = { 280 | ...context, 281 | config: { ...context.config, clientSecret: undefined }, 282 | }; 283 | const handlerWithoutClientSecret = new CodeVersionToolHandler(contextWithoutClientSecret, 'CodeVersion'); 284 | 285 | await (handlerWithoutClientSecret as any).initialize(); 286 | 287 | const MockedConstructor = jest.requireMock('../src/clients/ocapi/code-versions-client.js').OCAPICodeVersionsClient; 288 | expect(MockedConstructor).not.toHaveBeenCalled(); 289 | }); 290 | }); 291 | }); 292 | ``` -------------------------------------------------------------------------------- /tests/log-processor.test.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { LogProcessor } from '../src/clients/logs/log-processor'; 2 | import { LogEntry, LogFileMetadata, LogLevel } from '../src/clients/logs/log-types'; 3 | import { Logger } from '../src/utils/logger'; 4 | 5 | // Mock utils functions 6 | jest.mock('../src/utils/utils', () => ({ 7 | parseLogEntries: jest.fn(), 8 | extractUniqueErrors: jest.fn(), 9 | normalizeFilePath: jest.fn((path: string) => path), 10 | extractTimestampFromLogEntry: jest.fn(), 11 | })); 12 | 13 | import { parseLogEntries, extractTimestampFromLogEntry } from '../src/utils/utils'; 14 | const mockParseLogEntries = parseLogEntries as jest.MockedFunction<typeof parseLogEntries>; 15 | const mockExtractTimestamp = extractTimestampFromLogEntry as jest.MockedFunction< 16 | typeof extractTimestampFromLogEntry 17 | >; 18 | 19 | describe('LogProcessor', () => { 20 | let logProcessor: LogProcessor; 21 | let mockLogger: Logger; 22 | 23 | beforeEach(() => { 24 | mockLogger = { 25 | methodEntry: jest.fn(), 26 | methodExit: jest.fn(), 27 | debug: jest.fn(), 28 | warn: jest.fn(), 29 | error: jest.fn(), 30 | timing: jest.fn(), 31 | log: jest.fn(), 32 | info: jest.fn(), 33 | createChildLogger: jest.fn(), 34 | setDebugEnabled: jest.fn(), 35 | getLogDirectory: jest.fn(), 36 | } as unknown as Logger; 37 | 38 | logProcessor = new LogProcessor(mockLogger); 39 | jest.clearAllMocks(); 40 | }); 41 | 42 | describe('processLogFiles', () => { 43 | it('should process files in chronological order by modification date (newest first)', async () => { 44 | const files: LogFileMetadata[] = [ 45 | { filename: 'error-server1-20250819.log', lastmod: '2025-08-19T08:00:00.000Z' }, // oldest 46 | { filename: 'error-server2-20250819.log', lastmod: '2025-08-19T10:00:00.000Z' }, // newest 47 | { filename: 'error-server3-20250819.log', lastmod: '2025-08-19T09:00:00.000Z' }, // middle 48 | ]; 49 | 50 | const fileContents = new Map([ 51 | ['error-server1-20250819.log', '[2025-08-19T08:30:00.000 GMT] ERROR Class1 - Old error'], 52 | ['error-server2-20250819.log', '[2025-08-19T10:30:00.000 GMT] ERROR Class2 - New error'], 53 | ['error-server3-20250819.log', '[2025-08-19T09:30:00.000 GMT] ERROR Class3 - Middle error'], 54 | ]); 55 | 56 | // Mock parseLogEntries to return the messages as they appear in content 57 | mockParseLogEntries 58 | .mockReturnValueOnce(['[2025-08-19T10:30:00.000 GMT] ERROR Class2 - New error']) // newest file processed first 59 | .mockReturnValueOnce(['[2025-08-19T09:30:00.000 GMT] ERROR Class3 - Middle error']) // middle file 60 | .mockReturnValueOnce(['[2025-08-19T08:30:00.000 GMT] ERROR Class1 - Old error']); // oldest file processed last 61 | 62 | // Mock extractTimestampFromLogEntry to return appropriate timestamps 63 | mockExtractTimestamp 64 | .mockReturnValueOnce(new Date('2025-08-19T10:30:00.000Z')) // newest error 65 | .mockReturnValueOnce(new Date('2025-08-19T09:30:00.000Z')) // middle error 66 | .mockReturnValueOnce(new Date('2025-08-19T08:30:00.000Z')); // oldest error 67 | 68 | const result = await logProcessor.processLogFiles(files, 'error' as LogLevel, fileContents); 69 | 70 | // Verify files were processed in chronological order by modification date (newest first) 71 | expect(mockParseLogEntries).toHaveBeenNthCalledWith(1, fileContents.get('error-server2-20250819.log'), 'ERROR'); 72 | expect(mockParseLogEntries).toHaveBeenNthCalledWith(2, fileContents.get('error-server3-20250819.log'), 'ERROR'); 73 | expect(mockParseLogEntries).toHaveBeenNthCalledWith(3, fileContents.get('error-server1-20250819.log'), 'ERROR'); 74 | 75 | // Verify entries have timestamps extracted and order values assigned 76 | expect(result).toHaveLength(3); 77 | expect(result[0].timestamp).toEqual(new Date('2025-08-19T10:30:00.000Z')); // newest timestamp 78 | expect(result[0].order).toBe(0); // newest file, first entry: 0 * 1000000 + 0 = 0 79 | expect(result[1].timestamp).toEqual(new Date('2025-08-19T09:30:00.000Z')); // middle timestamp 80 | expect(result[1].order).toBe(1000000); // middle file, first entry: 1 * 1000000 + 0 = 1000000 81 | expect(result[2].timestamp).toEqual(new Date('2025-08-19T08:30:00.000Z')); // oldest timestamp 82 | expect(result[2].order).toBe(2000000); // oldest file, first entry: 2 * 1000000 + 0 = 2000000 83 | }); 84 | 85 | it('should assign correct order values for multiple entries within files', async () => { 86 | const files: LogFileMetadata[] = [ 87 | { filename: 'error-new-20250819.log', lastmod: '2025-08-19T10:00:00.000Z' }, // newer 88 | { filename: 'error-old-20250819.log', lastmod: '2025-08-19T08:00:00.000Z' }, // older 89 | ]; 90 | 91 | const fileContents = new Map([ 92 | ['error-new-20250819.log', 'content1'], 93 | ['error-old-20250819.log', 'content2'], 94 | ]); 95 | 96 | // Mock parseLogEntries to return multiple entries per file 97 | mockParseLogEntries 98 | .mockReturnValueOnce([ // newer file (processed first, order starts at 0) 99 | '[2025-08-19T10:30:00.000 GMT] ERROR Class1 - First new error', 100 | '[2025-08-19T10:25:00.000 GMT] ERROR Class2 - Second new error', 101 | ]) 102 | .mockReturnValueOnce([ // older file (processed second, order starts at 1000) 103 | '[2025-08-19T08:30:00.000 GMT] ERROR Class3 - First old error', 104 | '[2025-08-19T08:25:00.000 GMT] ERROR Class4 - Second old error', 105 | ]); 106 | 107 | // Mock extractTimestampFromLogEntry for all entries 108 | mockExtractTimestamp 109 | .mockReturnValueOnce(new Date('2025-08-19T10:30:00.000Z')) // First new error 110 | .mockReturnValueOnce(new Date('2025-08-19T10:25:00.000Z')) // Second new error 111 | .mockReturnValueOnce(new Date('2025-08-19T08:30:00.000Z')) // First old error 112 | .mockReturnValueOnce(new Date('2025-08-19T08:25:00.000Z')); // Second old error 113 | 114 | const result = await logProcessor.processLogFiles(files, 'error' as LogLevel, fileContents); 115 | 116 | expect(result).toHaveLength(4); 117 | 118 | // Verify order values and timestamps for all entries 119 | expect(result[0].order).toBe(0); // 0 * 1000000 + 0 = 0 120 | expect(result[0].timestamp).toEqual(new Date('2025-08-19T10:30:00.000Z')); 121 | expect(result[1].order).toBe(1); // 0 * 1000000 + 1 = 1 122 | expect(result[1].timestamp).toEqual(new Date('2025-08-19T10:25:00.000Z')); 123 | 124 | expect(result[2].order).toBe(1000000); // 1 * 1000000 + 0 = 1000000 125 | expect(result[2].timestamp).toEqual(new Date('2025-08-19T08:30:00.000Z')); 126 | expect(result[3].order).toBe(1000001); // 1 * 1000000 + 1 = 1000001 127 | expect(result[3].timestamp).toEqual(new Date('2025-08-19T08:25:00.000Z')); 128 | }); 129 | }); 130 | 131 | describe('sortAndLimitEntries', () => { 132 | it('should return the most recent entries by timestamp when limit is applied', () => { 133 | const entries: LogEntry[] = [ 134 | { 135 | entry: '[file1] Error 1', 136 | filename: 'file1.log', 137 | order: 0, 138 | timestamp: new Date('2025-08-19T08:00:00.000Z'), // oldest 139 | }, 140 | { 141 | entry: '[file1] Error 2', 142 | filename: 'file1.log', 143 | order: 1, 144 | timestamp: new Date('2025-08-19T10:00:00.000Z'), // newest 145 | }, 146 | { 147 | entry: '[file2] Error 3', 148 | filename: 'file2.log', 149 | order: 1000, 150 | timestamp: new Date('2025-08-19T09:00:00.000Z'), // middle 151 | }, 152 | ]; 153 | 154 | const result = logProcessor.sortAndLimitEntries(entries, 2); 155 | 156 | // Should take the 2 most recent entries chronologically (newest first) 157 | expect(result).toHaveLength(2); 158 | expect(result[0].entry).toBe('[file1] Error 2'); // 10:00 - newest timestamp 159 | expect(result[1].entry).toBe('[file2] Error 3'); // 09:00 - second newest 160 | }); 161 | 162 | it('should handle limit greater than available entries', () => { 163 | const entries: LogEntry[] = [ 164 | { 165 | entry: '[file1] Error 1', 166 | filename: 'file1.log', 167 | order: 0, 168 | timestamp: new Date('2025-08-19T08:00:00.000Z'), 169 | }, 170 | { 171 | entry: '[file1] Error 2', 172 | filename: 'file1.log', 173 | order: 1, 174 | timestamp: new Date('2025-08-19T09:00:00.000Z'), 175 | }, 176 | ]; 177 | 178 | const result = logProcessor.sortAndLimitEntries(entries, 10); 179 | 180 | expect(result).toHaveLength(2); 181 | expect(result[0].entry).toBe('[file1] Error 2'); // newest first 182 | expect(result[1].entry).toBe('[file1] Error 1'); 183 | }); 184 | 185 | it('should handle empty entries array', () => { 186 | const result = logProcessor.sortAndLimitEntries([], 5); 187 | expect(result).toHaveLength(0); 188 | }); 189 | 190 | it('should fallback to order-based sorting when timestamps are missing', () => { 191 | const entries: LogEntry[] = [ 192 | { entry: '[file1] Error A', filename: 'file1.log', order: 2 }, // no timestamp 193 | { entry: '[file1] Error B', filename: 'file1.log', order: 0 }, // no timestamp 194 | { entry: '[file1] Error C', filename: 'file1.log', order: 1 }, // no timestamp 195 | ]; 196 | 197 | const result = logProcessor.sortAndLimitEntries(entries, 3); 198 | 199 | expect(result).toHaveLength(3); 200 | // Should sort by order (ascending) when no timestamps 201 | expect(result[0].entry).toBe('[file1] Error B'); // order: 0 202 | expect(result[1].entry).toBe('[file1] Error C'); // order: 1 203 | expect(result[2].entry).toBe('[file1] Error A'); // order: 2 204 | }); 205 | 206 | it('should prioritize entries with timestamps over those without', () => { 207 | const entries: LogEntry[] = [ 208 | { entry: '[file1] Error A', filename: 'file1.log', order: 0 }, // no timestamp 209 | { 210 | entry: '[file1] Error B', 211 | filename: 'file1.log', 212 | order: 1000, 213 | timestamp: new Date('2025-08-19T09:00:00.000Z'), // has timestamp 214 | }, 215 | ]; 216 | 217 | const result = logProcessor.sortAndLimitEntries(entries, 2); 218 | 219 | expect(result).toHaveLength(2); 220 | // Entry with timestamp should come first 221 | expect(result[0].entry).toBe('[file1] Error B'); 222 | expect(result[1].entry).toBe('[file1] Error A'); 223 | }); 224 | }); 225 | 226 | describe('extractFormattedEntries', () => { 227 | it('should extract entries from sorted log entries', () => { 228 | const sortedEntries: LogEntry[] = [ 229 | { entry: '[file1] Error 1', filename: 'file1.log', order: 0 }, 230 | { entry: '[file1] Error 2', filename: 'file1.log', order: 1 }, 231 | ]; 232 | 233 | const result = logProcessor.extractFormattedEntries(sortedEntries); 234 | 235 | expect(result).toEqual(['[file1] Error 1', '[file1] Error 2']); 236 | }); 237 | 238 | it('should handle empty array', () => { 239 | const result = logProcessor.extractFormattedEntries([]); 240 | expect(result).toEqual([]); 241 | }); 242 | }); 243 | }); 244 | ``` -------------------------------------------------------------------------------- /tests/mcp/yaml/activate-code-version.docs-only.test.mcp.yml: -------------------------------------------------------------------------------- ```yaml 1 | # ================================================================================== 2 | # SFCC MCP Server - activate_code_version Tool YAML Tests (Docs-Only Mode) 3 | # Validates that code version activation tools are NOT available in docs-only mode 4 | # This tool requires SFCC credentials and should not be available without them 5 | # 6 | # Test Coverage: 7 | # - Tool unavailability in docs-only mode (not listed in tools/list) 8 | # - Authentication error responses when tool is called directly 9 | # - Consistent error messaging across different parameter scenarios 10 | # - Error structure validation (proper MCP response format) 11 | # - Performance requirements (fast failure under 1000ms) 12 | # - Mode verification (ensuring we're actually in docs-only mode) 13 | # - Authentication error precedence over parameter validation 14 | # 15 | # Quick Test Commands: 16 | # aegis "tests/mcp/yaml/activate-code-version.docs-only.test.mcp.yml" --config "aegis.config.docs-only.json" --verbose 17 | # aegis query activate_code_version 'codeVersionId:test-version-001' --config "aegis.config.docs-only.json" 18 | # ================================================================================== 19 | 20 | description: "activate_code_version tool docs-only mode tests - Tool unavailability validation" 21 | 22 | tests: 23 | # ================================================================================== 24 | # TOOL UNAVAILABILITY IN DOCS-ONLY MODE 25 | # ================================================================================== 26 | - it: "should NOT list activate_code_version tool in docs-only mode" 27 | request: 28 | jsonrpc: "2.0" 29 | id: "tool-not-available-docs" 30 | method: "tools/list" 31 | params: {} 32 | expect: 33 | response: 34 | jsonrpc: "2.0" 35 | id: "tool-not-available-docs" 36 | result: 37 | tools: 38 | match:arrayElements: 39 | match:partial: 40 | name: "match:type:string" 41 | description: "match:type:string" 42 | match:extractField: "tools.*.name" 43 | value: "match:not:arrayContains:activate_code_version" 44 | stderr: "toBeEmpty" 45 | 46 | - it: "should exclude activate_code_version from available tools list" 47 | request: 48 | jsonrpc: "2.0" 49 | id: "excluded-from-list" 50 | method: "tools/list" 51 | params: {} 52 | expect: 53 | response: 54 | jsonrpc: "2.0" 55 | id: "excluded-from-list" 56 | result: 57 | tools: "match:not:arrayContains:name:activate_code_version" 58 | stderr: "toBeEmpty" 59 | 60 | # ================================================================================== 61 | # AUTHENTICATION ERROR TESTS (Tool Can Be Called But Returns Error) 62 | # ================================================================================== 63 | - it: "should return authentication error when calling activate_code_version in docs-only mode" 64 | request: 65 | jsonrpc: "2.0" 66 | id: "auth-error-default" 67 | method: "tools/call" 68 | params: 69 | name: "activate_code_version" 70 | arguments: 71 | codeVersionId: "test-version-001" 72 | expect: 73 | response: 74 | jsonrpc: "2.0" 75 | id: "auth-error-default" 76 | result: 77 | content: 78 | match:arrayElements: 79 | type: "text" 80 | text: "match:contains:OCAPI client not configured" 81 | isError: true 82 | stderr: "toBeEmpty" 83 | performance: 84 | maxResponseTime: "1000ms" 85 | 86 | - it: "should return consistent error message for authentication failure" 87 | request: 88 | jsonrpc: "2.0" 89 | id: "auth-error-consistent" 90 | method: "tools/call" 91 | params: 92 | name: "activate_code_version" 93 | arguments: 94 | codeVersionId: "any-version-id" 95 | expect: 96 | response: 97 | jsonrpc: "2.0" 98 | id: "auth-error-consistent" 99 | result: 100 | content: 101 | match:arrayElements: 102 | type: "text" 103 | text: "match:regex:Error[\\s\\S]*OCAPI[\\s\\S]*not configured[\\s\\S]*" 104 | isError: true 105 | stderr: "toBeEmpty" 106 | performance: 107 | maxResponseTime: "1000ms" 108 | 109 | - it: "should return authentication error regardless of code version ID validity" 110 | request: 111 | jsonrpc: "2.0" 112 | id: "auth-error-regardless" 113 | method: "tools/call" 114 | params: 115 | name: "activate_code_version" 116 | arguments: 117 | codeVersionId: "invalid@#$%^version" 118 | expect: 119 | response: 120 | jsonrpc: "2.0" 121 | id: "auth-error-regardless" 122 | result: 123 | content: 124 | match:arrayElements: 125 | type: "text" 126 | text: "match:contains:OCAPI client not configured" 127 | isError: true 128 | stderr: "toBeEmpty" 129 | performance: 130 | maxResponseTime: "1000ms" 131 | 132 | # ================================================================================== 133 | # ERROR RESPONSE VALIDATION 134 | # ================================================================================== 135 | - it: "should return proper error structure in docs-only mode" 136 | request: 137 | jsonrpc: "2.0" 138 | id: "error-structure" 139 | method: "tools/call" 140 | params: 141 | name: "activate_code_version" 142 | arguments: 143 | codeVersionId: "test-version" 144 | expect: 145 | response: 146 | jsonrpc: "2.0" 147 | id: "error-structure" 148 | result: 149 | content: 150 | match:arrayElements: 151 | match:partial: 152 | type: "text" 153 | text: "match:type:string" 154 | isError: true 155 | stderr: "toBeEmpty" 156 | 157 | - it: "should include helpful guidance in error message" 158 | request: 159 | jsonrpc: "2.0" 160 | id: "helpful-error" 161 | method: "tools/call" 162 | params: 163 | name: "activate_code_version" 164 | arguments: 165 | codeVersionId: "test-guidance" 166 | expect: 167 | response: 168 | jsonrpc: "2.0" 169 | id: "helpful-error" 170 | result: 171 | content: 172 | match:arrayElements: 173 | type: "text" 174 | text: "match:regex:Error[\\s\\S]*credentials[\\s\\S]*full mode[\\s\\S]*" 175 | isError: true 176 | stderr: "toBeEmpty" 177 | 178 | # ================================================================================== 179 | # PARAMETER VALIDATION IN DOCS-ONLY MODE 180 | # ================================================================================== 181 | - it: "should return authentication error even with missing parameters" 182 | request: 183 | jsonrpc: "2.0" 184 | id: "auth-error-missing-params" 185 | method: "tools/call" 186 | params: 187 | name: "activate_code_version" 188 | arguments: {} 189 | expect: 190 | response: 191 | jsonrpc: "2.0" 192 | id: "auth-error-missing-params" 193 | result: 194 | content: 195 | match:arrayElements: 196 | type: "text" 197 | text: "match:regex:(OCAPI client not configured|required.*codeVersionId)" 198 | isError: true 199 | stderr: "toBeEmpty" 200 | performance: 201 | maxResponseTime: "1000ms" 202 | 203 | - it: "should return authentication error even with empty parameters" 204 | request: 205 | jsonrpc: "2.0" 206 | id: "auth-error-empty-params" 207 | method: "tools/call" 208 | params: 209 | name: "activate_code_version" 210 | arguments: 211 | codeVersionId: "" 212 | expect: 213 | response: 214 | jsonrpc: "2.0" 215 | id: "auth-error-empty-params" 216 | result: 217 | content: 218 | match:arrayElements: 219 | type: "text" 220 | text: "match:regex:(OCAPI client not configured|required.*codeVersionId|invalid.*empty)" 221 | isError: true 222 | stderr: "toBeEmpty" 223 | performance: 224 | maxResponseTime: "1000ms" 225 | 226 | # ================================================================================== 227 | # PERFORMANCE TESTS FOR DOCS-ONLY MODE 228 | # ================================================================================== 229 | - it: "should fail fast in docs-only mode" 230 | request: 231 | jsonrpc: "2.0" 232 | id: "fail-fast" 233 | method: "tools/call" 234 | params: 235 | name: "activate_code_version" 236 | arguments: 237 | codeVersionId: "performance-test" 238 | expect: 239 | response: 240 | jsonrpc: "2.0" 241 | id: "fail-fast" 242 | result: 243 | content: 244 | match:arrayElements: 245 | type: "text" 246 | text: "match:contains:Error" 247 | isError: true 248 | stderr: "toBeEmpty" 249 | performance: 250 | maxResponseTime: "500ms" 251 | 252 | # ================================================================================== 253 | # CONSISTENCY TESTS ACROSS DIFFERENT INPUTS 254 | # ================================================================================== 255 | - it: "should return consistent auth errors for different valid-looking IDs" 256 | request: 257 | jsonrpc: "2.0" 258 | id: "consistent-auth-1" 259 | method: "tools/call" 260 | params: 261 | name: "activate_code_version" 262 | arguments: 263 | codeVersionId: "version-001" 264 | expect: 265 | response: 266 | jsonrpc: "2.0" 267 | id: "consistent-auth-1" 268 | result: 269 | content: 270 | match:arrayElements: 271 | type: "text" 272 | text: "match:regex:(OCAPI client not configured|required.*codeVersionId)" 273 | isError: true 274 | stderr: "toBeEmpty" 275 | 276 | - it: "should return consistent auth errors for different format IDs" 277 | request: 278 | jsonrpc: "2.0" 279 | id: "consistent-auth-2" 280 | method: "tools/call" 281 | params: 282 | name: "activate_code_version" 283 | arguments: 284 | codeVersionId: "v1.2.3-release" 285 | expect: 286 | response: 287 | jsonrpc: "2.0" 288 | id: "consistent-auth-2" 289 | result: 290 | content: 291 | match:arrayElements: 292 | type: "text" 293 | text: "match:regex:(OCAPI client not configured|required.*codeVersionId)" 294 | isError: true 295 | stderr: "toBeEmpty" 296 | 297 | # ================================================================================== 298 | # MODE VERIFICATION 299 | # ================================================================================== 300 | - it: "should verify that we're actually in docs-only mode by checking available tools" 301 | request: 302 | jsonrpc: "2.0" 303 | id: "verify-docs-mode" 304 | method: "tools/list" 305 | params: {} 306 | expect: 307 | response: 308 | jsonrpc: "2.0" 309 | id: "verify-docs-mode" 310 | result: 311 | tools: 312 | # Docs-only mode should have documentation tools but not OCAPI tools 313 | match:extractField: "tools.*.name" 314 | value: "match:arrayContains:get_sfcc_class_info" 315 | # And should NOT have code version tools 316 | match:extractField: "tools.*.name" 317 | value: "match:not:arrayContains:activate_code_version" 318 | stderr: "toBeEmpty" ```