This is page 24 of 43. Use http://codebase.md/taurgis/sfcc-dev-mcp?lines=false&page={x} to view the full context. # Directory Structure ``` ├── .DS_Store ├── .github │ ├── dependabot.yml │ ├── instructions │ │ ├── mcp-node-tests.instructions.md │ │ └── mcp-yml-tests.instructions.md │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.yml │ │ ├── config.yml │ │ ├── documentation.yml │ │ ├── feature_request.yml │ │ └── question.yml │ ├── PULL_REQUEST_TEMPLATE │ │ ├── bug_fix.md │ │ ├── documentation.md │ │ └── new_tool.md │ ├── pull_request_template.md │ └── workflows │ ├── ci.yml │ ├── deploy-pages.yml │ ├── publish.yml │ └── update-docs.yml ├── .gitignore ├── .husky │ └── pre-commit ├── aegis.config.docs-only.json ├── aegis.config.json ├── aegis.config.with-dw.json ├── AGENTS.md ├── ai-instructions │ ├── claude-desktop │ │ └── claude_custom_instructions.md │ ├── cursor │ │ └── .cursor │ │ └── rules │ │ ├── debugging-workflows.mdc │ │ ├── hooks-development.mdc │ │ ├── isml-templates.mdc │ │ ├── job-framework.mdc │ │ ├── performance-optimization.mdc │ │ ├── scapi-endpoints.mdc │ │ ├── security-patterns.mdc │ │ ├── sfcc-development.mdc │ │ ├── sfra-controllers.mdc │ │ ├── sfra-models.mdc │ │ ├── system-objects.mdc │ │ └── testing-patterns.mdc │ └── github-copilot │ └── copilot-instructions.md ├── CHANGELOG.md ├── CONTRIBUTING.md ├── docs │ ├── best-practices │ │ ├── cartridge_creation.md │ │ ├── isml_templates.md │ │ ├── job_framework.md │ │ ├── localserviceregistry.md │ │ ├── ocapi_hooks.md │ │ ├── performance.md │ │ ├── scapi_custom_endpoint.md │ │ ├── scapi_hooks.md │ │ ├── security.md │ │ ├── sfra_client_side_js.md │ │ ├── sfra_controllers.md │ │ ├── sfra_models.md │ │ └── sfra_scss.md │ ├── dw_campaign │ │ ├── ABTest.md │ │ ├── ABTestMgr.md │ │ ├── ABTestSegment.md │ │ ├── AmountDiscount.md │ │ ├── ApproachingDiscount.md │ │ ├── BonusChoiceDiscount.md │ │ ├── BonusDiscount.md │ │ ├── Campaign.md │ │ ├── CampaignMgr.md │ │ ├── CampaignStatusCodes.md │ │ ├── Coupon.md │ │ ├── CouponMgr.md │ │ ├── CouponRedemption.md │ │ ├── CouponStatusCodes.md │ │ ├── Discount.md │ │ ├── DiscountPlan.md │ │ ├── FixedPriceDiscount.md │ │ ├── FixedPriceShippingDiscount.md │ │ ├── FreeDiscount.md │ │ ├── FreeShippingDiscount.md │ │ ├── PercentageDiscount.md │ │ ├── PercentageOptionDiscount.md │ │ ├── PriceBookPriceDiscount.md │ │ ├── Promotion.md │ │ ├── PromotionMgr.md │ │ ├── PromotionPlan.md │ │ ├── SlotContent.md │ │ ├── SourceCodeGroup.md │ │ ├── SourceCodeInfo.md │ │ ├── SourceCodeStatusCodes.md │ │ └── TotalFixedPriceDiscount.md │ ├── dw_catalog │ │ ├── Catalog.md │ │ ├── CatalogMgr.md │ │ ├── Category.md │ │ ├── CategoryAssignment.md │ │ ├── CategoryLink.md │ │ ├── PriceBook.md │ │ ├── PriceBookMgr.md │ │ ├── Product.md │ │ ├── ProductActiveData.md │ │ ├── ProductAttributeModel.md │ │ ├── ProductAvailabilityLevels.md │ │ ├── ProductAvailabilityModel.md │ │ ├── ProductInventoryList.md │ │ ├── ProductInventoryMgr.md │ │ ├── ProductInventoryRecord.md │ │ ├── ProductLink.md │ │ ├── ProductMgr.md │ │ ├── ProductOption.md │ │ ├── ProductOptionModel.md │ │ ├── ProductOptionValue.md │ │ ├── ProductPriceInfo.md │ │ ├── ProductPriceModel.md │ │ ├── ProductPriceTable.md │ │ ├── ProductSearchHit.md │ │ ├── ProductSearchModel.md │ │ ├── ProductSearchRefinementDefinition.md │ │ ├── ProductSearchRefinements.md │ │ ├── ProductSearchRefinementValue.md │ │ ├── ProductVariationAttribute.md │ │ ├── ProductVariationAttributeValue.md │ │ ├── ProductVariationModel.md │ │ ├── Recommendation.md │ │ ├── SearchModel.md │ │ ├── SearchRefinementDefinition.md │ │ ├── SearchRefinements.md │ │ ├── SearchRefinementValue.md │ │ ├── SortingOption.md │ │ ├── SortingRule.md │ │ ├── Store.md │ │ ├── StoreGroup.md │ │ ├── StoreInventoryFilter.md │ │ ├── StoreInventoryFilterValue.md │ │ ├── StoreMgr.md │ │ ├── Variant.md │ │ └── VariationGroup.md │ ├── dw_content │ │ ├── Content.md │ │ ├── ContentMgr.md │ │ ├── ContentSearchModel.md │ │ ├── ContentSearchRefinementDefinition.md │ │ ├── ContentSearchRefinements.md │ │ ├── ContentSearchRefinementValue.md │ │ ├── Folder.md │ │ ├── Library.md │ │ ├── MarkupText.md │ │ └── MediaFile.md │ ├── dw_crypto │ │ ├── CertificateRef.md │ │ ├── CertificateUtils.md │ │ ├── Cipher.md │ │ ├── Encoding.md │ │ ├── JWE.md │ │ ├── JWEHeader.md │ │ ├── JWS.md │ │ ├── JWSHeader.md │ │ ├── KeyRef.md │ │ ├── Mac.md │ │ ├── MessageDigest.md │ │ ├── SecureRandom.md │ │ ├── Signature.md │ │ ├── WeakCipher.md │ │ ├── WeakMac.md │ │ ├── WeakMessageDigest.md │ │ ├── WeakSignature.md │ │ └── X509Certificate.md │ ├── dw_customer │ │ ├── AddressBook.md │ │ ├── AgentUserMgr.md │ │ ├── AgentUserStatusCodes.md │ │ ├── AuthenticationStatus.md │ │ ├── Credentials.md │ │ ├── Customer.md │ │ ├── CustomerActiveData.md │ │ ├── CustomerAddress.md │ │ ├── CustomerCDPData.md │ │ ├── CustomerContextMgr.md │ │ ├── CustomerGroup.md │ │ ├── CustomerList.md │ │ ├── CustomerMgr.md │ │ ├── CustomerPasswordConstraints.md │ │ ├── CustomerPaymentInstrument.md │ │ ├── CustomerStatusCodes.md │ │ ├── EncryptedObject.md │ │ ├── ExternalProfile.md │ │ ├── OrderHistory.md │ │ ├── ProductList.md │ │ ├── ProductListItem.md │ │ ├── ProductListItemPurchase.md │ │ ├── ProductListMgr.md │ │ ├── ProductListRegistrant.md │ │ ├── Profile.md │ │ └── Wallet.md │ ├── dw_extensions.applepay │ │ ├── ApplePayHookResult.md │ │ └── ApplePayHooks.md │ ├── dw_extensions.facebook │ │ ├── FacebookFeedHooks.md │ │ └── FacebookProduct.md │ ├── dw_extensions.paymentrequest │ │ ├── PaymentRequestHookResult.md │ │ └── PaymentRequestHooks.md │ ├── dw_extensions.payments │ │ ├── SalesforceBancontactPaymentDetails.md │ │ ├── SalesforceCardPaymentDetails.md │ │ ├── SalesforceEpsPaymentDetails.md │ │ ├── SalesforceIdealPaymentDetails.md │ │ ├── SalesforceKlarnaPaymentDetails.md │ │ ├── SalesforcePaymentDetails.md │ │ ├── SalesforcePaymentIntent.md │ │ ├── SalesforcePaymentMethod.md │ │ ├── SalesforcePaymentRequest.md │ │ ├── SalesforcePaymentsHooks.md │ │ ├── SalesforcePaymentsMgr.md │ │ ├── SalesforcePaymentsSiteConfiguration.md │ │ ├── SalesforcePayPalOrder.md │ │ ├── SalesforcePayPalOrderAddress.md │ │ ├── SalesforcePayPalOrderPayer.md │ │ ├── SalesforcePayPalPaymentDetails.md │ │ ├── SalesforceSepaDebitPaymentDetails.md │ │ └── SalesforceVenmoPaymentDetails.md │ ├── dw_extensions.pinterest │ │ ├── PinterestAvailability.md │ │ ├── PinterestFeedHooks.md │ │ ├── PinterestOrder.md │ │ ├── PinterestOrderHooks.md │ │ └── PinterestProduct.md │ ├── dw_io │ │ ├── CSVStreamReader.md │ │ ├── CSVStreamWriter.md │ │ ├── File.md │ │ ├── FileReader.md │ │ ├── FileWriter.md │ │ ├── InputStream.md │ │ ├── OutputStream.md │ │ ├── PrintWriter.md │ │ ├── RandomAccessFileReader.md │ │ ├── Reader.md │ │ ├── StringWriter.md │ │ ├── Writer.md │ │ ├── XMLIndentingStreamWriter.md │ │ ├── XMLStreamConstants.md │ │ ├── XMLStreamReader.md │ │ └── XMLStreamWriter.md │ ├── dw_job │ │ ├── JobExecution.md │ │ └── JobStepExecution.md │ ├── dw_net │ │ ├── FTPClient.md │ │ ├── FTPFileInfo.md │ │ ├── HTTPClient.md │ │ ├── HTTPRequestPart.md │ │ ├── Mail.md │ │ ├── SFTPClient.md │ │ ├── SFTPFileInfo.md │ │ ├── WebDAVClient.md │ │ └── WebDAVFileInfo.md │ ├── dw_object │ │ ├── ActiveData.md │ │ ├── CustomAttributes.md │ │ ├── CustomObject.md │ │ ├── CustomObjectMgr.md │ │ ├── Extensible.md │ │ ├── ExtensibleObject.md │ │ ├── Note.md │ │ ├── ObjectAttributeDefinition.md │ │ ├── ObjectAttributeGroup.md │ │ ├── ObjectAttributeValueDefinition.md │ │ ├── ObjectTypeDefinition.md │ │ ├── PersistentObject.md │ │ ├── SimpleExtensible.md │ │ └── SystemObjectMgr.md │ ├── dw_order │ │ ├── AbstractItem.md │ │ ├── AbstractItemCtnr.md │ │ ├── Appeasement.md │ │ ├── AppeasementItem.md │ │ ├── Basket.md │ │ ├── BasketMgr.md │ │ ├── BonusDiscountLineItem.md │ │ ├── CouponLineItem.md │ │ ├── CreateAgentBasketLimitExceededException.md │ │ ├── CreateBasketFromOrderException.md │ │ ├── CreateCouponLineItemException.md │ │ ├── CreateOrderException.md │ │ ├── CreateTemporaryBasketLimitExceededException.md │ │ ├── GiftCertificate.md │ │ ├── GiftCertificateLineItem.md │ │ ├── GiftCertificateMgr.md │ │ ├── GiftCertificateStatusCodes.md │ │ ├── Invoice.md │ │ ├── InvoiceItem.md │ │ ├── LineItem.md │ │ ├── LineItemCtnr.md │ │ ├── Order.md │ │ ├── OrderAddress.md │ │ ├── OrderItem.md │ │ ├── OrderMgr.md │ │ ├── OrderPaymentInstrument.md │ │ ├── OrderProcessStatusCodes.md │ │ ├── PaymentCard.md │ │ ├── PaymentInstrument.md │ │ ├── PaymentMethod.md │ │ ├── PaymentMgr.md │ │ ├── PaymentProcessor.md │ │ ├── PaymentStatusCodes.md │ │ ├── PaymentTransaction.md │ │ ├── PriceAdjustment.md │ │ ├── PriceAdjustmentLimitTypes.md │ │ ├── ProductLineItem.md │ │ ├── ProductShippingCost.md │ │ ├── ProductShippingLineItem.md │ │ ├── ProductShippingModel.md │ │ ├── Return.md │ │ ├── ReturnCase.md │ │ ├── ReturnCaseItem.md │ │ ├── ReturnItem.md │ │ ├── Shipment.md │ │ ├── ShipmentShippingCost.md │ │ ├── ShipmentShippingModel.md │ │ ├── ShippingLineItem.md │ │ ├── ShippingLocation.md │ │ ├── ShippingMethod.md │ │ ├── ShippingMgr.md │ │ ├── ShippingOrder.md │ │ ├── ShippingOrderItem.md │ │ ├── SumItem.md │ │ ├── TaxGroup.md │ │ ├── TaxItem.md │ │ ├── TaxMgr.md │ │ ├── TrackingInfo.md │ │ └── TrackingRef.md │ ├── dw_order.hooks │ │ ├── CalculateHooks.md │ │ ├── OrderHooks.md │ │ ├── PaymentHooks.md │ │ ├── ReturnHooks.md │ │ └── ShippingOrderHooks.md │ ├── dw_rpc │ │ ├── SOAPUtil.md │ │ ├── Stub.md │ │ └── WebReference.md │ ├── dw_suggest │ │ ├── BrandSuggestions.md │ │ ├── CategorySuggestions.md │ │ ├── ContentSuggestions.md │ │ ├── CustomSuggestions.md │ │ ├── ProductSuggestions.md │ │ ├── SearchPhraseSuggestions.md │ │ ├── SuggestedCategory.md │ │ ├── SuggestedContent.md │ │ ├── SuggestedPhrase.md │ │ ├── SuggestedProduct.md │ │ ├── SuggestedTerm.md │ │ ├── SuggestedTerms.md │ │ ├── Suggestions.md │ │ └── SuggestModel.md │ ├── dw_svc │ │ ├── FTPService.md │ │ ├── FTPServiceDefinition.md │ │ ├── HTTPFormService.md │ │ ├── HTTPFormServiceDefinition.md │ │ ├── HTTPService.md │ │ ├── HTTPServiceDefinition.md │ │ ├── LocalServiceRegistry.md │ │ ├── Result.md │ │ ├── Service.md │ │ ├── ServiceCallback.md │ │ ├── ServiceConfig.md │ │ ├── ServiceCredential.md │ │ ├── ServiceDefinition.md │ │ ├── ServiceProfile.md │ │ ├── ServiceRegistry.md │ │ ├── SOAPService.md │ │ └── SOAPServiceDefinition.md │ ├── dw_system │ │ ├── AgentUserStatusCodes.md │ │ ├── Cache.md │ │ ├── CacheMgr.md │ │ ├── HookMgr.md │ │ ├── InternalObject.md │ │ ├── JobProcessMonitor.md │ │ ├── Log.md │ │ ├── Logger.md │ │ ├── LogNDC.md │ │ ├── OrganizationPreferences.md │ │ ├── Pipeline.md │ │ ├── PipelineDictionary.md │ │ ├── RemoteInclude.md │ │ ├── Request.md │ │ ├── RequestHooks.md │ │ ├── Response.md │ │ ├── RESTErrorResponse.md │ │ ├── RESTResponseMgr.md │ │ ├── RESTSuccessResponse.md │ │ ├── SearchStatus.md │ │ ├── Session.md │ │ ├── Site.md │ │ ├── SitePreferences.md │ │ ├── Status.md │ │ ├── StatusItem.md │ │ ├── System.md │ │ └── Transaction.md │ ├── dw_util │ │ ├── ArrayList.md │ │ ├── Assert.md │ │ ├── BigInteger.md │ │ ├── Bytes.md │ │ ├── Calendar.md │ │ ├── Collection.md │ │ ├── Currency.md │ │ ├── DateUtils.md │ │ ├── Decimal.md │ │ ├── FilteringCollection.md │ │ ├── Geolocation.md │ │ ├── HashMap.md │ │ ├── HashSet.md │ │ ├── Iterator.md │ │ ├── LinkedHashMap.md │ │ ├── LinkedHashSet.md │ │ ├── List.md │ │ ├── Locale.md │ │ ├── Map.md │ │ ├── MapEntry.md │ │ ├── MappingKey.md │ │ ├── MappingMgr.md │ │ ├── PropertyComparator.md │ │ ├── SecureEncoder.md │ │ ├── SecureFilter.md │ │ ├── SeekableIterator.md │ │ ├── Set.md │ │ ├── SortedMap.md │ │ ├── SortedSet.md │ │ ├── StringUtils.md │ │ ├── Template.md │ │ └── UUIDUtils.md │ ├── dw_value │ │ ├── EnumValue.md │ │ ├── MimeEncodedText.md │ │ ├── Money.md │ │ └── Quantity.md │ ├── dw_web │ │ ├── ClickStream.md │ │ ├── ClickStreamEntry.md │ │ ├── Cookie.md │ │ ├── Cookies.md │ │ ├── CSRFProtection.md │ │ ├── Form.md │ │ ├── FormAction.md │ │ ├── FormElement.md │ │ ├── FormElementValidationResult.md │ │ ├── FormField.md │ │ ├── FormFieldOption.md │ │ ├── FormFieldOptions.md │ │ ├── FormGroup.md │ │ ├── FormList.md │ │ ├── FormListItem.md │ │ ├── Forms.md │ │ ├── HttpParameter.md │ │ ├── HttpParameterMap.md │ │ ├── LoopIterator.md │ │ ├── PageMetaData.md │ │ ├── PageMetaTag.md │ │ ├── PagingModel.md │ │ ├── Resource.md │ │ ├── URL.md │ │ ├── URLAction.md │ │ ├── URLParameter.md │ │ ├── URLRedirect.md │ │ ├── URLRedirectMgr.md │ │ └── URLUtils.md │ ├── sfra │ │ ├── account.md │ │ ├── address.md │ │ ├── billing.md │ │ ├── cart.md │ │ ├── categories.md │ │ ├── content.md │ │ ├── locale.md │ │ ├── order.md │ │ ├── payment.md │ │ ├── price-default.md │ │ ├── price-range.md │ │ ├── price-tiered.md │ │ ├── product-bundle.md │ │ ├── product-full.md │ │ ├── product-line-items.md │ │ ├── product-search.md │ │ ├── product-tile.md │ │ ├── querystring.md │ │ ├── render.md │ │ ├── request.md │ │ ├── response.md │ │ ├── server.md │ │ ├── shipping.md │ │ ├── store.md │ │ ├── stores.md │ │ └── totals.md │ └── TopLevel │ ├── APIException.md │ ├── arguments.md │ ├── Array.md │ ├── ArrayBuffer.md │ ├── BigInt.md │ ├── Boolean.md │ ├── ConversionError.md │ ├── DataView.md │ ├── Date.md │ ├── Error.md │ ├── ES6Iterator.md │ ├── EvalError.md │ ├── Fault.md │ ├── Float32Array.md │ ├── Float64Array.md │ ├── Function.md │ ├── Generator.md │ ├── global.md │ ├── Int16Array.md │ ├── Int32Array.md │ ├── Int8Array.md │ ├── InternalError.md │ ├── IOError.md │ ├── Iterable.md │ ├── Iterator.md │ ├── JSON.md │ ├── Map.md │ ├── Math.md │ ├── Module.md │ ├── Namespace.md │ ├── Number.md │ ├── Object.md │ ├── QName.md │ ├── RangeError.md │ ├── ReferenceError.md │ ├── RegExp.md │ ├── Set.md │ ├── StopIteration.md │ ├── String.md │ ├── Symbol.md │ ├── SyntaxError.md │ ├── SystemError.md │ ├── TypeError.md │ ├── Uint16Array.md │ ├── Uint32Array.md │ ├── Uint8Array.md │ ├── Uint8ClampedArray.md │ ├── URIError.md │ ├── WeakMap.md │ ├── WeakSet.md │ ├── XML.md │ ├── XMLList.md │ └── XMLStreamError.md ├── docs-site │ ├── .gitignore │ ├── App.tsx │ ├── components │ │ ├── Badge.tsx │ │ ├── BreadcrumbSchema.tsx │ │ ├── CodeBlock.tsx │ │ ├── Collapsible.tsx │ │ ├── ConfigBuilder.tsx │ │ ├── ConfigHero.tsx │ │ ├── ConfigModeTabs.tsx │ │ ├── icons.tsx │ │ ├── Layout.tsx │ │ ├── LightCodeContainer.tsx │ │ ├── NewcomerCTA.tsx │ │ ├── NextStepsStrip.tsx │ │ ├── OnThisPage.tsx │ │ ├── Search.tsx │ │ ├── SEO.tsx │ │ ├── Sidebar.tsx │ │ ├── StructuredData.tsx │ │ ├── ToolCard.tsx │ │ ├── ToolFilters.tsx │ │ ├── Typography.tsx │ │ └── VersionBadge.tsx │ ├── constants.tsx │ ├── index.html │ ├── main.tsx │ ├── metadata.json │ ├── package-lock.json │ ├── package.json │ ├── pages │ │ ├── AIInterfacesPage.tsx │ │ ├── ConfigurationPage.tsx │ │ ├── DevelopmentPage.tsx │ │ ├── ExamplesPage.tsx │ │ ├── FeaturesPage.tsx │ │ ├── HomePage.tsx │ │ ├── SecurityPage.tsx │ │ ├── ToolsPage.tsx │ │ └── TroubleshootingPage.tsx │ ├── postcss.config.js │ ├── public │ │ ├── .well-known │ │ │ └── security.txt │ │ ├── 404.html │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon.png │ │ ├── explain-product-pricing-methods-no-mcp.png │ │ ├── explain-product-pricing-methods.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon.ico │ │ ├── llms.txt │ │ ├── robots.txt │ │ ├── site.webmanifest │ │ └── sitemap.xml │ ├── README.md │ ├── scripts │ │ ├── generate-search-index.js │ │ ├── generate-sitemap.js │ │ └── search-dev.js │ ├── src │ │ └── styles │ │ ├── input.css │ │ └── prism-theme.css │ ├── tailwind.config.js │ ├── tsconfig.json │ ├── types.ts │ ├── utils │ │ ├── search.ts │ │ └── toolsData.ts │ └── vite.config.ts ├── eslint.config.js ├── jest.config.js ├── LICENSE ├── package-lock.json ├── package.json ├── README.md ├── scripts │ └── convert-docs.js ├── SECURITY.md ├── server.json ├── src │ ├── clients │ │ ├── base │ │ │ ├── http-client.ts │ │ │ ├── oauth-token.ts │ │ │ └── ocapi-auth-client.ts │ │ ├── best-practices-client.ts │ │ ├── cartridge-generation-client.ts │ │ ├── docs │ │ │ ├── class-content-parser.ts │ │ │ ├── class-name-resolver.ts │ │ │ ├── documentation-scanner.ts │ │ │ ├── index.ts │ │ │ └── referenced-types-extractor.ts │ │ ├── docs-client.ts │ │ ├── log-client.ts │ │ ├── logs │ │ │ ├── index.ts │ │ │ ├── log-analyzer.ts │ │ │ ├── log-client.ts │ │ │ ├── log-constants.ts │ │ │ ├── log-file-discovery.ts │ │ │ ├── log-file-reader.ts │ │ │ ├── log-formatter.ts │ │ │ ├── log-processor.ts │ │ │ ├── log-types.ts │ │ │ └── webdav-client-manager.ts │ │ ├── ocapi │ │ │ ├── code-versions-client.ts │ │ │ ├── site-preferences-client.ts │ │ │ └── system-objects-client.ts │ │ ├── ocapi-client.ts │ │ └── sfra-client.ts │ ├── config │ │ ├── configuration-factory.ts │ │ └── dw-json-loader.ts │ ├── core │ │ ├── handlers │ │ │ ├── abstract-log-tool-handler.ts │ │ │ ├── base-handler.ts │ │ │ ├── best-practices-handler.ts │ │ │ ├── cartridge-handler.ts │ │ │ ├── client-factory.ts │ │ │ ├── code-version-handler.ts │ │ │ ├── docs-handler.ts │ │ │ ├── job-log-handler.ts │ │ │ ├── job-log-tool-config.ts │ │ │ ├── log-handler.ts │ │ │ ├── log-tool-config.ts │ │ │ ├── sfra-handler.ts │ │ │ ├── system-object-handler.ts │ │ │ └── validation-helpers.ts │ │ ├── server.ts │ │ └── tool-definitions.ts │ ├── index.ts │ ├── main.ts │ ├── services │ │ ├── file-system-service.ts │ │ ├── index.ts │ │ └── path-service.ts │ ├── tool-configs │ │ ├── best-practices-tool-config.ts │ │ ├── cartridge-tool-config.ts │ │ ├── code-version-tool-config.ts │ │ ├── docs-tool-config.ts │ │ ├── job-log-tool-config.ts │ │ ├── log-tool-config.ts │ │ ├── sfra-tool-config.ts │ │ └── system-object-tool-config.ts │ ├── types │ │ └── types.ts │ └── utils │ ├── cache.ts │ ├── job-log-tool-config.ts │ ├── job-log-utils.ts │ ├── log-cache.ts │ ├── log-tool-config.ts │ ├── log-tool-constants.ts │ ├── log-tool-utils.ts │ ├── logger.ts │ ├── ocapi-url-builder.ts │ ├── path-resolver.ts │ ├── query-builder.ts │ ├── utils.ts │ └── validator.ts ├── tests │ ├── __mocks__ │ │ ├── docs-client.ts │ │ ├── src │ │ │ └── clients │ │ │ └── base │ │ │ └── http-client.js │ │ └── webdav.js │ ├── base-handler.test.ts │ ├── base-http-client.test.ts │ ├── best-practices-handler.test.ts │ ├── cache.test.ts │ ├── cartridge-handler.test.ts │ ├── class-content-parser.test.ts │ ├── class-name-resolver.test.ts │ ├── client-factory.test.ts │ ├── code-version-handler.test.ts │ ├── code-versions-client.test.ts │ ├── config.test.ts │ ├── configuration-factory.test.ts │ ├── docs-handler.test.ts │ ├── documentation-scanner.test.ts │ ├── file-system-service.test.ts │ ├── job-log-handler.test.ts │ ├── job-log-utils.test.ts │ ├── log-client.test.ts │ ├── log-handler.test.ts │ ├── log-processor.test.ts │ ├── logger.test.ts │ ├── mcp │ │ ├── AGENTS.md │ │ ├── node │ │ │ ├── activate-code-version-advanced.full-mode.programmatic.test.js │ │ │ ├── code-versions.full-mode.programmatic.test.js │ │ │ ├── generate-cartridge-structure.docs-only.programmatic.test.js │ │ │ ├── get-available-best-practice-guides.docs-only.programmatic.test.js │ │ │ ├── get-available-sfra-documents.programmatic.test.js │ │ │ ├── get-best-practice-guide.docs-only.programmatic.test.js │ │ │ ├── get-hook-reference.docs-only.programmatic.test.js │ │ │ ├── get-job-execution-summary.full-mode.programmatic.test.js │ │ │ ├── get-job-log-entries.full-mode.programmatic.test.js │ │ │ ├── get-latest-debug.full-mode.programmatic.test.js │ │ │ ├── get-latest-error.full-mode.programmatic.test.js │ │ │ ├── get-latest-info.full-mode.programmatic.test.js │ │ │ ├── get-latest-job-log-files.full-mode.programmatic.test.js │ │ │ ├── get-latest-warn.full-mode.programmatic.test.js │ │ │ ├── get-log-file-contents.full-mode.programmatic.test.js │ │ │ ├── get-sfcc-class-documentation.docs-only.programmatic.test.js │ │ │ ├── get-sfcc-class-info.docs-only.programmatic.test.js │ │ │ ├── get-sfra-categories.docs-only.programmatic.test.js │ │ │ ├── get-sfra-document.programmatic.test.js │ │ │ ├── get-sfra-documents-by-category.docs-only.programmatic.test.js │ │ │ ├── get-system-object-definition.full-mode.programmatic.test.js │ │ │ ├── get-system-object-definitions.docs-only.programmatic.test.js │ │ │ ├── get-system-object-definitions.full-mode.programmatic.test.js │ │ │ ├── list-log-files.full-mode.programmatic.test.js │ │ │ ├── list-sfcc-classes.docs-only.programmatic.test.js │ │ │ ├── search-best-practices.docs-only.programmatic.test.js │ │ │ ├── search-custom-object-attribute-definitions.full-mode.programmatic.test.js │ │ │ ├── search-job-logs-by-name.full-mode.programmatic.test.js │ │ │ ├── search-job-logs.full-mode.programmatic.test.js │ │ │ ├── search-logs.full-mode.programmatic.test.js │ │ │ ├── search-sfcc-classes.docs-only.programmatic.test.js │ │ │ ├── search-sfcc-methods.docs-only.programmatic.test.js │ │ │ ├── search-sfra-documentation.docs-only.programmatic.test.js │ │ │ ├── search-site-preferences.full-mode.programmatic.test.js │ │ │ ├── search-system-object-attribute-definitions.full-mode.programmatic.test.js │ │ │ ├── search-system-object-attribute-groups.full-mode.programmatic.test.js │ │ │ ├── summarize-logs.full-mode.programmatic.test.js │ │ │ ├── tools.docs-only.programmatic.test.js │ │ │ └── tools.full-mode.programmatic.test.js │ │ ├── README.md │ │ ├── test-fixtures │ │ │ └── dw.json │ │ └── yaml │ │ ├── activate-code-version.docs-only.test.mcp.yml │ │ ├── activate-code-version.full-mode.test.mcp.yml │ │ ├── get_latest_error.test.mcp.yml │ │ ├── get-available-best-practice-guides.docs-only.test.mcp.yml │ │ ├── get-available-best-practice-guides.full-mode.test.mcp.yml │ │ ├── get-available-sfra-documents.docs-only.test.mcp.yml │ │ ├── get-available-sfra-documents.full-mode.test.mcp.yml │ │ ├── get-best-practice-guide.docs-only.test.mcp.yml │ │ ├── get-best-practice-guide.full-mode.test.mcp.yml │ │ ├── get-code-versions.docs-only.test.mcp.yml │ │ ├── get-code-versions.full-mode.test.mcp.yml │ │ ├── get-hook-reference.docs-only.test.mcp.yml │ │ ├── get-hook-reference.full-mode.test.mcp.yml │ │ ├── get-job-execution-summary.full-mode.test.mcp.yml │ │ ├── get-job-log-entries.full-mode.test.mcp.yml │ │ ├── get-latest-debug.full-mode.test.mcp.yml │ │ ├── get-latest-error.full-mode.test.mcp.yml │ │ ├── get-latest-info.full-mode.test.mcp.yml │ │ ├── get-latest-job-log-files.full-mode.test.mcp.yml │ │ ├── get-latest-warn.full-mode.test.mcp.yml │ │ ├── get-log-file-contents.full-mode.test.mcp.yml │ │ ├── get-sfcc-class-documentation.docs-only.test.mcp.yml │ │ ├── get-sfcc-class-documentation.full-mode.test.mcp.yml │ │ ├── get-sfcc-class-info.docs-only.test.mcp.yml │ │ ├── get-sfcc-class-info.full-mode.test.mcp.yml │ │ ├── get-sfra-categories.docs-only.test.mcp.yml │ │ ├── get-sfra-categories.full-mode.test.mcp.yml │ │ ├── get-sfra-document.docs-only.test.mcp.yml │ │ ├── get-sfra-document.full-mode.test.mcp.yml │ │ ├── get-sfra-documents-by-category.docs-only.test.mcp.yml │ │ ├── get-sfra-documents-by-category.full-mode.test.mcp.yml │ │ ├── get-system-object-definition.docs-only.test.mcp.yml │ │ ├── get-system-object-definition.full-mode.test.mcp.yml │ │ ├── get-system-object-definitions.docs-only.test.mcp.yml │ │ ├── get-system-object-definitions.full-mode.test.mcp.yml │ │ ├── list-log-files.full-mode.test.mcp.yml │ │ ├── list-sfcc-classes.docs-only.test.mcp.yml │ │ ├── list-sfcc-classes.full-mode.test.mcp.yml │ │ ├── search-best-practices.docs-only.test.mcp.yml │ │ ├── search-best-practices.full-mode.test.mcp.yml │ │ ├── search-custom-object-attribute-definitions.docs-only.test.mcp.yml │ │ ├── search-custom-object-attribute-definitions.test.mcp.yml │ │ ├── search-job-logs-by-name.full-mode.test.mcp.yml │ │ ├── search-job-logs.full-mode.test.mcp.yml │ │ ├── search-logs.full-mode.test.mcp.yml │ │ ├── search-sfcc-classes.docs-only.test.mcp.yml │ │ ├── search-sfcc-classes.full-mode.test.mcp.yml │ │ ├── search-sfcc-methods.docs-only.test.mcp.yml │ │ ├── search-sfcc-methods.full-mode.test.mcp.yml │ │ ├── search-sfra-documentation.docs-only.test.mcp.yml │ │ ├── search-sfra-documentation.full-mode.test.mcp.yml │ │ ├── search-site-preferences.docs-only.test.mcp.yml │ │ ├── search-site-preferences.full-mode.test.mcp.yml │ │ ├── search-system-object-attribute-definitions.docs-only.test.mcp.yml │ │ ├── search-system-object-attribute-definitions.full-mode.test.mcp.yml │ │ ├── search-system-object-attribute-groups.docs-only.test.mcp.yml │ │ ├── search-system-object-attribute-groups.full-mode.test.mcp.yml │ │ ├── summarize-logs.full-mode.test.mcp.yml │ │ ├── tools.docs-only.test.mcp.yml │ │ └── tools.full-mode.test.mcp.yml │ ├── oauth-token.test.ts │ ├── ocapi-auth-client.test.ts │ ├── ocapi-client.test.ts │ ├── path-service.test.ts │ ├── query-builder.test.ts │ ├── referenced-types-extractor.test.ts │ ├── servers │ │ ├── sfcc-mock-server │ │ │ ├── mock-data │ │ │ │ └── ocapi │ │ │ │ ├── code-versions.json │ │ │ │ ├── custom-object-attributes-customapi.json │ │ │ │ ├── custom-object-attributes-globalsettings.json │ │ │ │ ├── custom-object-attributes-versionhistory.json │ │ │ │ ├── site-preferences-ccv.json │ │ │ │ ├── site-preferences-fastforward.json │ │ │ │ ├── site-preferences-sfra.json │ │ │ │ ├── site-preferences-storefront.json │ │ │ │ ├── site-preferences-system.json │ │ │ │ ├── system-object-attribute-groups-campaign.json │ │ │ │ ├── system-object-attribute-groups-category.json │ │ │ │ ├── system-object-attribute-groups-order.json │ │ │ │ ├── system-object-attribute-groups-product.json │ │ │ │ ├── system-object-attribute-groups-sitepreferences.json │ │ │ │ ├── system-object-attributes-customeraddress.json │ │ │ │ ├── system-object-attributes-product-expanded.json │ │ │ │ ├── system-object-attributes-product.json │ │ │ │ ├── system-object-definition-category.json │ │ │ │ ├── system-object-definition-customer.json │ │ │ │ ├── system-object-definition-customeraddress.json │ │ │ │ ├── system-object-definition-order.json │ │ │ │ ├── system-object-definition-product.json │ │ │ │ ├── system-object-definitions-old.json │ │ │ │ └── system-object-definitions.json │ │ │ ├── package-lock.json │ │ │ ├── package.json │ │ │ ├── README.md │ │ │ ├── scripts │ │ │ │ └── setup-logs.js │ │ │ ├── server.js │ │ │ └── src │ │ │ ├── app.js │ │ │ ├── config │ │ │ │ └── server-config.js │ │ │ ├── middleware │ │ │ │ ├── auth.js │ │ │ │ ├── cors.js │ │ │ │ └── logging.js │ │ │ ├── routes │ │ │ │ ├── ocapi │ │ │ │ │ ├── code-versions-handler.js │ │ │ │ │ ├── oauth-handler.js │ │ │ │ │ ├── ocapi-error-utils.js │ │ │ │ │ ├── ocapi-utils.js │ │ │ │ │ ├── site-preferences-handler.js │ │ │ │ │ └── system-objects-handler.js │ │ │ │ ├── ocapi.js │ │ │ │ └── webdav.js │ │ │ └── utils │ │ │ ├── mock-data-loader.js │ │ │ └── webdav-xml.js │ │ └── sfcc-mock-server-manager.ts │ ├── sfcc-mock-server.test.ts │ ├── site-preferences-client.test.ts │ ├── system-objects-client.test.ts │ ├── utils.test.ts │ ├── validation-helpers.test.ts │ └── validator.test.ts ├── tsconfig.json └── tsconfig.test.json ``` # Files -------------------------------------------------------------------------------- /docs/dw_catalog/ProductAvailabilityModel.md: -------------------------------------------------------------------------------- ```markdown ## Package: dw.catalog # Class ProductAvailabilityModel ## Inheritance Hierarchy - Object - dw.catalog.ProductAvailabilityModel ## Description The ProductAvailabilityModel provides methods for retrieving all information on availability of a single product. When using Omnichannel Inventory (OCI): OCI supports backorders, but does not support preorders or perpetual availability. OCI refers to expected restocks as Future inventory. OCI uses an eventual consistency model with asynchronous inventory data updates. Your code must not assume that inventory-affecting actions, such as placing orders, will immediately change inventory levels. ## Constants ### AVAILABILITY_STATUS_BACKORDER **Type:** String = "BACKORDER" Indicates that the product stock has run out, but will be replenished, and is therefore available for ordering. ### AVAILABILITY_STATUS_IN_STOCK **Type:** String = "IN_STOCK" Indicates that the product is in stock and available for ordering. ### AVAILABILITY_STATUS_NOT_AVAILABLE **Type:** String = "NOT_AVAILABLE" Indicates that the product is not currently available for ordering. ### AVAILABILITY_STATUS_PREORDER **Type:** String = "PREORDER" Indicates that the product is not yet in stock but is available for ordering. ## Properties ### availability **Type:** Number (Read Only) The availability of the product, which roughly defined is the ratio of the original stock that is still available to sell. The basic formula, if the current site uses an inventory list, is the ATS quantity divided by allocation amount. If the product is not orderable at all this method returns 0. The following specific rules apply for standard products: If inventory lists are in use: If no inventory record exists and the inventory list default-in-stock flag is true this method returns 1. If no inventory record exists the inventory list default-in-stock flag is false this method returns 0. If the product is not available this method returns 0. If the product is perpetually available this method returns 1. Otherwise, this method returns ATS / (allocation + preorderBackorderAllocation). (Values from ProductInventoryRecord.) If inventory lists are not in use the method returns 0. The following rules apply for special product types: For a master product this method returns the average availability of its online variations. For a master product with no online variations this method returns 0. For a master product with own inventory record the rules of the standard products apply. Note: In this case the availability of the variations is not considered. For a product set this method returns the greatest availability of the online products in the set. For a product set with no online products this method returns 0. For a product set with an inventory record the rules of the standard products apply. Note: In this case the availability of the set products is not considered. For a bundle, this method returns the least availability of the bundled products according to their bundled quantity and if it exist also from the bundle inventory record. ### availabilityStatus **Type:** String (Read Only) The availability-status for the minimum-orderable-quantity (MOQ) of the product. The MOQ essentially represents a single orderable unit, and therefore can be represented by a single availability-status. This method is essentially a convenience method. The same information can be retrieved by calling getAvailabilityLevels(Number) with the MOQ of the product as the parameter and then retrieving the single status from the returned map. This method is typically used to display a product's availability in the catalog when the order quantity is not known. ### inStock **Type:** isInStock(Number) (Read Only) Convenience method for isInStock(Number). Returns true, if the Product is available in the minimum-order-quantity. If the product does not have a minimum-order-quantity defined, in-stock is checked for a quantity value 1. ### inventoryRecord **Type:** ProductInventoryRecord (Read Only) The ProductInventoryRecord for the Product associated with this model. ### orderable **Type:** isOrderable(Number) (Read Only) Convenience method for isOrderable(Number). Returns true if the Product is currently online (based on its online flag and online dates) and is orderable in its minimum-order-quantity. If the product does not have a minimum-order-quantity specified, then 1 is used. The method returns false otherwise. Note: Orderable status is more general than in-stock status. A product may be out-of-stock but orderable because it is back-orderable or pre-orderable. ### SKUCoverage **Type:** Number (Read Only) The SKU coverage of the product. The basic formula for a master product is the ratio of online variations that are in stock to the total number of online variations. The following specific rules apply for standard products: If the product is in stock this method returns the availability of the product. If the product is out of stock this method returns 0. The following rules apply for special product types: For a master product this method returns the average SKU coverage of its online variations. For a master product with no online variations this method returns 0. For a product set this method returns the ratio of orderable SKUs in the product set over the total number of online SKUs in the product set. For a product set with no online products this method returns 0. For a product bundle this method returns 1 if all of the bundled products are online, and 0 otherwise. For a product bundle with no online bundled products this method returns 0. ### timeToOutOfStock **Type:** Number (Read Only) The number of hours before the product is expected to go out of stock. The basic formula is the ATS quantity divided by the sales velocity for the most recent day. The following specific rules apply for standard products: If the product is out of stock this method returns 0. If the product is perpetually available this method returns 1. If the sales velocity or ATS is not available this method returns 0. Otherwise this method returns ATS / sales velocity. The following rules apply for special product types: For a master product this method returns the greatest time to out of stock of its online variations. For a master product with no online variations this method returns 0. For a product set this method returns the greatest time to out of stock of the online products in the set. For a product set with no online products this method returns 0. For a bundle with no product inventory record, this method returns the least time to out of stock of the online bundled products. For a bundle with no product inventory record, and no online bundled products, this method returns 0. ## Constructor Summary ## Method Summary ### getAvailability **Signature:** `getAvailability() : Number` Returns the availability of the product, which roughly defined is the ratio of the original stock that is still available to sell. ### getAvailabilityLevels **Signature:** `getAvailabilityLevels(quantity : Number) : ProductAvailabilityLevels` Returns an instance of ProductAvailabilityLevels, where each available quantity represents a part of the input quantity. ### getAvailabilityStatus **Signature:** `getAvailabilityStatus() : String` Returns the availability-status for the minimum-orderable-quantity (MOQ) of the product. ### getInventoryRecord **Signature:** `getInventoryRecord() : ProductInventoryRecord` Returns the ProductInventoryRecord for the Product associated with this model. ### getSKUCoverage **Signature:** `getSKUCoverage() : Number` Returns the SKU coverage of the product. ### getTimeToOutOfStock **Signature:** `getTimeToOutOfStock() : Number` Returns the number of hours before the product is expected to go out of stock. ### isInStock **Signature:** `isInStock(quantity : Number) : boolean` Returns true if the Product is in-stock in the given quantity. ### isInStock **Signature:** `isInStock() : boolean` Convenience method for isInStock(Number). ### isOrderable **Signature:** `isOrderable(quantity : Number) : boolean` Returns true if the Product is currently online (based on its online flag and online dates) and the specified quantity does not exceed the quantity available for sale, and returns false otherwise. ### isOrderable **Signature:** `isOrderable() : boolean` Convenience method for isOrderable(Number). ## Method Detail ## Method Details ### getAvailability **Signature:** `getAvailability() : Number` **Description:** Returns the availability of the product, which roughly defined is the ratio of the original stock that is still available to sell. The basic formula, if the current site uses an inventory list, is the ATS quantity divided by allocation amount. If the product is not orderable at all this method returns 0. The following specific rules apply for standard products: If inventory lists are in use: If no inventory record exists and the inventory list default-in-stock flag is true this method returns 1. If no inventory record exists the inventory list default-in-stock flag is false this method returns 0. If the product is not available this method returns 0. If the product is perpetually available this method returns 1. Otherwise, this method returns ATS / (allocation + preorderBackorderAllocation). (Values from ProductInventoryRecord.) If inventory lists are not in use the method returns 0. The following rules apply for special product types: For a master product this method returns the average availability of its online variations. For a master product with no online variations this method returns 0. For a master product with own inventory record the rules of the standard products apply. Note: In this case the availability of the variations is not considered. For a product set this method returns the greatest availability of the online products in the set. For a product set with no online products this method returns 0. For a product set with an inventory record the rules of the standard products apply. Note: In this case the availability of the set products is not considered. For a bundle, this method returns the least availability of the bundled products according to their bundled quantity and if it exist also from the bundle inventory record. --- ### getAvailabilityLevels **Signature:** `getAvailabilityLevels(quantity : Number) : ProductAvailabilityLevels` **Description:** Returns an instance of ProductAvailabilityLevels, where each available quantity represents a part of the input quantity. This method is typically used to display availability information in the context of a known order quantity, e.g. a shopping cart. For example, if for a given product there are 3 pieces in stock with no pre/backorder handling specified, and the order quantity is 10, then the return instance would have the following state: ProductAvailabilityLevels.getInStock() - 3 ProductAvailabilityLevels.getPreorder() - 0 ProductAvailabilityLevels.getBackorder() - 0 ProductAvailabilityLevels.getNotAvailable() - 7 The following assertions can be made about the state of the returned instance. Between 1 and 3 levels are non-zero. The sum of the levels equals the input quantity. ProductAvailabilityLevels.getPreorder() or ProductAvailabilityLevels.getBackorder() may be available, but not both. Product bundles are handled specially: The availability of product bundles is calculated based on the availability of the bundled products. Therefore, if a bundle contains products that are not in stock, then the bundle itself is not in stock. If all the products in the bundle are on backorder, then the bundle itself is backordered. If a product bundle has its own inventory record, then this record may further limit the availability. If a bundle has no record, then only the records of the bundled products are considered. Product masters and product sets without an own inventory record are handled specially too: The availability is calculated based on the availability of the variants or set products. A product master or product set is in stock as soon as one of its variants or set products is in stock. Each product master or product set availability level reflects the sum of the variant or set product availability levels up to the specified quantity. Product masters or product sets with own inventory record are handled like standard products. The availability of the variants or set products is not considered. (Such an inventory scenario should be avoided.) Offline products are always unavailable and will result in returned levels that are all unavailable. When using Omnichannel Inventory (OCI), future restocks provided by OCI are mapped to AVAILABILITY_STATUS_BACKORDER. For more information, see the comments for ProductInventoryRecord. **Parameters:** - `quantity`: The quantity to evaluate. **Returns:** an instance of ProductAvailabilityLevels, which encapsulates the number of items for each relevant availability-status. **See Also:** ProductAvailabilityLevels **Throws:** IllegalArgumentException - if the specified quantity is less or equal than zero --- ### getAvailabilityStatus **Signature:** `getAvailabilityStatus() : String` **Description:** Returns the availability-status for the minimum-orderable-quantity (MOQ) of the product. The MOQ essentially represents a single orderable unit, and therefore can be represented by a single availability-status. This method is essentially a convenience method. The same information can be retrieved by calling getAvailabilityLevels(Number) with the MOQ of the product as the parameter and then retrieving the single status from the returned map. This method is typically used to display a product's availability in the catalog when the order quantity is not known. **Returns:** the availability-status. --- ### getInventoryRecord **Signature:** `getInventoryRecord() : ProductInventoryRecord` **Description:** Returns the ProductInventoryRecord for the Product associated with this model. **Returns:** the ProductInventoryRecord or null if there is none. --- ### getSKUCoverage **Signature:** `getSKUCoverage() : Number` **Description:** Returns the SKU coverage of the product. The basic formula for a master product is the ratio of online variations that are in stock to the total number of online variations. The following specific rules apply for standard products: If the product is in stock this method returns the availability of the product. If the product is out of stock this method returns 0. The following rules apply for special product types: For a master product this method returns the average SKU coverage of its online variations. For a master product with no online variations this method returns 0. For a product set this method returns the ratio of orderable SKUs in the product set over the total number of online SKUs in the product set. For a product set with no online products this method returns 0. For a product bundle this method returns 1 if all of the bundled products are online, and 0 otherwise. For a product bundle with no online bundled products this method returns 0. --- ### getTimeToOutOfStock **Signature:** `getTimeToOutOfStock() : Number` **Description:** Returns the number of hours before the product is expected to go out of stock. The basic formula is the ATS quantity divided by the sales velocity for the most recent day. The following specific rules apply for standard products: If the product is out of stock this method returns 0. If the product is perpetually available this method returns 1. If the sales velocity or ATS is not available this method returns 0. Otherwise this method returns ATS / sales velocity. The following rules apply for special product types: For a master product this method returns the greatest time to out of stock of its online variations. For a master product with no online variations this method returns 0. For a product set this method returns the greatest time to out of stock of the online products in the set. For a product set with no online products this method returns 0. For a bundle with no product inventory record, this method returns the least time to out of stock of the online bundled products. For a bundle with no product inventory record, and no online bundled products, this method returns 0. --- ### isInStock **Signature:** `isInStock(quantity : Number) : boolean` **Description:** Returns true if the Product is in-stock in the given quantity. This is determined as follows: If the product is not currently online (based on its online flag and online dates), then return false. If there is no inventory-list for the current site, then return false. If there is no inventory-record for the product, then return the default setting on the inventory-list. If there is no allocation-amount on the inventory-record, then return the value of the perpetual-flag. If there is an allocation-amount, but the perpetual-flag is true, then return true. If the quantity is less than or equal to the stock-level, then return true. Otherwise return false. **Parameters:** - `quantity`: the quantity that is requested **Returns:** true if the Product is in-stock. **Throws:** Exception - if the specified quantity is less or equal than zero --- ### isInStock **Signature:** `isInStock() : boolean` **Description:** Convenience method for isInStock(Number). Returns true, if the Product is available in the minimum-order-quantity. If the product does not have a minimum-order-quantity defined, in-stock is checked for a quantity value 1. **Returns:** true if the Product is in stock, otherwise false. --- ### isOrderable **Signature:** `isOrderable(quantity : Number) : boolean` **Description:** Returns true if the Product is currently online (based on its online flag and online dates) and the specified quantity does not exceed the quantity available for sale, and returns false otherwise. Note: Orderable status is more general than in-stock status. A product may be out-of-stock but orderable because it is back-orderable or pre-orderable. **Parameters:** - `quantity`: the quantity to test against. **Returns:** true if the item can be ordered in the specified quantity. **Throws:** Exception - if the specified quantity is less or equal than zero --- ### isOrderable **Signature:** `isOrderable() : boolean` **Description:** Convenience method for isOrderable(Number). Returns true if the Product is currently online (based on its online flag and online dates) and is orderable in its minimum-order-quantity. If the product does not have a minimum-order-quantity specified, then 1 is used. The method returns false otherwise. Note: Orderable status is more general than in-stock status. A product may be out-of-stock but orderable because it is back-orderable or pre-orderable. **Returns:** true if the Product is orderable for the minimum-order-quantity of the product. --- ``` -------------------------------------------------------------------------------- /tests/base-handler.test.ts: -------------------------------------------------------------------------------- ```typescript import { BaseToolHandler, HandlerContext, ToolArguments, ToolExecutionResult, HandlerError, GenericToolSpec, ToolExecutionContext } from '../src/core/handlers/base-handler.js'; import { Logger } from '../src/utils/logger.js'; // Mock implementation for testing class TestHandler extends BaseToolHandler { public initializeCalled = false; public disposeCalled = false; public initializeError: Error | null = null; public disposeError: Error | null = null; private toolConfig: Record<string, GenericToolSpec> = { 'test_tool': { exec: async (args) => `test_tool executed successfully with ${JSON.stringify(args)}`, logMessage: () => 'Testing tool execution', }, 'failing_tool': { exec: async () => { throw new Error('Test operation failed'); }, logMessage: () => 'Testing failing tool', }, 'validate_tool': { validate: (args, toolName) => { this.validateArgs(args, ['required_field'], toolName); }, exec: async (args) => `validate_tool executed successfully with ${JSON.stringify(args)}`, logMessage: () => 'Testing validation tool', }, 'defaults_tool': { defaults: (args) => ({ ...args, defaultValue: args.defaultValue ?? 'default_applied', numericDefault: args.numericDefault ?? 42, }), exec: async (args) => ({ receivedArgs: args }), logMessage: (args) => `Defaults tool with ${JSON.stringify(args)}`, }, 'context_tool': { exec: async (args, context) => ({ hasContext: !!context, hasHandlerContext: !!context.handlerContext, hasLogger: !!context.logger, contextKeys: Object.keys(context), }), logMessage: () => 'Testing execution context', }, 'complex_validation_tool': { validate: (args, toolName) => { if (!args.email?.includes('@')) { throw new HandlerError('Invalid email format', toolName, 'VALIDATION_ERROR'); } if (!args.age || args.age < 18) { throw new HandlerError('Age must be 18 or older', toolName, 'AGE_VALIDATION_ERROR'); } }, exec: async (args) => ({ validated: true, args }), logMessage: () => 'Testing complex validation', }, }; constructor(context: HandlerContext, subLoggerName: string = 'Test') { super(context, subLoggerName); } protected getToolConfig(): Record<string, GenericToolSpec> { return this.toolConfig; } protected getToolNameSet(): Set<string> { return new Set(['test_tool', 'failing_tool', 'validate_tool', 'defaults_tool', 'context_tool', 'complex_validation_tool']); } protected async createExecutionContext(): Promise<ToolExecutionContext> { return { handlerContext: this.context, logger: this.logger, }; } protected async onInitialize(): Promise<void> { this.initializeCalled = true; if (this.initializeError) { throw this.initializeError; } } protected async onDispose(): Promise<void> { this.disposeCalled = true; if (this.disposeError) { throw this.disposeError; } } // Expose protected methods for testing public testValidateArgs(args: ToolArguments, required: string[], toolName: string): void { this.validateArgs(args, required, toolName); } public testCreateResponse(data: any, stringify: boolean = true): ToolExecutionResult { return this.createResponse(data, stringify); } public getIsInitialized(): boolean { return (this as any)._isInitialized; } public getContext(): HandlerContext { return this.context; } } describe('BaseToolHandler', () => { let mockLogger: jest.Mocked<Logger>; let context: HandlerContext; let handler: TestHandler; beforeEach(() => { mockLogger = { debug: jest.fn(), log: jest.fn(), error: jest.fn(), timing: jest.fn(), methodEntry: jest.fn(), methodExit: jest.fn(), } as any; jest.spyOn(Logger, 'getChildLogger').mockReturnValue(mockLogger); context = { logger: mockLogger, config: { hostname: 'test.demandware.net' }, capabilities: { canAccessLogs: true, canAccessOCAPI: true }, }; handler = new TestHandler(context); }); afterEach(() => { jest.restoreAllMocks(); }); describe('constructor', () => { it('should initialize with context and logger', () => { expect(handler.getContext()).toBe(context); expect(Logger.getChildLogger).toHaveBeenCalledWith('Handler:Test'); expect(handler.getIsInitialized()).toBe(false); }); }); describe('initialization lifecycle', () => { it('should initialize on first use', async () => { expect(handler.initializeCalled).toBe(false); expect(handler.getIsInitialized()).toBe(false); const result = await handler.handle('test_tool', {}, Date.now()); expect(handler.initializeCalled).toBe(true); expect(handler.getIsInitialized()).toBe(true); expect(result.content[0].text).toContain('test_tool executed successfully'); }); it('should not initialize twice', async () => { // First call await handler.handle('test_tool', {}, Date.now()); expect(handler.initializeCalled).toBe(true); // Reset the flag to test it's not called again handler.initializeCalled = false; // Second call await handler.handle('test_tool', {}, Date.now()); expect(handler.initializeCalled).toBe(false); // Should not be called again expect(handler.getIsInitialized()).toBe(true); }); it('should handle initialization errors', async () => { handler.initializeError = new Error('Initialization failed'); const result = await handler.handle('test_tool', {}, Date.now()); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Initialization failed'); expect(handler.getIsInitialized()).toBe(false); }); }); describe('disposal lifecycle', () => { it('should dispose properly', async () => { // Initialize first await handler.handle('test_tool', {}, Date.now()); expect(handler.getIsInitialized()).toBe(true); // Dispose await handler.dispose(); expect(handler.disposeCalled).toBe(true); expect(handler.getIsInitialized()).toBe(false); }); it('should handle disposal errors', async () => { handler.disposeError = new Error('Disposal failed'); await expect(handler.dispose()).rejects.toThrow('Disposal failed'); expect(handler.getIsInitialized()).toBe(false); // Should still reset the flag }); it('should allow disposal without initialization', async () => { expect(handler.getIsInitialized()).toBe(false); await handler.dispose(); expect(handler.disposeCalled).toBe(true); expect(handler.getIsInitialized()).toBe(false); }); }); describe('canHandle', () => { it('should correctly identify handled tools', () => { expect(handler.canHandle('test_tool')).toBe(true); expect(handler.canHandle('failing_tool')).toBe(true); expect(handler.canHandle('validate_tool')).toBe(true); expect(handler.canHandle('unknown_tool')).toBe(false); }); }); describe('handle method', () => { it('should execute tool successfully', async () => { const startTime = Date.now(); const args = { param1: 'value1' }; const result = await handler.handle('test_tool', args, startTime); expect(result.content[0].text).toContain('test_tool executed successfully'); expect(result.content[0].text).toContain('value1'); expect(mockLogger.timing).toHaveBeenCalledWith('test_tool', startTime); }); it('should handle tool execution errors', async () => { const result = await handler.handle('failing_tool', {}, Date.now()); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Test operation failed'); }); it('should validate arguments when required', async () => { const args = { required_field: 'value' }; // Should pass with required field const result = await handler.handle('validate_tool', args, Date.now()); expect(result.content[0].text).toContain('validate_tool executed successfully'); // Should fail without required field const errorResult = await handler.handle('validate_tool', {}, Date.now()); expect(errorResult.isError).toBe(true); expect(errorResult.content[0].text).toContain('required_field is required'); }); }); describe('validateArgs', () => { it('should pass when all required fields are present', () => { const args = { field1: 'value1', field2: 'value2' }; expect(() => { handler.testValidateArgs(args, ['field1', 'field2'], 'test_tool'); }).not.toThrow(); }); it('should throw HandlerError when required field is missing', () => { const args = { field1: 'value1' }; expect(() => { handler.testValidateArgs(args, ['field1', 'field2'], 'test_tool'); }).toThrow(HandlerError); try { handler.testValidateArgs(args, ['field1', 'field2'], 'test_tool'); } catch (error) { expect(error).toBeInstanceOf(HandlerError); expect((error as HandlerError).message).toBe('field2 is required'); expect((error as HandlerError).toolName).toBe('test_tool'); expect((error as HandlerError).code).toBe('MISSING_ARGUMENT'); expect((error as HandlerError).details).toEqual({ required: ['field1', 'field2'], provided: ['field1'], }); } }); it('should throw when required field is null or undefined', () => { expect(() => { handler.testValidateArgs({ field1: null }, ['field1'], 'test_tool'); }).toThrow('field1 is required'); expect(() => { handler.testValidateArgs({ field1: undefined }, ['field1'], 'test_tool'); }).toThrow('field1 is required'); }); it('should handle empty args object', () => { expect(() => { handler.testValidateArgs({}, ['field1'], 'test_tool'); }).toThrow('field1 is required'); }); it('should handle null args', () => { expect(() => { handler.testValidateArgs(null as any, ['field1'], 'test_tool'); }).toThrow('field1 is required'); }); }); describe('createResponse', () => { it('should create stringified response by default', () => { const data = { key: 'value', number: 42 }; const response = handler.testCreateResponse(data); expect(response.content[0].text).toBe(JSON.stringify(data, null, 2)); expect(response.isError).toBe(false); }); it('should create non-stringified response when requested', () => { const data = { key: 'value', number: 42 }; const response = handler.testCreateResponse(data, false); expect(response.content[0].text).toBe(data); expect(response.isError).toBe(false); }); it('should handle null data', () => { const response = handler.testCreateResponse(null); expect(response.content[0].text).toBe('null'); expect(response.isError).toBe(false); }); it('should handle primitive values', () => { expect(handler.testCreateResponse('string').content[0].text).toBe('"string"'); expect(handler.testCreateResponse(42).content[0].text).toBe('42'); expect(handler.testCreateResponse(true).content[0].text).toBe('true'); }); }); describe('HandlerError', () => { it('should create error with all properties', () => { const details = { key: 'value' }; const error = new HandlerError('Test error', 'test_tool', 'TEST_CODE', details); expect(error.message).toBe('Test error'); expect(error.toolName).toBe('test_tool'); expect(error.code).toBe('TEST_CODE'); expect(error.details).toBe(details); expect(error.name).toBe('HandlerError'); expect(error).toBeInstanceOf(Error); }); it('should create error with optional parameters', () => { const error = new HandlerError('Simple error', 'test_tool'); expect(error.message).toBe('Simple error'); expect(error.toolName).toBe('test_tool'); expect(error.code).toBe('HANDLER_ERROR'); expect(error.details).toBeUndefined(); }); }); describe('logging integration', () => { it('should log debug messages during execution', async () => { await handler.handle('test_tool', {}, Date.now()); expect(mockLogger.debug).toHaveBeenCalledWith( 'test_tool completed successfully', expect.any(Object), ); }); it('should log timing information', async () => { const startTime = Date.now(); await handler.handle('test_tool', {}, startTime); expect(mockLogger.timing).toHaveBeenCalledWith('test_tool', startTime); }); }); describe('error handling', () => { it('should preserve original error types', async () => { const customError = new TypeError('Custom type error'); handler.initializeError = customError; const result = await handler.handle('test_tool', {}, Date.now()); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Custom type error'); }); it('should handle async operation errors', async () => { const result = await handler.handle('failing_tool', {}, Date.now()); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Test operation failed'); }); }); describe('config-driven functionality', () => { describe('unsupported tools', () => { it('should throw error for unsupported tools', async () => { await expect(handler.handle('unknown_tool', {}, Date.now())) .rejects.toThrow('Unsupported tool: unknown_tool'); }); it('should return false for canHandle on unsupported tools', () => { expect(handler.canHandle('unknown_tool')).toBe(false); }); }); describe('default values', () => { it('should apply default values when not provided', async () => { const result = await handler.handle('defaults_tool', {}, Date.now()); const parsedResult = JSON.parse(result.content[0].text); expect(parsedResult.receivedArgs.defaultValue).toBe('default_applied'); expect(parsedResult.receivedArgs.numericDefault).toBe(42); }); it('should not override provided values with defaults', async () => { const args = { defaultValue: 'custom_value', numericDefault: 100 }; const result = await handler.handle('defaults_tool', args, Date.now()); const parsedResult = JSON.parse(result.content[0].text); expect(parsedResult.receivedArgs.defaultValue).toBe('custom_value'); expect(parsedResult.receivedArgs.numericDefault).toBe(100); }); it('should mix provided and default values', async () => { const args = { defaultValue: 'custom_value', otherParam: 'other' }; const result = await handler.handle('defaults_tool', args, Date.now()); const parsedResult = JSON.parse(result.content[0].text); expect(parsedResult.receivedArgs.defaultValue).toBe('custom_value'); expect(parsedResult.receivedArgs.numericDefault).toBe(42); // default applied expect(parsedResult.receivedArgs.otherParam).toBe('other'); }); }); describe('execution context', () => { it('should provide ToolExecutionContext to tool functions', async () => { const result = await handler.handle('context_tool', {}, Date.now()); const parsedResult = JSON.parse(result.content[0].text); expect(parsedResult.hasContext).toBe(true); expect(parsedResult.hasHandlerContext).toBe(true); expect(parsedResult.hasLogger).toBe(true); expect(parsedResult.contextKeys).toContain('handlerContext'); expect(parsedResult.contextKeys).toContain('logger'); }); }); describe('complex validation', () => { it('should pass complex validation with valid args', async () => { const args = { email: '[email protected]', age: 25 }; const result = await handler.handle('complex_validation_tool', args, Date.now()); const parsedResult = JSON.parse(result.content[0].text); expect(parsedResult.validated).toBe(true); expect(parsedResult.args).toEqual(args); }); it('should fail validation with invalid email', async () => { const args = { email: 'invalid-email', age: 25 }; const result = await handler.handle('complex_validation_tool', args, Date.now()); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Invalid email format'); }); it('should fail validation with invalid age', async () => { const args = { email: '[email protected]', age: 16 }; const result = await handler.handle('complex_validation_tool', args, Date.now()); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Age must be 18 or older'); }); it('should handle validation errors with custom error codes', async () => { const args = { email: 'invalid-email', age: 25 }; const result = await handler.handle('complex_validation_tool', args, Date.now()); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Invalid email format'); // The error should be a HandlerError with VALIDATION_ERROR code }); }); describe('logging with defaults', () => { it('should use log message with applied defaults', async () => { const args = { customParam: 'test' }; // Spy on the debug method to capture log messages const debugSpy = jest.spyOn(mockLogger, 'debug'); await handler.handle('defaults_tool', args, Date.now()); // Check that the log message includes the default values const debugCalls = debugSpy.mock.calls; const logMessageCall = debugCalls.find(call => typeof call[0] === 'string' && call[0].includes('Defaults tool with'), ); expect(logMessageCall).toBeDefined(); expect(logMessageCall?.[0]).toContain('default_applied'); expect(logMessageCall?.[0]).toContain('42'); }); }); describe('tool config edge cases', () => { it('should handle tools with minimal config', async () => { // The test_tool has minimal config - no validation, no defaults const result = await handler.handle('test_tool', { param: 'value' }, Date.now()); expect(result.content[0].text).toContain('test_tool executed successfully'); expect(result.content[0].text).toContain('value'); }); it('should handle empty arguments with defaults', async () => { const result = await handler.handle('defaults_tool', {}, Date.now()); const parsedResult = JSON.parse(result.content[0].text); expect(parsedResult.receivedArgs.defaultValue).toBe('default_applied'); expect(parsedResult.receivedArgs.numericDefault).toBe(42); }); }); }); }); ``` -------------------------------------------------------------------------------- /docs/dw_crypto/WeakCipher.md: -------------------------------------------------------------------------------- ```markdown ## Package: dw.crypto # Class WeakCipher ## Inheritance Hierarchy - Object - dw.crypto.WeakCipher ## Description This API provides access to Deprecated algorithms. See Cipher for full documentation. WeakCipher is simply a drop-in replacement that only supports deprecated algorithms and key lengths. This is helpful when you need to deal with weak algorithms for backward compatibility purposes, but Cipher should always be used for new development and for anything intended to be secure. Note: this class handles sensitive security-related data. Pay special attention to PCI DSS v3 requirements 2, 4, and 12. ## Constants ### CHAR_ENCODING **Type:** String = "UTF8" Strings containing keys, plain texts, cipher texts etc. are internally converted into byte arrays using this encoding (currently UTF8). ## Properties ## Constructor Summary WeakCipher() ## Method Summary ### decrypt **Signature:** `decrypt(base64Msg : String, key : String, transformation : String, saltOrIV : String, iterations : Number) : String` Decrypts the message using the given parameters. ### decrypt **Signature:** `decrypt(base64Msg : String, privateKey : KeyRef, transformation : String, saltOrIV : String, iterations : Number) : String` Alternative method to decrypt(String, String, String, String, Number), which allows using a key in the keystore for the decryption. ### decrypt **Signature:** `decrypt(base64Msg : String, key : String, transformation : String, saltOrIV : String, iterations : Number) : String` Decrypts the message using the given parameters. ### decrypt **Signature:** `decrypt(base64Msg : String, privateKey : KeyRef, transformation : String, saltOrIV : String, iterations : Number) : String` Alternative method to decrypt_3(String, String, String, String, Number), which allows using a key in the keystore for the decryption. ### decryptBytes **Signature:** `decryptBytes(encryptedBytes : Bytes, key : String, transformation : String, saltOrIV : String, iterations : Number) : Bytes` Lower-level decryption API. ### decryptBytes **Signature:** `decryptBytes(encryptedBytes : Bytes, privateKey : KeyRef, transformation : String, saltOrIV : String, iterations : Number) : Bytes` Alternative method to decryptBytes(Bytes, String, String, String, Number), which allows to use a key in the keystore for the decryption. ### decryptBytes **Signature:** `decryptBytes(encryptedBytes : Bytes, key : String, transformation : String, saltOrIV : String, iterations : Number) : Bytes` Lower-level decryption API. ### decryptBytes **Signature:** `decryptBytes(encryptedBytes : Bytes, privateKey : KeyRef, transformation : String, saltOrIV : String, iterations : Number) : Bytes` Alternative method to decryptBytes_3(Bytes, String, String, String, Number), which allows to use a key in the keystore for the decryption. ### encrypt **Signature:** `encrypt(message : String, key : String, transformation : String, saltOrIV : String, iterations : Number) : String` Encrypt the passed message by using the specified key and applying the transformations described by the specified parameters. ### encrypt **Signature:** `encrypt(message : String, publicKey : CertificateRef, transformation : String, saltOrIV : String, iterations : Number) : String` Encrypt the passed message by using the specified key and applying the transformations described by the specified parameters. ### encrypt **Signature:** `encrypt(message : String, key : String, transformation : String, saltOrIV : String, iterations : Number) : String` Encrypt the passed message by using the specified key and applying the transformations described by the specified parameters. ### encrypt **Signature:** `encrypt(message : String, publicKey : CertificateRef, transformation : String, saltOrIV : String, iterations : Number) : String` Encrypt the passed message by using the specified key and applying the transformations described by the specified parameters. ### encryptBytes **Signature:** `encryptBytes(messageBytes : Bytes, key : String, transformation : String, saltOrIV : String, iterations : Number) : Bytes` Lower-level encryption API. ### encryptBytes **Signature:** `encryptBytes(messageBytes : Bytes, publicKey : CertificateRef, transformation : String, saltOrIV : String, iterations : Number) : Bytes` Alternative method to encryptBytes(Bytes, String, String, String, Number), which allows to use a key in the keystore for the encryption. ### encryptBytes **Signature:** `encryptBytes(messageBytes : Bytes, key : String, transformation : String, saltOrIV : String, iterations : Number) : Bytes` Lower-level encryption API. ### encryptBytes **Signature:** `encryptBytes(messageBytes : Bytes, publicKey : CertificateRef, transformation : String, saltOrIV : String, iterations : Number) : Bytes` Alternative method to encryptBytes_3(Bytes, String, String, String, Number), which allows to use a key in the keystore for the encryption. ## Constructor Detail ## Method Detail ## Method Details ### decrypt **Signature:** `decrypt(base64Msg : String, key : String, transformation : String, saltOrIV : String, iterations : Number) : String` **Description:** Decrypts the message using the given parameters. See Cipher.decrypt(String, String, String, String, Number) for full documentation. **API Versioned:** From version 15.5. No longer available as of version 16.2. Requires Base64-encryption for the salt parameter. **Parameters:** - `base64Msg`: the base64 encoded data to decrypt - `key`: The decryption key - `transformation`: Transformation in "algorithm/mode/padding" format. - `saltOrIV`: Initialization value appropriate for the algorithm. - `iterations`: The number of passes to make when turning a passphrase into a key, if applicable **Returns:** the original plaintext message. --- ### decrypt **Signature:** `decrypt(base64Msg : String, privateKey : KeyRef, transformation : String, saltOrIV : String, iterations : Number) : String` **Description:** Alternative method to decrypt(String, String, String, String, Number), which allows using a key in the keystore for the decryption. See Cipher.decrypt(String, KeyRef, String, String, Number) for full documentation. **API Versioned:** From version 15.5. No longer available as of version 16.2. Requires Base64-encryption for the salt parameter. **Parameters:** - `base64Msg`: the base64 encoded data to decrypt - `privateKey`: A reference to a private key in the key store. - `transformation`: Transformation in "algorithm/mode/padding" format. - `saltOrIV`: Initialization value appropriate for the algorithm. - `iterations`: The number of passes to make when turning a passphrase into a key, if applicable **Returns:** the original plaintext message. --- ### decrypt **Signature:** `decrypt(base64Msg : String, key : String, transformation : String, saltOrIV : String, iterations : Number) : String` **Description:** Decrypts the message using the given parameters. See Cipher.decrypt_3(String, String, String, String, Number) for full documentation. **API Versioned:** From version 16.2. Does not use a default initialization vector. **Parameters:** - `base64Msg`: the base64 encoded data to decrypt - `key`: The decryption key - `transformation`: Transformation in "algorithm/mode/padding" format. - `saltOrIV`: Initialization value appropriate for the algorithm. - `iterations`: The number of passes to make when turning a passphrase into a key, if applicable **Returns:** the original plaintext message. --- ### decrypt **Signature:** `decrypt(base64Msg : String, privateKey : KeyRef, transformation : String, saltOrIV : String, iterations : Number) : String` **Description:** Alternative method to decrypt_3(String, String, String, String, Number), which allows using a key in the keystore for the decryption. See Cipher.decrypt_3(String, KeyRef, String, String, Number) for full documentation. **API Versioned:** From version 16.2. Does not use a default initialization vector. **Parameters:** - `base64Msg`: the base64 encoded data to decrypt - `privateKey`: A reference to a private key in the key store. - `transformation`: Transformation in "algorithm/mode/padding" format. - `saltOrIV`: Initialization value appropriate for the algorithm. - `iterations`: The number of passes to make when turning a passphrase into a key, if applicable **Returns:** the original plaintext message. --- ### decryptBytes **Signature:** `decryptBytes(encryptedBytes : Bytes, key : String, transformation : String, saltOrIV : String, iterations : Number) : Bytes` **Description:** Lower-level decryption API. Decrypts the passed bytes using the specified key and applying the transformations described by the specified parameters. See Cipher.decryptBytes(Bytes, String, String, String, Number) for full documentation. **API Versioned:** From version 15.5. No longer available as of version 16.2. Requires Base64-encryption for the salt parameter. **Parameters:** - `encryptedBytes`: The bytes to decrypt. - `key`: The key to use for decryption. - `transformation`: The transformation used to originally encrypt. - `saltOrIV`: the salt or IV to use. - `iterations`: the iterations to use. **Returns:** The decrypted bytes. **See Also:** decrypt(String, String, String, String, Number) --- ### decryptBytes **Signature:** `decryptBytes(encryptedBytes : Bytes, privateKey : KeyRef, transformation : String, saltOrIV : String, iterations : Number) : Bytes` **Description:** Alternative method to decryptBytes(Bytes, String, String, String, Number), which allows to use a key in the keystore for the decryption. See Cipher.decryptBytes(Bytes, KeyRef, String, String, Number) for full documentation. **API Versioned:** From version 15.5. No longer available as of version 16.2. Requires Base64-encryption for the salt parameter. **Parameters:** - `encryptedBytes`: The bytes to decrypt. - `privateKey`: A reference to a private key in the key store. - `transformation`: The transformation used to originally encrypt. - `saltOrIV`: the salt or IV to use. - `iterations`: the iterations to use. **Returns:** The decrypted bytes. --- ### decryptBytes **Signature:** `decryptBytes(encryptedBytes : Bytes, key : String, transformation : String, saltOrIV : String, iterations : Number) : Bytes` **Description:** Lower-level decryption API. Decrypts the passed bytes using the specified key and applying the transformations described by the specified parameters. See Cipher.decryptBytes_3(Bytes, String, String, String, Number) for full documentation. **API Versioned:** From version 16.2. Does not use a default initialization vector. **Parameters:** - `encryptedBytes`: The bytes to decrypt. - `key`: The key to use for decryption. - `transformation`: The transformation used to originally encrypt. - `saltOrIV`: the salt or IV to use. - `iterations`: the iterations to use. **Returns:** The decrypted bytes. **See Also:** decrypt_3(String, String, String, String, Number) --- ### decryptBytes **Signature:** `decryptBytes(encryptedBytes : Bytes, privateKey : KeyRef, transformation : String, saltOrIV : String, iterations : Number) : Bytes` **Description:** Alternative method to decryptBytes_3(Bytes, String, String, String, Number), which allows to use a key in the keystore for the decryption. See Cipher.decryptBytes_3(Bytes, KeyRef, String, String, Number) for full documentation. **API Versioned:** From version 16.2. Does not use a default initialization vector. **Parameters:** - `encryptedBytes`: The bytes to decrypt. - `privateKey`: A reference to a private key in the key store. - `transformation`: The transformation used to originally encrypt. - `saltOrIV`: the salt or IV to use. - `iterations`: the iterations to use. **Returns:** The decrypted bytes. --- ### encrypt **Signature:** `encrypt(message : String, key : String, transformation : String, saltOrIV : String, iterations : Number) : String` **Description:** Encrypt the passed message by using the specified key and applying the transformations described by the specified parameters. See Cipher.encrypt(String, String, String, String, Number) for full documentation. **API Versioned:** From version 15.5. No longer available as of version 16.2. Requires Base64-encryption for the salt parameter. **Parameters:** - `message`: Message to encrypt (this will be converted to UTF-8 first) - `key`: Key - `transformation`: Transformation in "algorithm/mode/padding" format - `saltOrIV`: Initialization value appropriate for the algorithm - `iterations`: The number of passes to make when turning a passphrase into a key, if applicable **Returns:** Base64-encoded encrypted data --- ### encrypt **Signature:** `encrypt(message : String, publicKey : CertificateRef, transformation : String, saltOrIV : String, iterations : Number) : String` **Description:** Encrypt the passed message by using the specified key and applying the transformations described by the specified parameters. See Cipher.encrypt(String, CertificateRef, String, String, Number) for full documentation. **API Versioned:** From version 15.5. No longer available as of version 16.2. Requires Base64-encryption for the salt parameter. **Parameters:** - `message`: Message to encrypt (this will be converted to UTF-8 first) - `publicKey`: A reference to a public key - `transformation`: Transformation in "algorithm/mode/padding" format - `saltOrIV`: Initialization value appropriate for the algorithm - `iterations`: The number of passes to make when turning a passphrase into a key, if applicable **Returns:** Base64-encoded encrypted data --- ### encrypt **Signature:** `encrypt(message : String, key : String, transformation : String, saltOrIV : String, iterations : Number) : String` **Description:** Encrypt the passed message by using the specified key and applying the transformations described by the specified parameters. See Cipher.encrypt_3(String, String, String, String, Number) for full documentation. **API Versioned:** From version 16.2. Does not use a default initialization vector. **Parameters:** - `message`: Message to encrypt (this will be converted to UTF-8 first) - `key`: Key - `transformation`: Transformation in "algorithm/mode/padding" format - `saltOrIV`: Initialization value appropriate for the algorithm - `iterations`: The number of passes to make when turning a passphrase into a key, if applicable **Returns:** Base64-encoded encrypted data --- ### encrypt **Signature:** `encrypt(message : String, publicKey : CertificateRef, transformation : String, saltOrIV : String, iterations : Number) : String` **Description:** Encrypt the passed message by using the specified key and applying the transformations described by the specified parameters. See Cipher.encrypt_3(String, CertificateRef, String, String, Number) for full documentation. **API Versioned:** From version 16.2. Does not use a default initialization vector. **Parameters:** - `message`: Message to encrypt (this will be converted to UTF-8 first) - `publicKey`: A reference to a public key - `transformation`: Transformation in "algorithm/mode/padding" format - `saltOrIV`: Initialization value appropriate for the algorithm - `iterations`: The number of passes to make when turning a passphrase into a key, if applicable **Returns:** Base64-encoded encrypted data --- ### encryptBytes **Signature:** `encryptBytes(messageBytes : Bytes, key : String, transformation : String, saltOrIV : String, iterations : Number) : Bytes` **Description:** Lower-level encryption API. Encrypts the passed bytes by using the specified key and applying the transformations described by the specified parameters. See Cipher.encryptBytes(Bytes, String, String, String, Number) for full documentation. **API Versioned:** From version 15.5. No longer available as of version 16.2. Requires Base64-encryption for the salt parameter. **Parameters:** - `messageBytes`: The bytes to encrypt. - `key`: The key to use for encryption. - `transformation`: Transformation in "algorithm/mode/padding" format. - `saltOrIV`: Initialization value appropriate for the algorithm. - `iterations`: The number of passes to make when turning a passphrase into a key. **Returns:** the encrypted bytes. **See Also:** encrypt(String, String, String, String, Number) --- ### encryptBytes **Signature:** `encryptBytes(messageBytes : Bytes, publicKey : CertificateRef, transformation : String, saltOrIV : String, iterations : Number) : Bytes` **Description:** Alternative method to encryptBytes(Bytes, String, String, String, Number), which allows to use a key in the keystore for the encryption. See Cipher.encryptBytes(Bytes, CertificateRef, String, String, Number) for full documentation. **API Versioned:** From version 15.5. No longer available as of version 16.2. Requires Base64-encryption for the salt parameter. **Parameters:** - `messageBytes`: The bytes to encrypt. - `publicKey`: A reference to a public key. - `transformation`: Transformation in "algorithm/mode/padding" format. - `saltOrIV`: Initialization value appropriate for the algorithm. - `iterations`: The number of passes to make when turning a passphrase into a key. **Returns:** the encrypted bytes. **See Also:** encrypt(String, CertificateRef, String, String, Number) --- ### encryptBytes **Signature:** `encryptBytes(messageBytes : Bytes, key : String, transformation : String, saltOrIV : String, iterations : Number) : Bytes` **Description:** Lower-level encryption API. Encrypts the passed bytes by using the specified key and applying the transformations described by the specified parameters. See Cipher.encryptBytes_3(Bytes, String, String, String, Number) for full documentation. **API Versioned:** From version 16.2. Does not use a default initialization vector. **Parameters:** - `messageBytes`: The bytes to encrypt. - `key`: The key to use for encryption. - `transformation`: Transformation in "algorithm/mode/padding" format. - `saltOrIV`: Initialization value appropriate for the algorithm. - `iterations`: The number of passes to make when turning a passphrase into a key. **Returns:** the encrypted bytes. **See Also:** encrypt_3(String, String, String, String, Number) --- ### encryptBytes **Signature:** `encryptBytes(messageBytes : Bytes, publicKey : CertificateRef, transformation : String, saltOrIV : String, iterations : Number) : Bytes` **Description:** Alternative method to encryptBytes_3(Bytes, String, String, String, Number), which allows to use a key in the keystore for the encryption. See Cipher.encryptBytes_3(Bytes, CertificateRef, String, String, Number) for full documentation. **API Versioned:** From version 16.2. Does not use a default initialization vector. **Parameters:** - `messageBytes`: The bytes to encrypt. - `publicKey`: A reference to a public key. - `transformation`: Transformation in "algorithm/mode/padding" format. - `saltOrIV`: Initialization value appropriate for the algorithm. - `iterations`: The number of passes to make when turning a passphrase into a key. **Returns:** the encrypted bytes. **See Also:** encrypt_3(String, CertificateRef, String, String, Number) --- ``` -------------------------------------------------------------------------------- /tests/mcp/yaml/search-system-object-attribute-groups.full-mode.test.mcp.yml: -------------------------------------------------------------------------------- ```yaml # ================================================================================== # SFCC MCP Server - search_system_object_attribute_groups Tool YAML Tests (Full Mode) # Streamlined smoke testing and declarative validation for core functionality # Complex business logic, edge cases, and workflows are covered in programmatic tests # # Available System Objects for Attribute Group Testing: # - Product (35 groups: ChannelIntegration, ExternalSearch, Order, PXL3_Hoesjes, etc.) # - SitePreferences (8 groups: CCV, SFRA Unified Feature Cartridge, Storefront Configs, etc.) # - Customer, Order, Category, etc. (varying group counts) # # Quick Test Commands: # aegis "tests/mcp/yaml/search-system-object-attribute-groups.full-mode.test.mcp.yml" --config "aegis.config.with-dw.json" --verbose # aegis query search_system_object_attribute_groups '{"objectType": "Product", "searchRequest": {"query": {"match_all_query": {}}, "count": 3}}' --config "aegis.config.with-dw.json" # aegis query search_system_object_attribute_groups '{"objectType": "SitePreferences", "searchRequest": {"query": {"match_all_query": {}}, "count": 3}}' --config "aegis.config.with-dw.json" # ================================================================================== description: "search_system_object_attribute_groups tool smoke tests - Basic functionality validation" tests: # ================================================================================== # TOOL AVAILABILITY VALIDATION # ================================================================================== - it: "should have search_system_object_attribute_groups tool available with proper schema" request: jsonrpc: "2.0" id: "tool-available" method: "tools/list" params: {} expect: response: jsonrpc: "2.0" id: "tool-available" result: tools: match:arrayElements: match:partial: name: "match:type:string" description: "match:type:string" match:extractField: "tools.*.name" value: "match:arrayContains:search_system_object_attribute_groups" stderr: "toBeEmpty" # ================================================================================== # CORE FUNCTIONALITY VALIDATION - Product Object Type # ================================================================================== - it: "should successfully search Product attribute groups with match_all_query and return valid structure" request: jsonrpc: "2.0" id: "product-match-all-success" method: "tools/call" params: name: "search_system_object_attribute_groups" arguments: objectType: "Product" searchRequest: query: match_all_query: {} count: 5 expect: response: jsonrpc: "2.0" id: "product-match-all-success" result: content: - type: "text" text: "match:contains:object_attribute_group_search_result" isError: false performance: maxResponseTime: "2000ms" stderr: "toBeEmpty" - it: "should return valid JSON structure with attribute group data for Product" request: jsonrpc: "2.0" id: "product-structure" method: "tools/call" params: name: "search_system_object_attribute_groups" arguments: objectType: "Product" searchRequest: query: match_all_query: {} count: 3 expect: response: jsonrpc: "2.0" id: "product-structure" result: content: - type: "text" text: "match:regex:[\\s\\S]*\"_type\"[\\s\\S]*\"object_attribute_group_search_result\"[\\s\\S]*" isError: false stderr: "toBeEmpty" - it: "should include pagination metadata and query echo in Product search response" request: jsonrpc: "2.0" id: "product-pagination" method: "tools/call" params: name: "search_system_object_attribute_groups" arguments: objectType: "Product" searchRequest: query: match_all_query: {} count: 2 expect: response: jsonrpc: "2.0" id: "product-pagination" result: content: - type: "text" text: "match:contains:\"query\"" isError: false stderr: "toBeEmpty" - it: "should return attribute groups with id and link fields for Product" request: jsonrpc: "2.0" id: "product-hits-structure" method: "tools/call" params: name: "search_system_object_attribute_groups" arguments: objectType: "Product" searchRequest: query: match_all_query: {} count: 2 expect: response: jsonrpc: "2.0" id: "product-hits-structure" result: content: - type: "text" text: "match:regex:[\\s\\S]*\"hits\"[\\s\\S]*\"id\"[\\s\\S]*\"link\"[\\s\\S]*object_attribute_group[\\s\\S]*" isError: false stderr: "toBeEmpty" # ================================================================================== # SITE PREFERENCES FUNCTIONALITY - Essential for Site Preference Discovery # ================================================================================== - it: "should successfully search SitePreferences attribute groups (for site preference discovery)" request: jsonrpc: "2.0" id: "siteprefs-success" method: "tools/call" params: name: "search_system_object_attribute_groups" arguments: objectType: "SitePreferences" searchRequest: query: match_all_query: {} count: 3 expect: response: jsonrpc: "2.0" id: "siteprefs-success" result: content: - type: "text" text: "match:contains:object_attribute_group_search_result" isError: false performance: maxResponseTime: "2000ms" stderr: "toBeEmpty" - it: "should return SitePreferences groups containing expected groups like CCV or Storefront" request: jsonrpc: "2.0" id: "siteprefs-content" method: "tools/call" params: name: "search_system_object_attribute_groups" arguments: objectType: "SitePreferences" searchRequest: query: match_all_query: {} count: 5 expect: response: jsonrpc: "2.0" id: "siteprefs-content" result: content: - type: "text" text: "match:regex:[\\s\\S]*\"id\".*\"(?:CCV|Storefront|SFRA)[\\s\\S]*" isError: false stderr: "toBeEmpty" # ================================================================================== # TEXT SEARCH FUNCTIONALITY VALIDATION # ================================================================================== - it: "should support text_query search on id field" request: jsonrpc: "2.0" id: "text-search-id" method: "tools/call" params: name: "search_system_object_attribute_groups" arguments: objectType: "Product" searchRequest: query: text_query: fields: ["id"] search_phrase: "Order" count: 3 expect: response: jsonrpc: "2.0" id: "text-search-id" result: content: - type: "text" text: "match:contains:object_attribute_group_search_result" isError: false performance: maxResponseTime: "2000ms" stderr: "toBeEmpty" - it: "should include the text_query in response echo for text search" request: jsonrpc: "2.0" id: "text-search-echo" method: "tools/call" params: name: "search_system_object_attribute_groups" arguments: objectType: "Product" searchRequest: query: text_query: fields: ["id", "display_name"] search_phrase: "External" count: 2 expect: response: jsonrpc: "2.0" id: "text-search-echo" result: content: - type: "text" text: "match:regex:[\\s\\S]*\"text_query\"[\\s\\S]*\"fields\"[\\s\\S]*\"search_phrase\"[\\s\\S]*\"External\"[\\s\\S]*" isError: false stderr: "toBeEmpty" # ================================================================================== # PAGINATION AND SORTING VALIDATION # ================================================================================== - it: "should support custom count parameter for pagination" request: jsonrpc: "2.0" id: "custom-count" method: "tools/call" params: name: "search_system_object_attribute_groups" arguments: objectType: "Product" searchRequest: query: match_all_query: {} count: 2 expect: response: jsonrpc: "2.0" id: "custom-count" result: content: - type: "text" text: "match:regex:[\\s\\S]*\"count\":\\s*2[\\s\\S]*" isError: false stderr: "toBeEmpty" - it: "should support start parameter for pagination offset" request: jsonrpc: "2.0" id: "pagination-start" method: "tools/call" params: name: "search_system_object_attribute_groups" arguments: objectType: "Product" searchRequest: query: match_all_query: {} start: 2 count: 3 expect: response: jsonrpc: "2.0" id: "pagination-start" result: content: - type: "text" text: "match:regex:[\\s\\S]*\"start\":\\s*2[\\s\\S]*" isError: false stderr: "toBeEmpty" - it: "should support sorting by id field in ascending order" request: jsonrpc: "2.0" id: "sorting-id-asc" method: "tools/call" params: name: "search_system_object_attribute_groups" arguments: objectType: "Product" searchRequest: query: match_all_query: {} sorts: - field: "id" sort_order: "asc" count: 3 expect: response: jsonrpc: "2.0" id: "sorting-id-asc" result: content: - type: "text" text: "match:contains:object_attribute_group_search_result" isError: false performance: maxResponseTime: "2000ms" stderr: "toBeEmpty" # ================================================================================== # PARAMETER VALIDATION AND ERROR HANDLING # ================================================================================== - it: "should reject missing objectType parameter with validation error" request: jsonrpc: "2.0" id: "missing-object-type" method: "tools/call" params: name: "search_system_object_attribute_groups" arguments: searchRequest: query: match_all_query: {} count: 3 expect: response: jsonrpc: "2.0" id: "missing-object-type" result: content: - type: "text" text: "match:contains:objectType must be a non-empty string" isError: true performance: maxResponseTime: "800ms" stderr: "toBeEmpty" - it: "should reject empty objectType parameter with validation error" request: jsonrpc: "2.0" id: "empty-object-type" method: "tools/call" params: name: "search_system_object_attribute_groups" arguments: objectType: "" searchRequest: query: match_all_query: {} count: 3 expect: response: jsonrpc: "2.0" id: "empty-object-type" result: content: - type: "text" text: "match:contains:objectType must be a non-empty string" isError: true performance: maxResponseTime: "800ms" stderr: "toBeEmpty" - it: "should handle missing searchRequest parameter by defaulting to match_all_query (mock server behavior)" request: jsonrpc: "2.0" id: "missing-search-request" method: "tools/call" params: name: "search_system_object_attribute_groups" arguments: objectType: "Product" expect: response: jsonrpc: "2.0" id: "missing-search-request" result: content: - type: "text" text: "match:contains:object_attribute_group_search_result" isError: false performance: maxResponseTime: "2000ms" stderr: "toBeEmpty" - it: "should handle invalid objectType with OCAPI 404 error" request: jsonrpc: "2.0" id: "invalid-object-type" method: "tools/call" params: name: "search_system_object_attribute_groups" arguments: objectType: "InvalidObjectType" searchRequest: query: match_all_query: {} count: 3 expect: response: jsonrpc: "2.0" id: "invalid-object-type" result: content: - type: "text" text: "match:contains:ObjectTypeNotFoundException" isError: true performance: maxResponseTime: "2000ms" stderr: "toBeEmpty" - it: "should include specific error message for invalid object type" request: jsonrpc: "2.0" id: "invalid-object-type-message" method: "tools/call" params: name: "search_system_object_attribute_groups" arguments: objectType: "NonExistentObject" searchRequest: query: match_all_query: {} count: 2 expect: response: jsonrpc: "2.0" id: "invalid-object-type-message" result: content: - type: "text" text: "match:regex:[\\s\\S]*No object type with ID[\\s\\S]*NonExistentObject[\\s\\S]*could be found[\\s\\S]*" isError: true stderr: "toBeEmpty" # ================================================================================== # QUERY TYPE VALIDATION # ================================================================================== - it: "should support bool_query with must conditions" request: jsonrpc: "2.0" id: "bool-query-must" method: "tools/call" params: name: "search_system_object_attribute_groups" arguments: objectType: "Product" searchRequest: query: bool_query: must: - text_query: fields: ["id"] search_phrase: "Channel" count: 3 expect: response: jsonrpc: "2.0" id: "bool-query-must" result: content: - type: "text" text: "match:contains:object_attribute_group_search_result" isError: false performance: maxResponseTime: "2000ms" stderr: "toBeEmpty" - it: "should support term_query with exact matching" request: jsonrpc: "2.0" id: "term-query-exact" method: "tools/call" params: name: "search_system_object_attribute_groups" arguments: objectType: "Product" searchRequest: query: term_query: fields: ["id"] operator: "is" values: ["Order"] count: 3 expect: response: jsonrpc: "2.0" id: "term-query-exact" result: content: - type: "text" text: "match:contains:object_attribute_group_search_result" isError: false performance: maxResponseTime: "2000ms" stderr: "toBeEmpty" # ================================================================================== # PERFORMANCE VALIDATION # ================================================================================== - it: "should complete match_all_query search within reasonable time" request: jsonrpc: "2.0" id: "performance-match-all" method: "tools/call" params: name: "search_system_object_attribute_groups" arguments: objectType: "Product" searchRequest: query: match_all_query: {} count: 10 expect: response: jsonrpc: "2.0" id: "performance-match-all" result: content: - type: "text" text: "match:contains:object_attribute_group_search_result" isError: false performance: maxResponseTime: "1500ms" stderr: "toBeEmpty" - it: "should complete text search within reasonable time" request: jsonrpc: "2.0" id: "performance-text-search" method: "tools/call" params: name: "search_system_object_attribute_groups" arguments: objectType: "SitePreferences" searchRequest: query: text_query: fields: ["id", "display_name"] search_phrase: "Config" count: 5 expect: response: jsonrpc: "2.0" id: "performance-text-search" result: content: - type: "text" text: "match:contains:object_attribute_group_search_result" isError: false performance: maxResponseTime: "2000ms" stderr: "toBeEmpty" # ================================================================================== # COMPREHENSIVE FEATURE VALIDATION # ================================================================================== - it: "should support comprehensive search with all parameters (sorting, pagination, selection)" request: jsonrpc: "2.0" id: "comprehensive-search" method: "tools/call" params: name: "search_system_object_attribute_groups" arguments: objectType: "Product" searchRequest: query: match_all_query: {} sorts: - field: "id" sort_order: "desc" start: 0 count: 4 select: "(**)" expect: response: jsonrpc: "2.0" id: "comprehensive-search" result: content: - type: "text" text: "match:regex:[\\s\\S]*object_attribute_group_search_result[\\s\\S]*count.*4[\\s\\S]*" isError: false performance: maxResponseTime: "2000ms" stderr: "toBeEmpty" ``` -------------------------------------------------------------------------------- /tests/mcp/node/get-system-object-definition.full-mode.programmatic.test.js: -------------------------------------------------------------------------------- ```javascript /** * ================================================================================== * SFCC MCP Server - get_system_object_definition Tool Programmatic Tests (Full Mode) * Streamlined programmatic testing focused on tool functionality and critical workflows * * These tests require SFCC credentials and use the mock server for realistic testing * Focus on complex business logic, multi-step workflows, and advanced error handling * that cannot be effectively tested in YAML format * * Quick Test Commands: * node --test tests/mcp/node/get-system-object-definition.full-mode.programmatic.test.js * npm test -- --grep "get_system_object_definition" * ================================================================================== */ import { test, describe, before, after, beforeEach } from 'node:test'; import { strict as assert } from 'node:assert'; import { connect } from 'mcp-aegis'; describe('get_system_object_definition Tool - Full Mode Programmatic Tests', () => { let client; // Streamlined test data - focus on representative objects const coreObjectTypes = ['Product', 'Customer', 'Order']; const expectedSystemObjectFlags = { 'Product': { content_object: true, queryable: false, read_only: false }, 'Customer': { content_object: false, queryable: true, read_only: false }, 'Order': { content_object: false, queryable: true, read_only: true } }; before(async () => { client = await connect('./aegis.config.with-dw.json'); }); after(async () => { if (client?.connected) { await client.disconnect(); } }); beforeEach(() => { // CRITICAL: Clear all buffers to prevent test interference client.clearAllBuffers(); }); // ================================================================================== // CONSOLIDATED TOOL DISCOVERY AND FUNCTIONALITY // ================================================================================== describe('Tool Discovery and Core Functionality', () => { test('should discover tool with comprehensive schema and execute successfully', async () => { // Tool discovery with schema validation const tools = await client.listTools(); const systemObjectTool = tools.find(tool => tool.name === 'get_system_object_definition'); assert.ok(systemObjectTool, 'get_system_object_definition tool should be available in full mode'); assert.ok(systemObjectTool.description.includes('system object'), 'Description should mention system objects'); const schema = systemObjectTool.inputSchema; assert.equal(schema.type, 'object', 'Schema should be object type'); assert.ok(schema.properties?.objectType, 'Schema should have objectType property'); assert.equal(schema.properties.objectType.type, 'string', 'objectType should be string type'); assert.ok(schema.required.includes('objectType'), 'objectType should be required'); // Functional validation - execute tool successfully const result = await client.callTool('get_system_object_definition', { objectType: 'Product' }); assert.equal(result.isError, false, 'Should execute successfully with valid parameters'); assert.ok(result.content?.[0]?.text, 'Should return content'); // Validate response structure const objectData = JSON.parse(result.content[0].text); assertValidObjectDefinition(objectData, 'Product'); }); test('should validate tool context and availability with other system tools', async () => { const tools = await client.listTools(); const toolNames = tools.map(t => t.name); // Should have related system object tools assert.ok(toolNames.includes('get_system_object_definition'), 'Should have single object tool'); assert.ok(toolNames.includes('get_system_object_definitions'), 'Should have multi-object tool'); assert.ok(toolNames.includes('search_system_object_attribute_definitions'), 'Should have attribute search tool'); // Should have substantial tool count in full mode assert.ok(tools.length >= 30, `Should have many tools in full mode, got ${tools.length}`); }); }); // ================================================================================== // STREAMLINED BUSINESS LOGIC VALIDATION // ================================================================================== describe('Business Logic and System Object Validation', () => { test('should validate core SFCC object characteristics and flag logic', async () => { const results = new Map(); // Test core object types sequentially (streamlined from 5 to 3 objects) for (const objectType of coreObjectTypes) { const result = await client.callTool('get_system_object_definition', { objectType }); assert.equal(result.isError, false, `${objectType} should be retrieved successfully`); const objectData = JSON.parse(result.content[0].text); results.set(objectType, objectData); // Use helper function for comprehensive validation assertValidObjectDefinition(objectData, objectType); // Validate expected flags (resilient to mock data changes) const expectedFlags = expectedSystemObjectFlags[objectType]; if (expectedFlags) { assert.equal(objectData.content_object, expectedFlags.content_object, `${objectType} content_object should match expected value`); assert.equal(objectData.queryable, expectedFlags.queryable, `${objectType} queryable should match expected value`); assert.equal(objectData.read_only, expectedFlags.read_only, `${objectType} read_only should match expected value`); } } // Cross-object business rule validation const productData = results.get('Product'); const customerData = results.get('Customer'); const orderData = results.get('Order'); // Validate business rules (resilient assertions) assert.ok(productData.attribute_definition_count > 0, 'Product should have attributes'); assert.ok(customerData.attribute_definition_count > 0, 'Customer should have attributes'); assert.equal(orderData.read_only, true, 'Order should be read-only'); assert.equal(customerData.queryable, true, 'Customer should be queryable'); }); test('should validate localization structure and metadata consistency', async () => { const result = await client.callTool('get_system_object_definition', { objectType: 'Product' }); const objectData = JSON.parse(result.content[0].text); // Validate localization structure (flexible assertions) validateLocalizationStructure(objectData.display_name, 'display_name'); assert.ok(objectData.description?.default, 'Should have default description'); // Validate timestamps and metadata assert.ok(objectData.creation_date, 'Should have creation date'); assert.ok(objectData.last_modified, 'Should have last modified date'); assert.ok(!isNaN(Date.parse(objectData.creation_date)), 'Creation date should be valid'); assert.ok(!isNaN(Date.parse(objectData.last_modified)), 'Last modified should be valid'); }); }); // ================================================================================== // ESSENTIAL WORKFLOW VALIDATION // ================================================================================== describe('Multi-Tool Integration Workflows', () => { test('should support object discovery and analysis workflow', async () => { // Step 1: Get all system object definitions const allObjectsResult = await client.callTool('get_system_object_definitions', {}); assert.equal(allObjectsResult.isError, false, 'Should retrieve all object definitions'); const allObjects = JSON.parse(allObjectsResult.content[0].text); assert.ok(Array.isArray(allObjects.data), 'Should return array of objects'); assert.ok(allObjects.data.length > 0, 'Should have multiple object types'); // Step 2: Analyze representative objects in detail const analysisResults = []; const testObjects = ['Product', 'Customer']; // Streamlined from 3 to 2 objects for (const objectType of testObjects) { const detailResult = await client.callTool('get_system_object_definition', { objectType }); assert.equal(detailResult.isError, false, `Should get details for ${objectType}`); const detailData = JSON.parse(detailResult.content[0].text); analysisResults.push({ objectType, attributes: detailData.attribute_definition_count, groups: detailData.attribute_group_count, flags: { content_object: detailData.content_object, queryable: detailData.queryable, read_only: detailData.read_only } }); } // Step 3: Validate workflow results assert.equal(analysisResults.length, 2, 'Should analyze expected objects'); // At least one should be queryable const queryableCount = analysisResults.filter(r => r.flags.queryable).length; assert.ok(queryableCount > 0, 'Should have at least one queryable object (Customer)'); }); test('should support attribute discovery and cross-tool integration', async () => { // Step 1: Get object definition for context const productResult = await client.callTool('get_system_object_definition', { objectType: 'Product' }); assert.equal(productResult.isError, false, 'Should get Product definition successfully'); // Step 2: Search for Product attributes using integration tool const attributeSearchResult = await client.callTool('search_system_object_attribute_definitions', { objectType: 'Product', searchRequest: { query: { match_all_query: {} }, count: 10 // Reduced from 20 to 10 for efficiency } }); assert.equal(attributeSearchResult.isError, false, 'Should search attributes successfully'); const attributeData = JSON.parse(attributeSearchResult.content[0].text); // Step 3: Validate cross-tool consistency assert.ok(attributeData.hits !== undefined, 'Should have search results structure'); assert.ok(attributeData.total >= 0, 'Should have reasonable total count'); // Validate attribute structure if results exist if (attributeData.hits.length > 0) { const sampleAttribute = attributeData.hits[0]; assert.ok(sampleAttribute.id, 'Attribute should have ID'); assert.ok(sampleAttribute._type === 'object_attribute_definition', 'Should have correct type'); } }); }); // ================================================================================== // COMPREHENSIVE ERROR HANDLING // ================================================================================== describe('Advanced Error Handling and Edge Cases', () => { test('should handle comprehensive parameter validation scenarios', async () => { const invalidCases = [ { args: {}, description: 'missing objectType', shouldError: true }, { args: { objectType: null }, description: 'null objectType', shouldError: true }, { args: { objectType: 123 }, description: 'numeric objectType', shouldError: true }, { args: { objectType: '' }, description: 'empty objectType', shouldError: true }, { args: { objectType: 'NonExistentObject' }, description: 'unknown object', shouldError: false }, // Fallback response { args: { objectType: 'Product123' }, description: 'object with numbers', shouldError: false }, // Fallback response ]; for (const testCase of invalidCases) { const result = await client.callTool('get_system_object_definition', testCase.args); if (testCase.shouldError) { assert.equal(result.isError, true, `Should return error for ${testCase.description}`); assert.ok(result.content[0].text.includes('Error'), `Error message should contain 'Error' for ${testCase.description}`); } else { // Fallback responses are acceptable for unknown object types if (result.isError) { assert.ok(result.content[0].text.includes('Error'), `Should have error message for ${testCase.description}`); } else { const data = JSON.parse(result.content[0].text); assert.ok(data.object_type, 'Should have fallback object type'); } } } }); test('should maintain performance and reliability under moderate load', async () => { const testObjects = ['Product', 'Customer', 'Order']; const results = []; const startTime = Date.now(); // Streamlined stress test (reduced from 10 to 5 iterations) for (let i = 0; i < 5; i++) { const objectType = testObjects[i % testObjects.length]; const iterationStart = Date.now(); const result = await client.callTool('get_system_object_definition', { objectType }); const iterationTime = Date.now() - iterationStart; results.push({ iteration: i, objectType, success: !result.isError, responseTime: iterationTime }); assert.equal(result.isError, false, `Iteration ${i} should succeed for ${objectType}`); } const totalTime = Date.now() - startTime; const successRate = results.filter(r => r.success).length / results.length; // Functional validation focused on reliability assert.equal(successRate, 1.0, 'All stress test iterations should succeed'); assert.ok(totalTime < 20000, 'Load test should complete within 20 seconds'); // Validate response consistency const responseTimes = results.map(r => r.responseTime); const avgResponseTime = responseTimes.reduce((sum, time) => sum + time, 0) / responseTimes.length; assert.ok(avgResponseTime < 3000, 'Average response time should be reasonable'); }); }); // ================================================================================== // FOCUSED INTEGRATION VALIDATION // ================================================================================== describe('Integration and Context Validation', () => { test('should validate mock server integration and SFCC API compliance', async () => { const result = await client.callTool('get_system_object_definition', { objectType: 'Product' }); const data = JSON.parse(result.content[0].text); // Validate SFCC API compliance (essential checks only) assert.ok(data._v && data._v.match(/^\d+\.\d+$/), 'Should have valid API version format'); assert.ok(data.link && data.link.includes('/dw/data/'), 'Should have SFCC-style API link'); // Validate realistic data structure assert.ok(!isNaN(Date.parse(data.creation_date)), 'Should have valid creation date'); assert.ok(!isNaN(Date.parse(data.last_modified)), 'Should have valid last modified date'); assert.ok(data.attribute_definition_count >= 0, 'Should have realistic attribute count'); assert.ok(data.attribute_group_count >= 0, 'Should have realistic group count'); }); test('should validate cross-tool integration consistency', async () => { // Single object definition const singleResult = await client.callTool('get_system_object_definition', { objectType: 'Customer' }); const singleData = JSON.parse(singleResult.content[0].text); // Bulk object definitions (for context) const allResult = await client.callTool('get_system_object_definitions', {}); const allData = JSON.parse(allResult.content[0].text); // Validate integration consistency assert.equal(singleData.object_type, 'Customer', 'Should access Customer directly'); assert.ok(Array.isArray(allData.data), 'Bulk tool should return array'); // Find Customer in bulk results if present (pagination may affect this) const customerFromAll = allData.data.find(obj => obj.object_type === 'Customer'); if (customerFromAll) { assert.equal(singleData.object_type, customerFromAll.object_type, 'Object type should match between single and bulk operations'); } }); }); // ================================================================================== // HELPER FUNCTIONS // ================================================================================== function assertValidObjectDefinition(data, objectType) { assert.equal(data._type, 'object_type_definition', 'Should have correct type'); assert.equal(data.object_type, objectType, 'Should match requested object type'); assert.ok(data.display_name, 'Should have display name'); assert.ok(data.description, 'Should have description'); assert.ok(typeof data.attribute_definition_count === 'number', 'Should have attribute count'); assert.ok(typeof data.attribute_group_count === 'number', 'Should have group count'); assert.ok(typeof data.content_object === 'boolean', 'Should have content_object flag'); assert.ok(typeof data.queryable === 'boolean', 'Should have queryable flag'); assert.ok(typeof data.read_only === 'boolean', 'Should have read_only flag'); } function validateLocalizationStructure(localizationObject, fieldName) { assert.ok(localizationObject, `Should have ${fieldName}`); assert.ok(typeof localizationObject === 'object', `${fieldName} should be object`); assert.ok(localizationObject.default, `${fieldName} should have default value`); const locales = Object.keys(localizationObject); assert.ok(locales.length >= 1, `${fieldName} should have at least default locale`); locales.forEach(locale => { assert.ok(typeof localizationObject[locale] === 'string', `${fieldName}.${locale} should be string`); assert.ok(localizationObject[locale].length > 0, `${fieldName}.${locale} should not be empty`); }); } }); // ================================================================================== // OPTIMIZATION NOTES // ================================================================================== // This programmatic test suite has been optimized for focused, essential testing. // // OPTIMIZATIONS APPLIED: // - Consolidated tool discovery tests (2 tests → 1 comprehensive test) // - Streamlined business logic validation (3 tests → 2 focused tests) // - Simplified multi-step workflows (3 tests → 2 essential workflows) // - Reduced stress testing iterations (10 → 5 iterations) // - Consolidated error handling patterns (3 tests → 2 comprehensive tests) // - Focused integration testing (3 tests → 2 targeted tests) // - Removed mock-data-specific assertions for resilience // - Reduced object type testing (5 objects → 3 core objects) // // TOTAL REDUCTION: 14 tests → 9 focused tests // FOCUS: Tool functionality, critical workflows, error handling // RESILIENCE: Tests adapt to mock data changes // EFFICIENCY: Reduced API calls while maintaining comprehensive coverage // ================================================================================== ``` -------------------------------------------------------------------------------- /docs/dw_customer/Profile.md: -------------------------------------------------------------------------------- ```markdown ## Package: dw.customer # Class Profile ## Inheritance Hierarchy - Object - dw.object.PersistentObject - dw.object.ExtensibleObject - dw.customer.EncryptedObject - dw.customer.Profile ## Description The class represents a customer profile. It also provides access to the customers address book and credentials. Note: this class handles sensitive security-related data. Pay special attention to PCI DSS v3. requirements 2, 4, and 12. ## Properties ### addressBook **Type:** AddressBook (Read Only) The customer's address book. ### birthday **Type:** Date The customer's birthday as a date. ### companyName **Type:** String The customer's company name. ### credentials **Type:** Credentials (Read Only) The customer's credentials. ### customer **Type:** Customer (Read Only) The customer object related to this profile. ### customerNo **Type:** String (Read Only) The customer's number, which is a number used to identify the Customer. ### email **Type:** String The customer's email address. ### fax **Type:** String The fax number to use for the customer. The length is restricted to 32 characters. ### female **Type:** boolean (Read Only) Indicates that the customer is female when set to true. ### firstName **Type:** String The customer's first name. ### gender **Type:** EnumValue The customer's gender. ### jobTitle **Type:** String The customer's job title. ### lastLoginTime **Type:** Date (Read Only) The last login time of the customer. ### lastName **Type:** String The customer's last name. ### lastVisitTime **Type:** Date (Read Only) The last visit time of the customer. ### male **Type:** boolean (Read Only) Indicates that the customer is male when set to true. ### nextBirthday **Type:** Date (Read Only) The upcoming customer's birthday as a date. If the customer already had birthday this year the method returns the birthday of the next year. Otherwise its birthday in this year. If the customer has not set a birthday this method returns null. ### phoneBusiness **Type:** String The business phone number to use for the customer. ### phoneHome **Type:** String The phone number to use for the customer. ### phoneMobile **Type:** String The mobile phone number to use for the customer. ### preferredLocale **Type:** String The customer's preferred locale. ### previousLoginTime **Type:** Date (Read Only) The time the customer logged in prior to the current login. ### previousVisitTime **Type:** Date (Read Only) The time the customer visited the store prior to the current visit. ### salutation **Type:** String The salutation to use for the customer. ### secondName **Type:** String The customer's second name. ### suffix **Type:** String The customer's suffix, such as "Jr." or "Sr.". ### taxID **Type:** String The tax ID value. The value is returned either plain text if the current context allows plain text access, or if it's not allowed, the ID value will be returned masked. The following criteria must be met in order to have plain text access: the method call must happen in the context of a storefront request; the current customer must be registered and authenticated; it is the profile of the current customer; and the current protocol is HTTPS. ### taxIDMasked **Type:** String (Read Only) The masked value of the tax ID. ### taxIDType **Type:** EnumValue The tax ID type. ### title **Type:** String The customer's title, such as "Mrs" or "Mr". ### wallet **Type:** Wallet (Read Only) The wallet of this customer. ## Constructor Summary ## Method Summary ### getAddressBook **Signature:** `getAddressBook() : AddressBook` Returns the customer's address book. ### getBirthday **Signature:** `getBirthday() : Date` Returns the customer's birthday as a date. ### getCompanyName **Signature:** `getCompanyName() : String` Returns the customer's company name. ### getCredentials **Signature:** `getCredentials() : Credentials` Returns the customer's credentials. ### getCustomer **Signature:** `getCustomer() : Customer` Returns the customer object related to this profile. ### getCustomerNo **Signature:** `getCustomerNo() : String` Returns the customer's number, which is a number used to identify the Customer. ### getEmail **Signature:** `getEmail() : String` Returns the customer's email address. ### getFax **Signature:** `getFax() : String` Returns the fax number to use for the customer. ### getFirstName **Signature:** `getFirstName() : String` Returns the customer's first name. ### getGender **Signature:** `getGender() : EnumValue` Returns the customer's gender. ### getJobTitle **Signature:** `getJobTitle() : String` Returns the customer's job title. ### getLastLoginTime **Signature:** `getLastLoginTime() : Date` Returns the last login time of the customer. ### getLastName **Signature:** `getLastName() : String` Returns the customer's last name. ### getLastVisitTime **Signature:** `getLastVisitTime() : Date` Returns the last visit time of the customer. ### getNextBirthday **Signature:** `getNextBirthday() : Date` Returns the upcoming customer's birthday as a date. ### getPhoneBusiness **Signature:** `getPhoneBusiness() : String` Returns the business phone number to use for the customer. ### getPhoneHome **Signature:** `getPhoneHome() : String` Returns the phone number to use for the customer. ### getPhoneMobile **Signature:** `getPhoneMobile() : String` Returns the mobile phone number to use for the customer. ### getPreferredLocale **Signature:** `getPreferredLocale() : String` Returns the customer's preferred locale. ### getPreviousLoginTime **Signature:** `getPreviousLoginTime() : Date` Returns the time the customer logged in prior to the current login. ### getPreviousVisitTime **Signature:** `getPreviousVisitTime() : Date` Returns the time the customer visited the store prior to the current visit. ### getSalutation **Signature:** `getSalutation() : String` Returns the salutation to use for the customer. ### getSecondName **Signature:** `getSecondName() : String` Returns the customer's second name. ### getSuffix **Signature:** `getSuffix() : String` Returns the customer's suffix, such as "Jr." or "Sr.". ### getTaxID **Signature:** `getTaxID() : String` Returns the tax ID value. ### getTaxIDMasked **Signature:** `getTaxIDMasked() : String` Returns the masked value of the tax ID. ### getTaxIDType **Signature:** `getTaxIDType() : EnumValue` Returns the tax ID type. ### getTitle **Signature:** `getTitle() : String` Returns the customer's title, such as "Mrs" or "Mr". ### getWallet **Signature:** `getWallet() : Wallet` Returns the wallet of this customer. ### isFemale **Signature:** `isFemale() : boolean` Indicates that the customer is female when set to true. ### isMale **Signature:** `isMale() : boolean` Indicates that the customer is male when set to true. ### setBirthday **Signature:** `setBirthday(aValue : Date) : void` Sets the customer's birthday as a date. ### setCompanyName **Signature:** `setCompanyName(aValue : String) : void` Sets the customer's company name. ### setEmail **Signature:** `setEmail(aValue : String) : void` Sets the customer's email address. ### setFax **Signature:** `setFax(number : String) : void` Sets the fax number to use for the customer. ### setFirstName **Signature:** `setFirstName(aValue : String) : void` Sets the customer's first name. ### setGender **Signature:** `setGender(aValue : Number) : void` Sets the customer's gender. ### setJobTitle **Signature:** `setJobTitle(aValue : String) : void` Sets the customer's job title. ### setLastName **Signature:** `setLastName(aValue : String) : void` Sets the customer's last name. ### setPhoneBusiness **Signature:** `setPhoneBusiness(number : String) : void` Sets the business phone number to use for the customer. ### setPhoneHome **Signature:** `setPhoneHome(number : String) : void` Sets the phone number to use for the customer. ### setPhoneMobile **Signature:** `setPhoneMobile(number : String) : void` Sets the mobile phone number to use for the customer. ### setPreferredLocale **Signature:** `setPreferredLocale(aValue : String) : void` Sets the customer's preferred locale. ### setSaluation **Signature:** `setSaluation(salutation : String) : void` Sets the salutation to use for the customer. ### setSalutation **Signature:** `setSalutation(salutation : String) : void` Sets the salutation to use for the customer. ### setSecondName **Signature:** `setSecondName(aValue : String) : void` Sets the customer's second name. ### setSuffix **Signature:** `setSuffix(aValue : String) : void` Sets the the customer's suffix. ### setTaxID **Signature:** `setTaxID(taxID : String) : void` Sets the tax ID value. ### setTaxIDType **Signature:** `setTaxIDType(taxIdType : String) : void` Sets the tax ID type. ### setTitle **Signature:** `setTitle(aValue : String) : void` Sets the customer's title. ## Method Detail ## Method Details ### getAddressBook **Signature:** `getAddressBook() : AddressBook` **Description:** Returns the customer's address book. **Returns:** the customer's address book. --- ### getBirthday **Signature:** `getBirthday() : Date` **Description:** Returns the customer's birthday as a date. **Returns:** the customer's birthday as a date. --- ### getCompanyName **Signature:** `getCompanyName() : String` **Description:** Returns the customer's company name. **Returns:** the customer's company name. --- ### getCredentials **Signature:** `getCredentials() : Credentials` **Description:** Returns the customer's credentials. **Returns:** the customer's credentials. --- ### getCustomer **Signature:** `getCustomer() : Customer` **Description:** Returns the customer object related to this profile. **Returns:** customer object related to profile. --- ### getCustomerNo **Signature:** `getCustomerNo() : String` **Description:** Returns the customer's number, which is a number used to identify the Customer. **Returns:** the customer's number. --- ### getEmail **Signature:** `getEmail() : String` **Description:** Returns the customer's email address. **Returns:** the customer's email address. --- ### getFax **Signature:** `getFax() : String` **Description:** Returns the fax number to use for the customer. The length is restricted to 32 characters. **Returns:** the fax mobile phone number to use for the customer. --- ### getFirstName **Signature:** `getFirstName() : String` **Description:** Returns the customer's first name. **Returns:** the customer's first name. --- ### getGender **Signature:** `getGender() : EnumValue` **Description:** Returns the customer's gender. **Returns:** the customer's gender. --- ### getJobTitle **Signature:** `getJobTitle() : String` **Description:** Returns the customer's job title. **Returns:** the customer's job title. --- ### getLastLoginTime **Signature:** `getLastLoginTime() : Date` **Description:** Returns the last login time of the customer. **Returns:** the time, when the customer was last logged in. --- ### getLastName **Signature:** `getLastName() : String` **Description:** Returns the customer's last name. **Returns:** the customer's last name. --- ### getLastVisitTime **Signature:** `getLastVisitTime() : Date` **Description:** Returns the last visit time of the customer. **Returns:** the time, when the customer has visited the storefront the last time (with enabled remember me functionality). --- ### getNextBirthday **Signature:** `getNextBirthday() : Date` **Description:** Returns the upcoming customer's birthday as a date. If the customer already had birthday this year the method returns the birthday of the next year. Otherwise its birthday in this year. If the customer has not set a birthday this method returns null. **Returns:** the customer's next birthday as a date. --- ### getPhoneBusiness **Signature:** `getPhoneBusiness() : String` **Description:** Returns the business phone number to use for the customer. **Returns:** the business phone number to use for the customer. --- ### getPhoneHome **Signature:** `getPhoneHome() : String` **Description:** Returns the phone number to use for the customer. **Returns:** the phone number to use for the customer. --- ### getPhoneMobile **Signature:** `getPhoneMobile() : String` **Description:** Returns the mobile phone number to use for the customer. **Returns:** the mobile phone number to use for the customer. --- ### getPreferredLocale **Signature:** `getPreferredLocale() : String` **Description:** Returns the customer's preferred locale. **Returns:** the customer's preferred locale. --- ### getPreviousLoginTime **Signature:** `getPreviousLoginTime() : Date` **Description:** Returns the time the customer logged in prior to the current login. **Returns:** the time the customer logged in prior to the current login. --- ### getPreviousVisitTime **Signature:** `getPreviousVisitTime() : Date` **Description:** Returns the time the customer visited the store prior to the current visit. **Returns:** the time the customer visited the store prior to the current visit. --- ### getSalutation **Signature:** `getSalutation() : String` **Description:** Returns the salutation to use for the customer. **Returns:** the salutation to use for the customer. --- ### getSecondName **Signature:** `getSecondName() : String` **Description:** Returns the customer's second name. **Returns:** the customer's second name. --- ### getSuffix **Signature:** `getSuffix() : String` **Description:** Returns the customer's suffix, such as "Jr." or "Sr.". **Returns:** the customer's suffix. --- ### getTaxID **Signature:** `getTaxID() : String` **Description:** Returns the tax ID value. The value is returned either plain text if the current context allows plain text access, or if it's not allowed, the ID value will be returned masked. The following criteria must be met in order to have plain text access: the method call must happen in the context of a storefront request; the current customer must be registered and authenticated; it is the profile of the current customer; and the current protocol is HTTPS. **Returns:** the tax ID value --- ### getTaxIDMasked **Signature:** `getTaxIDMasked() : String` **Description:** Returns the masked value of the tax ID. **Returns:** the masked value of the tax ID --- ### getTaxIDType **Signature:** `getTaxIDType() : EnumValue` **Description:** Returns the tax ID type. **Returns:** the tax ID type --- ### getTitle **Signature:** `getTitle() : String` **Description:** Returns the customer's title, such as "Mrs" or "Mr". **Returns:** the customer's title. --- ### getWallet **Signature:** `getWallet() : Wallet` **Description:** Returns the wallet of this customer. **Returns:** the wallet of this customer. --- ### isFemale **Signature:** `isFemale() : boolean` **Description:** Indicates that the customer is female when set to true. **Returns:** true if the customer is a female, false otherwise. --- ### isMale **Signature:** `isMale() : boolean` **Description:** Indicates that the customer is male when set to true. **Returns:** true if the customer is a male, false otherwise. --- ### setBirthday **Signature:** `setBirthday(aValue : Date) : void` **Description:** Sets the customer's birthday as a date. **Parameters:** - `aValue`: the customer's birthday as a date. --- ### setCompanyName **Signature:** `setCompanyName(aValue : String) : void` **Description:** Sets the customer's company name. **Parameters:** - `aValue`: the customer's company name. --- ### setEmail **Signature:** `setEmail(aValue : String) : void` **Description:** Sets the customer's email address. **Parameters:** - `aValue`: the customer's email address. --- ### setFax **Signature:** `setFax(number : String) : void` **Description:** Sets the fax number to use for the customer. The length is restricted to 32 characters. **Parameters:** - `number`: the fax number to use for the customer. --- ### setFirstName **Signature:** `setFirstName(aValue : String) : void` **Description:** Sets the customer's first name. **Parameters:** - `aValue`: the customer's first name. --- ### setGender **Signature:** `setGender(aValue : Number) : void` **Description:** Sets the customer's gender. **Parameters:** - `aValue`: the customer's gender. --- ### setJobTitle **Signature:** `setJobTitle(aValue : String) : void` **Description:** Sets the customer's job title. **Parameters:** - `aValue`: the customer's job title. --- ### setLastName **Signature:** `setLastName(aValue : String) : void` **Description:** Sets the customer's last name. **Parameters:** - `aValue`: the customer's last name. --- ### setPhoneBusiness **Signature:** `setPhoneBusiness(number : String) : void` **Description:** Sets the business phone number to use for the customer. The length is restricted to 32 characters. **Parameters:** - `number`: the business phone number to use for the customer. --- ### setPhoneHome **Signature:** `setPhoneHome(number : String) : void` **Description:** Sets the phone number to use for the customer. The length is restricted to 32 characters. **Parameters:** - `number`: the phone number to use for the customer. --- ### setPhoneMobile **Signature:** `setPhoneMobile(number : String) : void` **Description:** Sets the mobile phone number to use for the customer. The length is restricted to 32 characters. **Parameters:** - `number`: the mobile phone number to use for the customer. --- ### setPreferredLocale **Signature:** `setPreferredLocale(aValue : String) : void` **Description:** Sets the customer's preferred locale. **Parameters:** - `aValue`: the customer's preferred locale. --- ### setSaluation **Signature:** `setSaluation(salutation : String) : void` **Description:** Sets the salutation to use for the customer. **Deprecated:** Use setSalutation(String) **Parameters:** - `salutation`: the salutation to use for the customer. --- ### setSalutation **Signature:** `setSalutation(salutation : String) : void` **Description:** Sets the salutation to use for the customer. **Parameters:** - `salutation`: the salutation to use for the customer. --- ### setSecondName **Signature:** `setSecondName(aValue : String) : void` **Description:** Sets the customer's second name. **Parameters:** - `aValue`: the customer's second name. --- ### setSuffix **Signature:** `setSuffix(aValue : String) : void` **Description:** Sets the the customer's suffix. **Parameters:** - `aValue`: the customer's suffix. --- ### setTaxID **Signature:** `setTaxID(taxID : String) : void` **Description:** Sets the tax ID value. The value can be set if the current context allows write access. The current context allows write access if the currently logged in user owns this profile and the connection is secured. **Parameters:** - `taxID`: the tax ID value to set --- ### setTaxIDType **Signature:** `setTaxIDType(taxIdType : String) : void` **Description:** Sets the tax ID type. **Parameters:** - `taxIdType`: the tax ID type to set --- ### setTitle **Signature:** `setTitle(aValue : String) : void` **Description:** Sets the customer's title. **Parameters:** - `aValue`: the customer's title. --- ``` -------------------------------------------------------------------------------- /docs-site/pages/FeaturesPage.tsx: -------------------------------------------------------------------------------- ```typescript import React from 'react'; import { NavLink } from 'react-router-dom'; import SEO from '../components/SEO'; import BreadcrumbSchema from '../components/BreadcrumbSchema'; import StructuredData from '../components/StructuredData'; import { H1, PageSubtitle, H2, H3 } from '../components/Typography'; import { Collapsible } from '../components/Collapsible'; import { SITE_DATES } from '../constants'; const badge = (label: string) => ( <span className="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium mr-2 mb-2 bg-slate-100 text-slate-700 border border-slate-200">{label}</span> ); const FeaturesPage: React.FC = () => { const featuresStructuredData = { "@context": "https://schema.org", "@type": "TechArticle", "headline": "Features & Capabilities - SFCC Development MCP Server", "description": "Comprehensive overview of SFCC Development MCP Server features: documentation access, real-time log & job log analysis, system & custom object exploration, site preferences, cartridge generation, best practices, and AI-powered development tools.", "author": { "@type": "Person", "name": "Thomas Theunen" }, "publisher": { "@type": "Person", "name": "Thomas Theunen" }, "datePublished": SITE_DATES.PUBLISHED, "dateModified": SITE_DATES.MODIFIED, "url": "https://sfcc-mcp-dev.rhino-inquisitor.com/features/", "about": [ { "@type": "SoftwareApplication", "name": "SFCC Development MCP Server", "applicationCategory": "DeveloperApplication", "operatingSystem": "Node.js", "offers": { "@type": "Offer", "price": "0", "priceCurrency": "USD", "availability": "https://schema.org/InStock" } } ], "mainEntity": { "@type": "SoftwareApplication", "name": "SFCC Development MCP Server", "applicationCategory": "DeveloperApplication", "operatingSystem": "Node.js", "offers": { "@type": "Offer", "price": "0", "priceCurrency": "USD", "availability": "https://schema.org/InStock" }, "featureList": [ "SFCC API Documentation Access", "Real-time Log & Job Log Analysis", "System & Custom Object Exploration", "Cartridge Generation", "Best Practices Guides", "AI Assistant Integration" ] } }; return ( <div className="max-w-6xl mx-auto px-6 py-12"> <SEO title="Features & Capabilities" description="Comprehensive overview of SFCC Development MCP Server features: documentation access, real-time log & job log analysis, system & custom object exploration, site preferences, cartridge generation, best practices, and AI-powered development tools." keywords="SFCC MCP features, Commerce Cloud development tools, SFCC documentation access, log analysis tools, system object tools, cartridge generation, SFCC best practices, AI development features" canonical="/features/" ogType="article" /> <BreadcrumbSchema items={[ { name: "Home", url: "/" }, { name: "Features", url: "/features/" } ]} /> <StructuredData structuredData={featuresStructuredData} /> <header className="mb-14 text-center"> <div className="inline-flex items-center gap-2 bg-gradient-to-r from-blue-600 to-purple-600 text-white px-4 py-2 rounded-full text-sm font-medium mb-6"> <svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20"><path fillRule="evenodd" d="M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z" clipRule="evenodd"/></svg> Feature Surface </div> <H1 id="features" className="text-5xl md:text-6xl font-extrabold bg-gradient-to-r from-gray-900 via-blue-900 to-purple-900 bg-clip-text text-transparent mb-6">Discover & Apply Faster</H1> <PageSubtitle className="text-xl md:text-2xl text-gray-600 max-w-4xl mx-auto leading-relaxed"> Lean overview of every capability—expand only what you need. Optimized for AI-assisted flows and rapid onboarding. </PageSubtitle> </header> <div className="flex flex-wrap gap-2 mt-2"> {badge('Docs')} {badge('SFRA')} {badge('Best Practices')} {badge('Logs')} {badge('Jobs')} {badge('Objects')} {badge('Code Versions')} {badge('Security')} </div> <div className="mb-6 rounded-lg border border-blue-200 bg-blue-50 p-4 text-sm text-blue-800"> <strong className="block font-semibold text-blue-900">Getting started</strong> <ul className="mt-2 list-disc pl-5 space-y-1"> <li>Generate a cartridge with <code>generate_cartridge_structure</code></li> <li>Or ask your AI assistant: <em>"Show me dw.catalog.Product methods"</em></li> </ul> <p className="mt-3 m-0 text-blue-800">Ready for deeper insight? Expand the sections below.</p> </div> {/* DEEP DIVES */} <section className="space-y-8" aria-label="Feature deep dives"> <Collapsible id="cartridge-generation" title="🚀 Cartridge Generation" defaultOpen> <p className="text-sm">Create a production-ready SFCC cartridge (or full project) in seconds via <code>generate_cartridge_structure</code>.</p> <ul className="list-disc pl-5 text-sm space-y-1"> <li>Controllers, models, ISML templates, static assets, config, lint, build</li> <li>Full project mode adds package.json, Webpack, linting, scripts</li> <li>Direct file creation in specified target path (no manual copy)</li> <li>Consistent patterns for AI to reason about project shape</li> </ul> <div className="mt-4 text-xs text-slate-600"> <p className="font-semibold mb-1 text-slate-700">Try asking:</p> <ul className="list-disc pl-4 space-y-1"> <li>"Generate a cartridge named <em>training_core</em> and show the created directories."</li> <li>"What files does generate_cartridge_structure create in full project mode?"</li> <li>"Add a controller skeleton to my generated cartridge for listing products."</li> </ul> </div> </Collapsible> <Collapsible id="sfcc-best-practices-guides" title="📚 Best Practices, Guides & Hook References"> <p className="text-sm mb-2">Structured guidance across 13 core development domains plus searchable OCAPI / SCAPI hook reference tables with extension point signatures.</p> <div className="grid sm:grid-cols-2 gap-4 text-sm"> <div> <h4 className="font-semibold mb-1">Guides</h4> <ul className="list-disc pl-4 space-y-1"> <li>Cartridge creation & architecture</li> <li>ISML templates (security & performance)</li> <li>Job framework patterns</li> <li>LocalServiceRegistry integrations</li> <li>SFRA controllers & models</li> <li>SFRA client-side JavaScript architecture</li> <li>SFRA SCSS theming & override discipline</li> <li>Custom SCAPI endpoints</li> </ul> </div> <div> <h4 className="font-semibold mb-1">Also Covers</h4> <ul className="list-disc pl-4 space-y-1"> <li>OCAPI & SCAPI hooks</li> <li>Security hardening (OWASP)</li> <li>Performance & scalability tactics</li> <li>Search across all guides</li> <li>Hook reference tables (OCAPI / SCAPI)</li> </ul> </div> </div> <div className="mt-4 text-xs text-slate-600"> <p className="font-semibold mb-1 text-slate-700">Try asking:</p> <ul className="list-disc pl-4 space-y-1"> <li>"Show me security recommendations for ISML templates."</li> <li>"List OCAPI hook points for product import and explain when to use each."</li> <li>"Summarize performance best practices relevant to checkout."</li> </ul> </div> </Collapsible> <Collapsible id="sfcc-documentation" title="🔍 SFCC API Documentation"> <p className="text-sm">Query the entire <code>dw.*</code> namespace surface area.</p> <ul className="list-disc pl-5 text-sm space-y-1"> <li>Get class info (properties, methods, descriptions)</li> <li>Search classes & methods with partial matching</li> <li>List all classes for discovery</li> <li>Retrieve raw markdown documentation</li> </ul> <div className="mt-4 text-xs text-slate-600"> <p className="font-semibold mb-1 text-slate-700">Try asking:</p> <ul className="list-disc pl-4 space-y-1"> <li>"What methods does dw.catalog.Product have for pricing?"</li> <li>"Search SFCC classes related to inventory."</li> <li>"Show raw documentation for dw.system.Transaction."</li> </ul> </div> </Collapsible> <Collapsible id="sfra-docs" title="🏗️ Enhanced SFRA Documentation"> <p className="text-sm">26+ documents with smart categorization (core, product, order, customer, pricing, store, other).</p> <ul className="list-disc pl-5 text-sm space-y-1"> <li>Relevance-scored search with highlighted context</li> <li>Category filtering and model coverage</li> <li>Core classes: server, request, response, querystring, render</li> <li>Extensive product/order/customer/pricing/store models</li> </ul> <div className="mt-4 text-xs text-slate-600"> <p className="font-semibold mb-1 text-slate-700">Try asking:</p> <ul className="list-disc pl-4 space-y-1"> <li>"Search SFRA docs for middleware examples."</li> <li>"Show differences between product-full and product-tile models."</li> <li>"List order-related SFRA documents and summarize each."</li> </ul> </div> </Collapsible> <Collapsible id="logs" title="📊 Log & Job Log Analysis"> <p className="text-sm mb-2">Real-time visibility into runtime behaviour plus deep job execution insight (multi-level logs in single files).</p> <div className="grid sm:grid-cols-2 gap-4 text-sm"> <div> <h4 className="font-semibold mb-1">Runtime Logs</h4> <ul className="list-disc pl-4 space-y-1"> <li>Latest error / warn / info / debug</li> <li>Pattern search & daily summarization</li> <li>Full file listing & tail reads</li> <li>Health & recurrence analysis</li> </ul> </div> <div> <h4 className="font-semibold mb-1">Job Logs</h4> <ul className="list-disc pl-4 space-y-1"> <li>Latest job log files</li> <li>Search by job name or pattern</li> <li>Execution summary (steps, timings)</li> <li>Unified multi-level parsing</li> </ul> </div> </div> <div className="mt-3 text-xs text-amber-800 bg-amber-50 border border-amber-200 rounded px-3 py-2"> <strong className="font-semibold">Requires full mode:</strong> WebDAV / log tools need sandbox <code>username</code> & <code>password</code> (basic auth) plus any required realm access. </div> <div className="mt-4 text-xs text-slate-600"> <p className="font-semibold mb-1 text-slate-700">Try asking:</p> <ul className="list-disc pl-4 space-y-1"> <li>"Show the latest 10 error log entries containing 'OCAPI'."</li> <li>"Summarize today’s log health and top recurring issues."</li> <li>"Get the latest job execution summary for ProductFeedJob."</li> </ul> </div> </Collapsible> <Collapsible id="system-objects" title="⚙️ System & Custom Objects / Data Model"> <p className="text-sm">Explore system & custom object schemas, attributes, groups and site preferences with advanced querying.</p> <ul className="list-disc pl-5 text-sm space-y-1"> <li>List all system objects with metadata</li> <li>Attribute & group searches (boolean, text, sort)</li> <li>Custom object attribute discovery (targeted or match-all queries)</li> <li>Site preference group & value exploration</li> </ul> <div className="mt-3 text-xs text-amber-800 bg-amber-50 border border-amber-200 rounded px-3 py-2"> <strong className="font-semibold">Requires full mode:</strong> OCAPI system object & preference tools need <code>client_id</code> and <code>client_secret</code> with proper scopes (Data API + Shop as applicable). </div> <div className="mt-4 text-xs text-slate-600"> <p className="font-semibold mb-1 text-slate-700">Try asking:</p> <ul className="list-disc pl-4 space-y-1"> <li>"List custom attributes for Product containing 'brand'."</li> <li>"Search site preferences in the SEO group for description fields."</li> <li>"Show attribute definitions for custom object Global_String matching 'locale'."</li> </ul> </div> </Collapsible> <Collapsible id="code-versions" title="🔄 Code Version Management"> <p className="text-sm">Manage deployment states directly via MCP.</p> <ul className="list-disc pl-5 text-sm space-y-1"> <li>List available code versions</li> <li>Activate version to resolve endpoint or job issues</li> <li>AI can propose switches based on log diagnostics</li> </ul> <div className="mt-3 text-xs text-amber-800 bg-amber-50 border border-amber-200 rounded px-3 py-2"> <strong className="font-semibold">Requires full mode:</strong> Code version tools require OCAPI <code>client_id</code> and <code>client_secret</code> authorized for code/version management. </div> <div className="mt-4 text-xs text-slate-600"> <p className="font-semibold mb-1 text-slate-700">Try asking:</p> <ul className="list-disc pl-4 space-y-1"> <li>"List code versions and highlight the active one."</li> <li>"Activate code version <em>int_release_2025_09</em>."</li> <li>"Which inactive code versions look safe to remove?"</li> </ul> </div> </Collapsible> <Collapsible id="security-performance" title="🛡️ Security & Performance"> <div className="grid sm:grid-cols-2 gap-6 text-sm"> <div> <h4 className="font-semibold mb-1">Security</h4> <ul className="list-disc pl-4 space-y-1"> <li>Credential isolation & never persisted plaintext</li> <li>Path traversal & input validation guards</li> <li>Scoped tool surface (principle of least privilege)</li> <li>Structured error sanitization</li> </ul> </div> <div> <h4 className="font-semibold mb-1">Performance</h4> <ul className="list-disc pl-4 space-y-1"> <li>Layered caching & deduplicated requests</li> <li>Chunked log tailing (range reads)</li> <li>Lazy loading of heavy docs</li> <li>Resource bounding for single-dev usage</li> </ul> </div> </div> </Collapsible> <Collapsible id="ai-integration" title="🤖 AI Integration Rationale"> <p className="text-sm">Designed so assistants like GitHub Copilot, Claude, and Cursor can produce higher-quality SFCC code with deterministic tool surfaces and enriched local context.</p> <ul className="list-disc pl-5 text-sm space-y-1"> <li>Deterministic tool naming & argument shapes</li> <li>High-signal, low-noise responses (agent friendly)</li> <li>Semantic grouping reduces prompt tokens</li> <li>Guides & raw docs unify knowledge surface</li> </ul> </Collapsible> </section> {/* NEXT STEPS (aligned with home/config CTA style) */} <section className="mt-20 mb-12 text-center" aria-labelledby="next-steps"> <H2 id="next-steps" className="text-3xl font-bold mb-4">🔗 Next Steps</H2> <p className="text-sm md:text-base text-gray-600 max-w-2xl mx-auto mb-8">Pick a direction—inspect the precise tool surface first or jump straight into multi-step usage patterns.</p> <div className="flex flex-col sm:flex-row gap-4 justify-center mb-10"> <NavLink to="/tools/" className="group bg-gradient-to-r from-blue-600 to-purple-600 text-white px-8 py-4 rounded-xl font-semibold text-lg shadow-lg hover:shadow-xl transition-all duration-300 transform hover:-translate-y-1 no-underline hover:no-underline focus:no-underline" > Browse Tools <span className="ml-2 group-hover:translate-x-1 inline-block transition-transform">→</span> </NavLink> <NavLink to="/examples/" className="border-2 border-gray-300 text-gray-700 px-8 py-4 rounded-xl font-semibold text-lg hover:border-blue-500 hover:text-blue-600 transition-all duration-300 no-underline hover:no-underline focus:no-underline" > Examples & Use Cases </NavLink> </div> <div className="grid md:grid-cols-2 gap-6 max-w-4xl mx-auto mb-8 text-left"> <div className="rounded-xl border border-gray-200 bg-white p-5"> <h3 className="font-semibold text-sm mb-2">Need exact tool names?</h3> <p className="text-xs text-gray-600">The tools catalog lists arguments & intent so you can craft precise, low-token prompts.</p> </div> <div className="rounded-xl border border-blue-200 bg-blue-50 p-5"> <h3 className="font-semibold text-sm mb-2 text-blue-800">Prefer guided flows?</h3> <p className="text-xs text-blue-800">Example sequences show chained usage (docs → logs → versions) ready to reuse.</p> </div> </div> <div className="mx-auto max-w-xl p-4 bg-blue-50 border border-blue-200 rounded-lg text-sm text-blue-800"> 💡 <strong>Tip:</strong> Ask: <em>"Suggest a workflow to inspect a product attribute then trace related log errors using available tools."</em> </div> </section> </div> ); }; export default FeaturesPage; ``` -------------------------------------------------------------------------------- /tests/mcp/node/get-available-best-practice-guides.docs-only.programmatic.test.js: -------------------------------------------------------------------------------- ```javascript /** * Programmatic tests for get_available_best_practice_guides tool * * These tests provide advanced verification capabilities beyond YAML pattern matching, * including dynamic validation, comprehensive content analysis, * and advanced error categorization for the SFCC best practice guides functionality. * * Response format discovered via aegis query: * - Success: { content: [{ type: "text", text: "[{\"name\":\"guide_name\",\"title\":\"Guide Title\",\"description\":\"...\"}]" }], isError: false } * - Always successful: No isError field or error conditions in docs-only mode * - Ignores extra parameters: Gracefully handles unexpected parameters * - Comprehensive: Returns 13 best practice guides across SFCC development areas */ import { test, describe, before, after, beforeEach } from 'node:test'; import { strict as assert } from 'node:assert'; import { connect } from 'mcp-aegis'; /** * Content analysis utility for SFCC best practice guides validation */ class BestPracticeAnalyzer { constructor() { // Expected best practice guides based on project structure this.expectedGuides = [ 'cartridge_creation', 'isml_templates', 'job_framework', 'localserviceregistry', 'ocapi_hooks', 'scapi_hooks', 'scapi_custom_endpoint', 'sfra_controllers', 'sfra_models', 'sfra_client_side_js', 'sfra_scss', 'performance', 'security' ]; // Critical guides that must be present this.criticalGuides = [ 'cartridge_creation', 'sfra_controllers', 'ocapi_hooks', 'scapi_hooks', 'sfra_client_side_js', 'sfra_scss', 'security', 'performance' ]; // Guide categories for validation this.guideCategories = { core: ['cartridge_creation', 'isml_templates', 'job_framework'], sfra: ['sfra_controllers', 'sfra_models', 'sfra_client_side_js', 'sfra_scss'], api_hooks: ['ocapi_hooks', 'scapi_hooks', 'scapi_custom_endpoint'], services: ['localserviceregistry'], quality: ['performance', 'security'] }; // Required fields for each guide this.requiredFields = ['name', 'title', 'description']; } analyzeGuideList(guideArray) { const analysis = { totalGuides: guideArray.length, foundGuides: new Set(), missingGuides: [], missingCriticalGuides: [], foundCriticalGuides: [], invalidGuides: [], duplicates: [], guidesByCategory: {}, validationErrors: [] }; // Initialize category counters Object.keys(this.guideCategories).forEach(category => { analysis.guidesByCategory[category] = []; }); // Analyze each guide const seenGuides = new Set(); guideArray.forEach((guide, index) => { // Validate guide structure const structureErrors = this.validateGuideStructure(guide, index); if (structureErrors.length > 0) { analysis.validationErrors.push(...structureErrors); analysis.invalidGuides.push(guide); return; } const guideName = guide.name; // Check for duplicates if (seenGuides.has(guideName)) { analysis.duplicates.push(guideName); return; } seenGuides.add(guideName); analysis.foundGuides.add(guideName); // Check critical guides if (this.criticalGuides.includes(guideName)) { analysis.foundCriticalGuides.push(guideName); } // Categorize guide let categorized = false; Object.entries(this.guideCategories).forEach(([category, guides]) => { if (guides.includes(guideName)) { analysis.guidesByCategory[category].push(guideName); categorized = true; } }); if (!categorized) { if (!analysis.guidesByCategory.other) { analysis.guidesByCategory.other = []; } analysis.guidesByCategory.other.push(guideName); } }); // Find missing guides analysis.missingGuides = this.expectedGuides.filter( guide => !analysis.foundGuides.has(guide) ); // Find missing critical guides analysis.missingCriticalGuides = this.criticalGuides.filter( guide => !analysis.foundCriticalGuides.includes(guide) ); return analysis; } validateGuideStructure(guide, index) { const errors = []; if (typeof guide !== 'object' || guide === null) { errors.push(`Guide at index ${index} is not an object`); return errors; } // Check required fields this.requiredFields.forEach(field => { if (!Object.prototype.hasOwnProperty.call(guide, field)) { errors.push(`Guide at index ${index} missing required field: ${field}`); } else if (typeof guide[field] !== 'string') { errors.push(`Guide at index ${index} field ${field} should be string, got ${typeof guide[field]}`); } else if (guide[field].trim().length === 0) { errors.push(`Guide at index ${index} field ${field} is empty`); } }); // Validate name format (should be lowercase with underscores) if (guide.name && !/^[a-z][a-z0-9_]*$/.test(guide.name)) { errors.push(`Guide at index ${index} has invalid name format: ${guide.name}`); } // Validate title format (should contain "Best Practices") if (guide.title && !guide.title.includes('Best Practices')) { errors.push(`Guide at index ${index} title should contain "Best Practices": ${guide.title}`); } // Validate description length (should be meaningful) if (guide.description && guide.description.length < 20) { errors.push(`Guide at index ${index} description too short: ${guide.description.length} chars`); } return errors; } validateCompleteness(analysis) { const issues = []; if (analysis.totalGuides < 13) { issues.push(`Guide count too low: ${analysis.totalGuides} (expected 13+)`); } if (analysis.missingGuides.length > 0) { issues.push(`Missing guides: ${analysis.missingGuides.join(', ')}`); } if (analysis.missingCriticalGuides.length > 0) { issues.push(`Missing critical guides: ${analysis.missingCriticalGuides.join(', ')}`); } if (analysis.duplicates.length > 0) { issues.push(`Duplicate guides found: ${analysis.duplicates.length}`); } if (analysis.invalidGuides.length > 0) { issues.push(`Invalid guides found: ${analysis.invalidGuides.length}`); } if (analysis.validationErrors.length > 0) { issues.push(`Validation errors: ${analysis.validationErrors.length}`); } return issues; } } /** * Helper function to validate MCP response structure */ function assertValidMCPResponse(result) { assert.ok(result.content, 'Response should have content'); assert.ok(Array.isArray(result.content), 'Content should be array'); assert.equal(typeof result.isError, 'boolean', 'isError should be boolean'); } describe('get_available_best_practice_guides Programmatic Tests', () => { let client; const bestPracticeAnalyzer = new BestPracticeAnalyzer(); before(async () => { client = await connect('./aegis.config.docs-only.json'); }); after(async () => { if (client?.connected) { await client.disconnect(); } }); beforeEach(() => { client.clearAllBuffers(); }); describe('Protocol Compliance', () => { test('should be properly connected to MCP server', async () => { assert.ok(client.connected, 'Client should be connected'); }); test('should have get_available_best_practice_guides tool available', async () => { const tools = await client.listTools(); const guidesTool = tools.find(tool => tool.name === 'get_available_best_practice_guides'); assert.ok(guidesTool, 'get_available_best_practice_guides tool should be available'); assert.equal(guidesTool.name, 'get_available_best_practice_guides'); assert.ok(guidesTool.description, 'Tool should have description'); assert.ok(guidesTool.inputSchema, 'Tool should have input schema'); }); test('should have correct tool input schema', async () => { const tools = await client.listTools(); const guidesTool = tools.find(tool => tool.name === 'get_available_best_practice_guides'); assert.equal(guidesTool.inputSchema.type, 'object'); // get_available_best_practice_guides takes no required parameters assert.ok(!guidesTool.inputSchema.required || guidesTool.inputSchema.required.length === 0); }); }); describe('Basic Functionality', () => { test('should execute successfully with empty parameters', async () => { const result = await client.callTool('get_available_best_practice_guides', {}); assertValidMCPResponse(result); assert.equal(result.isError, false, 'Should not return error'); assert.equal(result.content.length, 1, 'Should return single content item'); assert.equal(result.content[0].type, 'text', 'Content type should be text'); }); test('should return valid JSON array in response', async () => { const result = await client.callTool('get_available_best_practice_guides', {}); assertValidMCPResponse(result); const responseText = result.content[0].text; // Should be valid JSON let guideArray; assert.doesNotThrow(() => { guideArray = JSON.parse(responseText); }, 'Response should be valid JSON'); assert.ok(Array.isArray(guideArray), 'Response should be JSON array'); assert.ok(guideArray.length > 0, 'Guide array should not be empty'); }); test('should ignore additional parameters gracefully', async () => { const result = await client.callTool('get_available_best_practice_guides', { unexpectedParam: 'should be ignored', anotherParam: 123, objectParam: { nested: 'value' } }); assertValidMCPResponse(result); assert.equal(result.isError, false, 'Should handle extra params gracefully'); // Result should be identical to empty params call const baselineResult = await client.callTool('get_available_best_practice_guides', {}); assert.equal(result.content[0].text, baselineResult.content[0].text, 'Result should be identical regardless of extra params'); }); }); describe('Content Quality and Completeness', () => { test('should return comprehensive best practice guide coverage', async () => { const result = await client.callTool('get_available_best_practice_guides', {}); const guideArray = JSON.parse(result.content[0].text); const analysis = bestPracticeAnalyzer.analyzeGuideList(guideArray); // Validate completeness const issues = bestPracticeAnalyzer.validateCompleteness(analysis); assert.equal(issues.length, 0, `Content issues found: ${issues.join('; ')}`); // Verify substantial guide count assert.ok(analysis.totalGuides >= 13, `Should have substantial guide count (got ${analysis.totalGuides})`); }); test('should include all critical best practice guides', async () => { const result = await client.callTool('get_available_best_practice_guides', {}); const guideArray = JSON.parse(result.content[0].text); const analysis = bestPracticeAnalyzer.analyzeGuideList(guideArray); bestPracticeAnalyzer.criticalGuides.forEach(guideName => { assert.ok(analysis.foundGuides.has(guideName), `Missing critical guide: ${guideName}`); }); // All critical guides should be found assert.equal(analysis.missingCriticalGuides.length, 0, `Missing critical guides: ${analysis.missingCriticalGuides.join(', ')}`); }); test('should have proper guide structure for all guides', async () => { const result = await client.callTool('get_available_best_practice_guides', {}); const guideArray = JSON.parse(result.content[0].text); guideArray.forEach((guide, index) => { // Validate required fields bestPracticeAnalyzer.requiredFields.forEach(field => { assert.ok(Object.prototype.hasOwnProperty.call(guide, field), `Guide ${index} missing field: ${field}`); assert.equal(typeof guide[field], 'string', `Guide ${index} field ${field} should be string`); assert.ok(guide[field].trim().length > 0, `Guide ${index} field ${field} should not be empty`); }); // Validate name format assert.ok(/^[a-z][a-z0-9_]*$/.test(guide.name), `Guide ${index} has invalid name format: ${guide.name}`); // Validate title contains "Best Practices" assert.ok(guide.title.includes('Best Practices'), `Guide ${index} title should contain "Best Practices": ${guide.title}`); // Validate description length assert.ok(guide.description.length >= 20, `Guide ${index} description too short: ${guide.description}`); }); }); test('should cover all major SFCC development areas', async () => { const result = await client.callTool('get_available_best_practice_guides', {}); const guideArray = JSON.parse(result.content[0].text); const analysis = bestPracticeAnalyzer.analyzeGuideList(guideArray); // Check each category has coverage Object.entries(bestPracticeAnalyzer.guideCategories).forEach(([category, expectedGuides]) => { const foundInCategory = analysis.guidesByCategory[category] || []; assert.ok(foundInCategory.length > 0, `Category ${category} should have at least one guide`); // Check that most expected guides in category are present const coverageRatio = foundInCategory.length / expectedGuides.length; assert.ok(coverageRatio >= 0.5, `Category ${category} should have good coverage (${foundInCategory.length}/${expectedGuides.length})`); }); }); test('should not have duplicate or invalid guides', async () => { const result = await client.callTool('get_available_best_practice_guides', {}); const guideArray = JSON.parse(result.content[0].text); const analysis = bestPracticeAnalyzer.analyzeGuideList(guideArray); assert.equal(analysis.duplicates.length, 0, `Found duplicate guides: ${analysis.duplicates.join(', ')}`); assert.equal(analysis.invalidGuides.length, 0, `Found invalid guides: ${analysis.invalidGuides.length}`); assert.equal(analysis.validationErrors.length, 0, `Validation errors: ${analysis.validationErrors.join('; ')}`); }); }); describe('Content Validation', () => { test('should include expected core guides', async () => { const result = await client.callTool('get_available_best_practice_guides', {}); const guideArray = JSON.parse(result.content[0].text); const guideNames = guideArray.map(guide => guide.name); // Check specific expected guides const expectedCoreGuides = [ 'cartridge_creation', 'sfra_controllers', 'sfra_models', 'sfra_client_side_js', 'sfra_scss', 'ocapi_hooks', 'scapi_hooks', 'security', 'performance' ]; expectedCoreGuides.forEach(guideName => { assert.ok(guideNames.includes(guideName), `Should include core guide: ${guideName}`); }); }); test('should have meaningful guide descriptions', async () => { const result = await client.callTool('get_available_best_practice_guides', {}); const guideArray = JSON.parse(result.content[0].text); guideArray.forEach(guide => { // Description should be substantial assert.ok(guide.description.length >= 50, `Guide ${guide.name} description should be substantial: ${guide.description.length} chars`); // Description should be related to the guide name const nameWords = guide.name.split('_'); const hasRelatedContent = nameWords.some(word => guide.description.toLowerCase().includes(word.toLowerCase()) ); assert.ok(hasRelatedContent || guide.description.toLowerCase().includes('sfcc') || guide.description.toLowerCase().includes('commerce'), `Guide ${guide.name} description should be related to guide name or SFCC`); }); }); test('should provide guides for different skill levels', async () => { const result = await client.callTool('get_available_best_practice_guides', {}); const guideArray = JSON.parse(result.content[0].text); // Should have guides for beginners (cartridge creation) const beginnerGuides = guideArray.filter(guide => guide.name.includes('cartridge') || guide.description.toLowerCase().includes('introduction') || guide.description.toLowerCase().includes('getting started') ); // Should have advanced guides (security, performance) const advancedGuides = guideArray.filter(guide => guide.name.includes('security') || guide.name.includes('performance') || guide.description.toLowerCase().includes('advanced') || guide.description.toLowerCase().includes('optimization') ); assert.ok(beginnerGuides.length > 0, 'Should have guides for beginners'); assert.ok(advancedGuides.length > 0, 'Should have guides for advanced users'); }); }); describe('Error Handling and Edge Cases', () => { test('should handle malformed parameters gracefully', async () => { // Test with various edge case parameters that are still valid objects const edgeCases = [ {}, // Empty object (valid) { malformed: 'data' }, { nested: { deeply: { malformed: 'data' } } }, { randomField: null }, { arrayField: [1, 2, 3] }, { booleanField: true } ]; for (const params of edgeCases) { const result = await client.callTool('get_available_best_practice_guides', params); assertValidMCPResponse(result); assert.equal(result.isError, false, `Should handle edge case gracefully: ${JSON.stringify(params)}`); // Result should still be valid const guideArray = JSON.parse(result.content[0].text); assert.ok(Array.isArray(guideArray), 'Should still return valid array'); assert.ok(guideArray.length > 0, 'Should still return guides'); } }); test('should maintain functionality under rapid consecutive calls', async () => { const rapidCalls = 10; const promises = Array.from({ length: rapidCalls }, () => client.callTool('get_available_best_practice_guides', {}) ); const results = await Promise.all(promises); results.forEach((result, index) => { assertValidMCPResponse(result); assert.equal(result.isError, false, `Rapid call ${index} should succeed`); const guideArray = JSON.parse(result.content[0].text); assert.ok(Array.isArray(guideArray), `Rapid call ${index} should return array`); assert.ok(guideArray.length > 0, `Rapid call ${index} should return guides`); }); // All results should be identical const firstResponse = results[0].content[0].text; results.forEach((result, index) => { assert.equal(result.content[0].text, firstResponse, `Rapid call ${index} result should be identical`); }); }); }); }); ``` -------------------------------------------------------------------------------- /docs/best-practices/sfra_scss.md: -------------------------------------------------------------------------------- ```markdown ## SFRA SCSS Best Practices (AI-Agent Optimized) Purpose: Provide a concise, implementation‑ready reference for styling and theming Salesforce B2C Commerce SFRA storefronts using SCSS. Optimized for automated reasoning, safe overrides, upgrade resilience, and performant output. --- ## 1. Core Principles (Never Skip) | Principle | Rule | Why It Exists | Action Pattern | |-----------|------|---------------|----------------| | Cartridge Overlay | Never edit `app_storefront_base` | Preserve upgrade path | Create mirror path in custom cartridge + import base first | | Import First, Then Override | Always `@import '~base/...';` at top | Keeps base layout + utilities | File header snippet template | | Single Source of Truth | Centralize tokens in `abstracts/_variables.scss` | Global theming + diffable changes | Import base → redefine only changed vars | | Shallow Specificity | Max selector depth 2–3 | Predictable cascade | Use BEM instead of nested HTML chains | | Mobile‑First | Base = smallest viewport | Less override churn | Add enhancements with `min-width` media mixin | | Immutable Generated CSS | Never hand edit `/static/default/css` | Prevent drift | Recompile via build scripts only | | Deterministic Build | Same inputs ⇒ same CSS | Debug + CI parity | Lock dependency versions | Red flag triggers for re‑evaluation: deep nesting (>3), repeated hardcoded colors, `!important`, duplicated breakpoint literals, editing compiled CSS, or missing initial `@import`. AI Guardrail (fast reject): If asked to "change base SCSS" directly, respond with an override strategy instead of editing base source. --- ## 2. Minimal Override Workflow (Algorithm) 1. Inspect element in browser → capture compiled CSS filename (e.g. `product/detail.css`) & selector(s). 2. Locate source in base: `app_storefront_base/cartridge/client/default/scss/<path>.scss`. 3. Recreate identical relative path in custom cartridge under `cartridge/client/default/scss/`. 4. Add header: `@import '~base/<path>';` (must be line 1; no preceding BOM/comment). 5. Append scoped overrides (BEM, tokens, mixins only—no raw hex unless introducing new token). 6. Run `npm run compile:scss` (or project alias) → deploy → verify cascade diff. 7. If change is global (color, spacing, radius) prefer variable override in `_variables.scss` before component override. Decision Tree: | Goal | Location to Change First | Fallback If Not Available | |------|--------------------------|---------------------------| | Color / Font / Spacing shift | `_variables.scss` | Component partial override | | Layout structure | ISML + classes | New component partial | | Responsive tweak | Mixin breakpoints | Local media query (still via mixin) | | Reusable visual pattern | New mixin | Placeholder + `@extend` | | Plugin override | `vendors/` override partial | Scoped component patch | Plugin Extension Nuances (Observed from `plugin_wishlists`): * Keep plugin additions minimal: import base global first inside plugin `global.scss` (`@import "~base/global";`) then only plugin-specific partials. * Do NOT re-import `variables` inside multiple plugin partials unless you need local compilation context—prefer central import in the entry file to avoid duplication. * Avoid redefining spacing tokens mid-file (e.g. `$spacer` inside a feature file) unless intentionally creating a component-scoped token; document with a comment `// local token`. * Consolidate repeated keyframes: If multiple alerts use identical fade animation, define it once in a shared partial (e.g. `components/_animations.scss`) instead of inline per component. * When plugin needs base mixins (e.g. Bootstrap breakpoints) prefer `@import "~base/global";` rather than directly importing bootstrap again to reduce risk of version drift. --- ## 3. Canonical SCSS Directory (7‑1 Adapted) ``` scss/ abstracts/ (_variables.scss, _mixins.scss, _functions.scss) base/ (_reset.scss, _typography.scss, _base.scss) layout/ (_header.scss, _footer.scss, _navigation.scss, _grid.scss) components/ (_buttons.scss, _product-tile.scss, _carousel.scss, ...) pages/ (_home.scss, _checkout.scss, _pdp.scss, ...) vendors/ (_bootstrap-overrides.scss, _plugin-X.scss) global.scss (imports only – no rules) ``` Import Order (global.scss): 1. abstracts (variables → mixins → functions) 2. vendors 3. base 4. layout 5. components 6. pages SFRA Reality Snapshot: The core `global.scss` in `app_storefront_base` imports: `variables`, `skin/skinVariables`, bootstrap custom, overrides, utilities, icon libraries, then individual component partials. AI agents should treat this as the authoritative ordering when composing custom `global.scss` overlays: always import base `global` first in plugin/global contexts, then plugin additions. Rule: No CSS selectors in `global.scss`; only `@import`. --- ## 4. Naming & Specificity Strategy (BEM + SFRA Conventions) | Aspect | Rule | Example | |--------|------|---------| | Block | Semantic, domain concept | `.product-tile` | | Element | `__` double underscore | `.product-tile__price` | | Modifier | `--` double hyphen | `.product-tile--featured` | | State (JS) | `is-` prefix (transient) | `.is-loading` | | Utility | `u-` + purpose | `.u-hidden` | Guidelines: * Avoid HTML element chaining: prefer `.mini-cart__count` not `header .mini-cart span.count`. * Never exceed 3 levels of specificity (block + element + modifier/state). * Do not style IDs; reserve for scripting anchor only if necessary. * Existing SFRA partials sometimes nest deeper (e.g. `.product-tile .tile-body .price .tiered .value`). When extending these, do NOT replicate entire nesting; extract just the block root with a modifier class you control (e.g. `.product-tile--compact`). * For legacy classes not following BEM (e.g. `.wishlistTile`), retain original naming but annotate exception with justification comment to aid future refactor and silence lint selectively—in code: `/* stylelint-disable-line */`. --- ## 5. Token & Theme Management Override pattern (`abstracts/_variables.scss`): ```scss @import '~base/variables'; // must stay first // Theme – Colors $primary: #0B5FFF; // Brand blue $secondary: #00A676; $body-bg: #F8F9FA; $body-color: #212529; // Typography $font-family-sans-serif: 'Inter', 'Helvetica Neue', Arial, sans-serif; $font-family-serif: 'Merriweather', Georgia, serif; // Radii / Spacing $border-radius: 2px; $btn-padding-y: 0.5rem; $btn-padding-x: 1.125rem; ``` Rules: * Only override variables you intentionally change. * Introduce new tokens with consistent naming (`$color-*`, `$space-*`, `$z-*`). * Never redefine variables in component partials—centralize. * If a plugin must create a small local scalar (e.g. `$spacer` for a one-off layout tweak), prefix with component context when possible (`$wishlist-spacer`) to avoid collision with global tokens. * Avoid re-importing base variables in each partial—imports are concatenated; repeated imports increase compile time and risk inconsistent overrides if order diverges. --- ## 6. Reuse Mechanisms (Decision Matrix) | Need | Use | Why | Avoid If | |------|-----|-----|----------| | Static property bundle reused widely | `%placeholder` + `@extend` | Single output selector group | Needs argumentization | | Dynamic pattern w/ params | `@mixin` | Flexible + readable | Output duplication causes bloat | | Global theming value | `$variable` | Single update point | Represents multi-rule semantic block | | Conditional computed value | `@function` | Returns scalar for calc chains | Can be plain variable | Responsive Mixin: ```scss $breakpoints: ( sm: 576px, md: 768px, lg: 992px, xl: 1200px ) !default; @mixin mq($bp) { @if map-has-key($breakpoints, $bp) { @media (min-width: map-get($breakpoints, $bp)) { @content; } } @else { @warn 'Unknown breakpoint: #{$bp}'; } } ``` Usage: ```scss .product-tile { width: 100%; @include mq(md) { width: 50%; } } ``` Bootstrap Breakpoints Interop: SFRA base uses Bootstrap's `media-breakpoint-*` mixins. When generating code referencing base breakpoints, prefer those existing mixins (`@include media-breakpoint-down(md) { ... }`) if the base already depends on Bootstrap, instead of introducing a parallel custom map to avoid divergence. Use the custom `$breakpoints` + `mq()` pattern only if project intentionally decouples from Bootstrap. --- ## 7. Example Override (PDP Title) File: `product/detail.scss` ```scss @import '~base/product/detail'; // inherit base .product-detail__name { font-family: $font-family-serif; font-size: 2.4rem; color: $primary; } ``` Compiled file resolved via `assets.addCss('/css/product/detail.css')` with cartridge path precedence—no ISML changes needed. AI Validation Step: After generation, assert snippet begins with base import and contains only new selectors or modified declarations—no duplication of entire base blocks. --- ## 8. Component Patterns Library (Sample Snippets) Buttons (`components/_buttons.scss`): ```scss @import '~base/components/buttons'; .btn--pill { border-radius: 999rem; } .btn--outline-secondary { color: $secondary; background: transparent; border: 1px solid $secondary; &:hover { background: $secondary; color: #fff; } } ``` Product Tile (`components/_product-tile.scss`): ```scss @import '~base/components/product-tile'; .product-tile { border: 1px solid transparent; transition: box-shadow .2s; &:hover { box-shadow: 0 4px 12px rgba(0,0,0,.1); border-color: $gray-300; } &__price--sale { color: $danger; font-weight: 600; } } Refactor Tip: Instead of deeply nested overrides inside `.product-tile .tile-body .price .tiered .value`, target a purposeful modifier class (`.product-tile__price--tiered-value`) you add in ISML for long-term maintainability. ``` Checkout (`pages/_checkout.scss`): ```scss @import '~base/pages/checkout'; .checkout-nav .nav-link { background:$gray-200; color:$gray-600; &.active { background:$primary; color:#fff; } } ``` --- ## 9. Responsive Strategy | Rule | Implementation | |------|----------------| | Mobile‑first | Base styles = small viewport | | Progressive enhancement | `@include mq(md){...}` not `max-width` overrides | | Centralized breakpoints | `$breakpoints` map only | | Avoid pixel drift | Use tokens, not ad‑hoc numbers | Anti‑Pattern: Desktop CSS first + mobile overrides via `@media (max-width)` – leads to override churn. Plugin Reality: Wishlist plugin currently mixes `media-breakpoint-up` and `media-breakpoint-down` usage; maintain consistency—choose mobile-first pattern (prefer `-up` for enhancements) and refactor legacy `-down` only when safe. --- ## 10. Accessibility & Inclusive Styling | Concern | Checklist | |---------|-----------| | Color Contrast | Ensure WCAG AA (> 4.5:1 body, 3:1 large text). Token review before merge. | | Focus States | Visible focus ring; never remove outline without replacement. | | Reduced Motion | Gate large transitions with `@media (prefers-reduced-motion: reduce)` | | Hidden Content | Use utility `.u-visually-hidden` for screen-reader only text; avoid `display:none` for needed ARIA live content. | | Touch Targets | Minimum 44px height for interactive elements (buttons, filters). | Utility Example: ```scss .u-visually-hidden { position:absolute!important; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden; clip:rect(0 0 0 0); white-space:nowrap; border:0; } ``` Animation Respect: Provide a reduced-motion alternative when introducing keyframe animations (e.g. fading alerts). Centralize keyframes and wrap any large movement inside `@media (prefers-reduced-motion: no-preference)`; fallback: immediate end state. --- ## 11. Performance Practices | Optimization | Action | Tooling | |--------------|--------|---------| | Critical CSS | Generate subset for key templates → inline via `store-skin` asset | `npm i critical` | | Prune Unused CSS | Integrate PurgeCSS scanning ISML & JS | PurgeCSS plugin | | Minification | Ensure prod build via `sgmf-scripts` produces minified bundle | CI check size | | Source Maps (dev only) | Enable for DX; disable prod | Sass compiler flag | | Font Strategy | Use WOFF2 + subsetting + `font-display: swap` | Font tooling | Suggested CI Budget: Global CSS ≤ 250KB uncompressed (enterprise baseline) – fail build if regression > +15% week‑over‑week. Duplicate Import Scan: Add a CI grep step to detect repeated `@import "~base/variables";` inside plugin component partials—emit warning to consolidate. --- ## 12. Linting & Quality Automation Install: ``` npm i -D stylelint stylelint-config-standard-scss stylelint-declaration-strict-value ``` Config (`.stylelintrc.json`): ```json { "extends": "stylelint-config-standard-scss", "plugins": ["stylelint-declaration-strict-value"], "rules": { "selector-class-pattern": ["^([a-z][a-z0-9]*)(__(?:[a-z0-9]+))?(--[a-z0-9-]+)?$", {"message": "Use BEM: block__element--modifier"}], "max-nesting-depth": 2, "declaration-no-important": true, "scale-unlimited/declaration-strict-value": [["color", "background-color", "border-color", "font-size"], {"ignoreValues": ["inherit", "transparent", "currentColor"]}] } } ``` Script: ```json "scripts": { "lint:scss": "stylelint 'cartridges/**/*.scss' --cache" } ``` Optional: pre‑commit hook with Husky + lint-staged. AI Generation Rule Set: When asked to produce SCSS: 1. Always start with required base import (unless file is `abstracts/_variables.scss`). 2. Do not inline raw colors already expressible as existing tokens—prefer token reference. 3. Replace any suggested `!important` with a cascade adjustment strategy comment. 4. Provide at most one new local token; advise moving to global if reused >2 times. 5. Add a 2-line header comment with PURPOSE + SAFE REMOVE classification. --- ## 13. Anti‑Patterns (Refactor Immediately) | Smell | Why Dangerous | Fix | |-------|---------------|-----| | Editing compiled CSS | Lost on next build | Modify SCSS source | | Missing base import | Breaks structural inheritance | Add `@import '~base/...';` line 1 | | Deep selector chains | Specificity lock-in | Convert to BEM blocks/elements | | Repeated hex codes | Inconsistent theming | Promote to variable | | `!important` usage | Overrides cascade discipline | Reduce specificity / re-order imports | | Inline media queries w/ literals | Hard to refactor | Use `mq()` mixin | | Component partial modifies unrelated block | Hidden coupling | Create new partial / utility | | Re-importing base variables in every plugin partial | Build bloat / risk inconsistent overrides | Import once at entry (e.g. plugin `global.scss`) | | Inline duplicated keyframes | Larger bundle & maintenance overhead | Centralize in shared `components/_animations.scss` | --- ## 14. Troubleshooting Matrix | Symptom | Likely Cause | Diagnostic | Resolution | |---------|--------------|-----------|-----------| | Style not applied | Not in compiled CSS | Search compiled file for selector | Check import order / rebuild | | Base style lost | Missing base import | Inspect diff of partial | Add import + revert overrides | | Wrong theme color persisting | Variable shadowed locally | Grep for variable redefinition | Centralize in `_variables.scss` | | Breakpoint shift inconsistent | Hardcoded px values | `grep -R "992px"` | Replace with token map | | Layout jumps FOUC | Critical CSS missing | Lighthouse trace | Implement store-skin inline CSS | | Repeated variable override ignored | Later import overrides earlier | Trace import graph order | Consolidate variable overrides at earliest import point | --- ## 15. Promptable Action Snippets (For AI Agents) | Intent | Prompt Template | |--------|-----------------| | Create component override | "Generate SFRA SCSS override for `<component>` (base path `<path>`). Include base import and BEM modifiers for states: loading, error." | | Introduce new theme color | "Add semantic token for highlight color with accessible contrast against `$body-bg` and update button hover style." | | Refactor deep selectors | "Refactor selector `<selector>` into BEM with max depth 2; preserve semantics." | | Add responsive variant | "Extend `.product-tile` to show 2 columns at md, 4 at lg via existing breakpoint mixin." | | Performance audit | "List top 10 heaviest SCSS partials by compiled size estimate and suggest consolidation." | | Plugin extension | "Generate wishlist plugin SCSS override importing '~base/global' then add toast animation using existing base token palette; avoid duplicating keyframes if fade already exists." | --- ## 16. Migration Checklist (Before Deploy) [] All overrides start with base import [] No direct edits to `app_storefront_base` [] Variables overridden centrally only [] Lint passes (no warnings in CI) [] No `!important` (or justified + documented) [] CSS bundle size budget respected [] Critical CSS updated for homepage + PDP [] Source maps disabled for production [] New utilities documented (inline comment header) [] No duplicate keyframe blocks with identical declarations [] Plugin global includes base global exactly once --- ## 17. Reference Quick Commands ``` # Compile SCSS npm run compile:scss # Lint SCSS npm run lint:scss # Find hardcoded colors (non-variable) // in app_custom/cartridge/client/default/scss/abstracts/_variables.scss # Estimate partial usage (appearance in compiled) @import '~base/variables'; ``` CI Grep Helpers (Optional): ``` grep -R "@import \"~base/variables\"" cartridges/plugin_* | sort grep -R "@keyframes fade" cartridges/ | wc -l ``` --- ## 18. Secure & Maintainable Patterns | Concern | Guidance | |---------|----------| | Multi-brand scaling | Abstract brand diffs to dedicated `_brand-<id>.scss` imported conditionally via build flag | | A/B testing CSS | Isolate experiment layer in `components/experiments/` with clear removal date comment | | Plugin overrides | Keep each plugin override in `vendors/_<plugin>-overrides.scss` with upstream version tag | | Future SFRA upgrade | Diff only variable + override partials; never fork base files wholesale | Header Annotation Block (optional for generated partial): ```scss // PURPOSE: Override of base product tile for brand visual refinement // DEPENDS: ~base/components/product-tile // SAFE REMOVE: Yes (reverts to base visual style) ``` Classification Legend: * SAFE REMOVE: Removing file reverts to acceptable base styling. * CONDITIONAL: Removal affects brand contract (document impact). * CRITICAL: Provides accessibility or functional styling (never remove without replacement). --- ## 19. When NOT to Override | Scenario | Better Option | |----------|---------------| | Structural HTML limitation | Adjust ISML markup first | | Repeated spacing hacks | Introduce spacing scale utility classes | | Overriding grid internals | Use flexbox utilities / custom wrapper | | One-off promotional layout | Page partial (`pages/_promo-<slug>.scss`) + sunset note | | Behavior actually JS-driven (e.g. show/hide) | Adjust JS or data attributes; keep CSS purely presentational | --- ## 20. AI Generation Quick Checklist (Inline in Responses) Provide along with any generated SCSS so humans can validate rapidly: 1. Base Import Present? (Yes/No) 2. Variable Overrides? (List or None) 3. New Tokens Introduced? (List or None) 4. Max Nesting Depth (#) 5. Uses Existing Breakpoint Mixins? (Yes/No) 6. Any Raw Hex Values? (List or None + justification) 7. Keyframes Added? (Name or None; already exists?) 8. Accessibility Considerations? (Focus/contrast/motion) 9. Removable Classification (SAFE REMOVE / CONDITIONAL / CRITICAL) 10. Lint‑Sensitive Areas (Exceptions with reason) If any answer violates a stated rule, propose an auto-correction diff, not just a warning. ```