From e0a1847127925f827be991fee2cde3b5f89fd186 Mon Sep 17 00:00:00 2001 From: Jacek Date: Sun, 8 Mar 2026 10:29:06 +0100 Subject: [PATCH] feat(api): Introduce shopPRO API documentation and endpoints - Added api-reference.json for API specifications including authentication, response formats, and available endpoints. - Created index.html for public API documentation, dynamically loading endpoint details from api-reference.json. - Removed htaccess.conf file and migrated routing logic to pp_routes for improved maintainability. - Added new 'type' column in pp_routes to differentiate between entity and system routes. --- .phpunit.result.cache | 2 +- .serena/project.yml | 4 + AGENTS.md | 262 ++++++- CLAUDE.md | 18 +- api-docs/api-reference.json | 292 ++++++++ api-docs/index.html | 60 ++ docs/API.md | 597 ---------------- docs/TODO.md | 1 + .../2026-02-27-htaccess-conf-elimination.md | 658 ------------------ .../2026-02-27-htaccess-to-routes-design.md | 121 ---- 10 files changed, 614 insertions(+), 1401 deletions(-) create mode 100644 api-docs/api-reference.json create mode 100644 api-docs/index.html delete mode 100644 docs/API.md delete mode 100644 docs/plans/2026-02-27-htaccess-conf-elimination.md delete mode 100644 docs/plans/2026-02-27-htaccess-to-routes-design.md diff --git a/.phpunit.result.cache b/.phpunit.result.cache index e28de50..784ee99 100644 --- a/.phpunit.result.cache +++ b/.phpunit.result.cache @@ -1 +1 @@ -{"version":1,"defects":{"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveCreatesNewArticle":3,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveUpdatesExistingArticle":3,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveTranslationsInsertsForNewArticle":4,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveTranslationsUpsertsForExistingArticle":4,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSavePagesForNewArticle":4,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveDeletesMarkedImagesOnUpdate":4,"Tests\\Unit\\Domain\\Scontainers\\ScontainersRepositoryTest::testFrontScontainerDetailsReturnsContainerWithLanguage":3,"Tests\\Unit\\front\\Controllers\\ShopCouponControllerTest::testConstructorAcceptsRepository":4,"Tests\\Unit\\front\\Controllers\\ShopCouponControllerTest::testHasMainActionMethods":4,"Tests\\Unit\\front\\Controllers\\ShopCouponControllerTest::testConstructorRequiresCouponRepository":4,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testArticleDetailsFrontendReturnsArticleWithRelations":3,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testArticleDetailsFrontendCopyFromFallback":4,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testTopArticlesOrderByViews":4,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testNewsListArticlesOrderByDateDesc":4,"Tests\\Unit\\api\\ApiRouterTest::testHandleReturns401WhenNoApiKey":4,"Tests\\Unit\\api\\ApiRouterTest::testHandleReturns401WhenWrongApiKey":4,"Tests\\Unit\\api\\ApiRouterTest::testHandleReturns401WhenStoredKeyEmpty":4,"Tests\\Unit\\api\\ApiRouterTest::testHandleReturns400WhenMissingEndpoint":4,"Tests\\Unit\\api\\ApiRouterTest::testHandleReturns400WhenMissingAction":4,"Tests\\Unit\\api\\ApiRouterTest::testHandleReturns404ForUnknownEndpoint":4,"Tests\\Unit\\Domain\\Order\\OrderAdminServiceTest::testProcessApiloSyncQueueKeepsTaskWhenApiloOrderIdIsNull":3,"Tests\\Unit\\Domain\\Order\\OrderAdminServiceTest::testProcessApiloSyncQueueRemovesTaskAfterMaxAttempts":3,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testStatusesReturnsFormattedList":4,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testStatusesRejectsPostMethod":4,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testTransportsReturnsFormattedList":4,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testTransportsRejectsPostMethod":4,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testPaymentMethodsReturnsFormattedList":4,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testPaymentMethodsRejectsPostMethod":4,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testAttributesReturnsFormattedList":4,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testAttributesRejectsPostMethod":4,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testEnsureAttributeRejectsGetMethod":4,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testEnsureAttributeReturns400WhenNoBody":4,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testEnsureAttributeValueRejectsGetMethod":4,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testEnsureAttributeValueReturns400WhenNoBody":4,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testFetchNextUpdatesStatusToProcessing":3,"Tests\\Unit\\admin\\Controllers\\UpdateControllerTest::testConstructorAcceptsRepository":4,"Tests\\Unit\\admin\\Controllers\\UpdateControllerTest::testHasMainViewMethod":4,"Tests\\Unit\\admin\\Controllers\\UpdateControllerTest::testMainViewReturnsString":4,"Tests\\Unit\\admin\\Controllers\\UpdateControllerTest::testHasUpdateMethod":4,"Tests\\Unit\\admin\\Controllers\\UpdateControllerTest::testHasUpdateAllMethod":4,"Tests\\Unit\\admin\\Controllers\\UpdateControllerTest::testConstructorRequiresRepository":4,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testDeletePermanentlyRemovesArticleAndRelations":3,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testCategoryDeleteReturnsTrueWhenDeleted":3},"times":{"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetQuantityReturnsCorrectValue":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetQuantityReturnsNullWhenProductNotFound":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testFindReturnsProductData":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testUpdateQuantitySuccess":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetPriceReturnsRegularPrice":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetPriceReturnsPromoPrice":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetPriceReturnsRegularWhenPromoIsHigher":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetPriceReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetNameReturnsProductName":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetNameReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetQuantityReturnsInteger":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testUnarchiveUpdatesProductAndChildren":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testArchiveUpdatesProductAndChildren":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testUnarchiveReturnsBool":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testArchiveReturnsBool":0,"Tests\\Unit\\admin\\Controllers\\ProductArchiveControllerTest::testConstructorAcceptsRepository":0.001,"Tests\\Unit\\admin\\Controllers\\ProductArchiveControllerTest::testHasListMethod":0,"Tests\\Unit\\admin\\Controllers\\ProductArchiveControllerTest::testHasUnarchiveMethod":0,"Tests\\Unit\\admin\\Controllers\\ProductArchiveControllerTest::testListMethodReturnType":0,"Tests\\Unit\\admin\\Controllers\\ProductArchiveControllerTest::testUnarchiveMethodReturnType":0,"Tests\\Unit\\admin\\Controllers\\ProductArchiveControllerTest::testConstructorRequiresProductRepository":0,"Tests\\Unit\\Domain\\Banner\\BannerRepositoryTest::testFindReturnsBannerWithTranslations":0.001,"Tests\\Unit\\Domain\\Banner\\BannerRepositoryTest::testFindReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\Banner\\BannerRepositoryTest::testDeleteReturnsTrue":0.002,"Tests\\Unit\\Domain\\Banner\\BannerRepositoryTest::testSaveInsertsNewBanner":0,"Tests\\Unit\\Domain\\Cache\\CacheRepositoryTest::testClearCacheWithRedis":0.001,"Tests\\Unit\\Domain\\Cache\\CacheRepositoryTest::testClearCacheRedisUnavailable":0,"Tests\\Unit\\Domain\\Cache\\CacheRepositoryTest::testClearCacheWithoutRedis":0,"Tests\\Unit\\Domain\\Cache\\CacheRepositoryTest::testClearCacheReturnStructure":0,"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testCanBeInstantiated":0,"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testHasSaveSettingsMethod":0,"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testHasGetSettingsMethod":0,"Tests\\Unit\\admin\\Controllers\\SettingsControllerTest::testConstructorAcceptsRepository":0.001,"Tests\\Unit\\admin\\Controllers\\SettingsControllerTest::testHasClearCacheMethod":0,"Tests\\Unit\\admin\\Controllers\\SettingsControllerTest::testHasClearCacheAjaxMethod":0,"Tests\\Unit\\admin\\Controllers\\SettingsControllerTest::testHasSaveMethod":0,"Tests\\Unit\\admin\\Controllers\\SettingsControllerTest::testHasViewMethod":0,"Tests\\Unit\\admin\\Controllers\\SettingsControllerTest::testIsNotAbstract":0,"Tests\\Unit\\admin\\Controllers\\SettingsControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testCanCreateController":0.005,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testHasListMethod":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testListMethodReturnType":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testHasEditMethod":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testEditMethodReturnType":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testConstructorAcceptsRepository":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testConstructorRequiresArticleRepository":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testFindReturnsArticleWithRelations":0.006,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testFindReturnsNullWhenArticleDoesNotExist":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testDeleteNonassignedFilesDeletesDbRows":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testDeleteNonassignedImagesDeletesDbRows":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveCreatesNewArticle":0.001,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveReturnsZeroWhenInsertFails":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveUpdatesExistingArticle":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveTranslationsInsertsForNewArticle":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveTranslationsUpsertsForExistingArticle":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSavePagesForNewArticle":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveDeletesMarkedImagesOnUpdate":0.001,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testArchiveSetsStatusToMinusOne":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testArchiveReturnsFalseWhenUpdateFails":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveGalleryOrderUpdatesImageOrder":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveGalleryOrderSkipsEmptyValues":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testHasBrowseListMethod":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testHasGalleryOrderSaveMethod":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testBrowseListMethodReturnType":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testGalleryOrderSaveMethodReturnType":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testListForAdminWhitelistsSortAndDirection":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testListForAdminUsesBoundParamsForTitleFilter":0,"Tests\\Unit\\Domain\\Banner\\BannerRepositoryTest::testSaveWithLegacyFormat":0,"Tests\\Unit\\Domain\\Banner\\BannerRepositoryTest::testSaveUpdatesExistingTranslationsByBannerAndLang":0,"Tests\\Unit\\Domain\\Banner\\BannerRepositoryTest::testListForAdminIncludesThumbnailSrc":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testRestoreSetsStatusToZero":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testDeletePermanentlyRemovesArticleAndRelations":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testListArchivedForAdminWhitelistsSortAndDirection":0,"Tests\\Unit\\Domain\\Dictionaries\\DictionariesRepositoryTest::testFindReturnsUnitWithTranslations":0.001,"Tests\\Unit\\Domain\\Dictionaries\\DictionariesRepositoryTest::testFindReturnsNullWhenUnitNotFound":0,"Tests\\Unit\\Domain\\Dictionaries\\DictionariesRepositoryTest::testSaveInsertsNewUnitAndTranslationsForStringLanguageId":0,"Tests\\Unit\\Domain\\Dictionaries\\DictionariesRepositoryTest::testDeleteRemovesUnitAndTranslations":0,"Tests\\Unit\\Domain\\Dictionaries\\DictionariesRepositoryTest::testGetUnitNameByIdReturnsTextFromDatabase":0,"Tests\\Unit\\Domain\\Dictionaries\\DictionariesRepositoryTest::testGetUnitNameByIdSupportsStringLanguageId":0,"Tests\\Unit\\Domain\\Dictionaries\\DictionariesRepositoryTest::testAllUnitsReturnsArrayIndexedById":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testLanguageDetailsReturnsArrayOrNull":0.002,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testLanguagesListReturnsArray":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testSaveLanguageRejectsInvalidLanguageId":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testSaveTranslationInsertsNewTranslationAndReturnsId":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testDeleteTranslationReturnsBoolean":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testListForAdminReturnsItemsAndTotal":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testDefaultLanguageIdReturnsLanguageWithStartFlag":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testDefaultLanguageIdFallsBackToFirstLanguageOrPl":0,"Tests\\Unit\\Domain\\Layouts\\LayoutsRepositoryTest::testFindReturnsLayoutWithRelations":0.001,"Tests\\Unit\\Domain\\Layouts\\LayoutsRepositoryTest::testDeleteReturnsFalseWhenOnlyOneLayoutExists":0,"Tests\\Unit\\Domain\\Layouts\\LayoutsRepositoryTest::testFindReturnsDefaultLayoutWhenRecordDoesNotExist":0,"Tests\\Unit\\Domain\\Layouts\\LayoutsRepositoryTest::testSaveInsertsNewLayoutAndReturnsId":0,"Tests\\Unit\\Domain\\Layouts\\LayoutsRepositoryTest::testListAllReturnsArray":0,"Tests\\Unit\\Domain\\Newsletter\\NewsletterRepositoryTest::testTemplateDetailsReturnsNullForInvalidId":0.003,"Tests\\Unit\\Domain\\Newsletter\\NewsletterRepositoryTest::testTemplateDetailsReturnsArray":0,"Tests\\Unit\\Domain\\Newsletter\\NewsletterRepositoryTest::testSaveSettingsUpdatesHeaderAndFooter":0,"Tests\\Unit\\Domain\\Newsletter\\NewsletterRepositoryTest::testDeleteTemplateReturnsFalseForAdminTemplate":0,"Tests\\Unit\\Domain\\Newsletter\\NewsletterRepositoryTest::testTemplateByNameReturnsText":0,"Tests\\Unit\\Domain\\Scontainers\\ScontainersRepositoryTest::testFindReturnsDefaultContainerForInvalidId":0.002,"Tests\\Unit\\Domain\\Scontainers\\ScontainersRepositoryTest::testDeleteReturnsFalseForInvalidId":0,"Tests\\Unit\\Domain\\Scontainers\\ScontainersRepositoryTest::testFindReturnsContainerWithTranslations":0,"Tests\\Unit\\Domain\\Scontainers\\ScontainersRepositoryTest::testDetailsForLanguageReturnsNullForInvalidData":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testFindReturnsUserWhenExists":0.002,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testFindReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testCheckLoginReturnsErrorWhenLoginIsTaken":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testCheckLoginReturnsOkWhenAvailable":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testSaveReturnsErrorForTooShortPasswordOnCreate":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testSaveReturnsErrorForMismatchedPasswordsOnCreate":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testSaveCreatesUserWithNormalizedSwitches":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testSaveUpdatesExistingUserWithPassword":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testSaveUpdatesExistingUserWithoutPassword":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testSaveReturnsErrorForTooShortPasswordOnUpdate":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testSaveReturnsErrorForMismatchedPasswordsOnUpdate":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testDeleteReturnsTrue":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testDeleteReturnsFalseOnFailure":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testDetailsReturnsUserByLogin":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testDetailsReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testLogonReturnsSuccessForValidCredentials":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testLogonReturnsZeroForNonexistentUser":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testLogonReturnsNegativeOneForBlockedUser":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testVerifyTwofaCodeReturnsFalseForNonexistentUser":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testVerifyTwofaCodeReturnsFalseAfterMaxAttempts":0.079,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testVerifyTwofaCodeReturnsFalseForExpiredCode":0.077,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testVerifyTwofaCodeReturnsTrueForValidCode":0.153,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testSendTwofaCodeReturnsFalseWhen2FADisabled":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testSendTwofaCodeReturnsFalseForInvalidEmail":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testUpdateByIdCallsDbUpdate":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testListForAdminReturnsItemsAndTotal":0,"Tests\\Unit\\admin\\Controllers\\ArticlesArchiveControllerTest::testConstructorAcceptsRepository":0.001,"Tests\\Unit\\admin\\Controllers\\ArticlesArchiveControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\admin\\Controllers\\ArticlesArchiveControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\ArticlesArchiveControllerTest::testConstructorRequiresArticleRepository":0,"Tests\\Unit\\admin\\Controllers\\DictionariesControllerTest::testConstructorAcceptsRepository":0.003,"Tests\\Unit\\admin\\Controllers\\DictionariesControllerTest::testHasListMethod":0,"Tests\\Unit\\admin\\Controllers\\DictionariesControllerTest::testHasEditMethod":0,"Tests\\Unit\\admin\\Controllers\\DictionariesControllerTest::testHasSaveMethod":0,"Tests\\Unit\\admin\\Controllers\\DictionariesControllerTest::testHasDeleteMethod":0,"Tests\\Unit\\admin\\Controllers\\DictionariesControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\DictionariesControllerTest::testConstructorRequiresDictionariesRepository":0,"Tests\\Unit\\admin\\Controllers\\LanguagesControllerTest::testConstructorAcceptsRepository":0.002,"Tests\\Unit\\admin\\Controllers\\LanguagesControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\admin\\Controllers\\LanguagesControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\LanguagesControllerTest::testConstructorRequiresLanguagesRepository":0,"Tests\\Unit\\admin\\Controllers\\LayoutsControllerTest::testConstructorAcceptsRepository":0.001,"Tests\\Unit\\admin\\Controllers\\LayoutsControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\admin\\Controllers\\LayoutsControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\LayoutsControllerTest::testConstructorRequiresLayoutsRepository":0,"Tests\\Unit\\admin\\Controllers\\NewsletterControllerTest::testConstructorAcceptsDependencies":0.002,"Tests\\Unit\\admin\\Controllers\\NewsletterControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\admin\\Controllers\\NewsletterControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\NewsletterControllerTest::testConstructorRequiresRepositoryAndRenderer":0,"Tests\\Unit\\admin\\Controllers\\ScontainersControllerTest::testConstructorAcceptsDependencies":0.002,"Tests\\Unit\\admin\\Controllers\\ScontainersControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\admin\\Controllers\\ScontainersControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\ScontainersControllerTest::testConstructorRequiresRepositoryAndLanguagesRepository":0,"Tests\\Unit\\admin\\Controllers\\UsersControllerTest::testConstructorAcceptsRepository":0.002,"Tests\\Unit\\admin\\Controllers\\UsersControllerTest::testHasViewListMethod":0,"Tests\\Unit\\admin\\Controllers\\UsersControllerTest::testHasUserEditMethod":0,"Tests\\Unit\\admin\\Controllers\\UsersControllerTest::testHasUserSaveMethod":0,"Tests\\Unit\\admin\\Controllers\\UsersControllerTest::testHasUserDeleteMethod":0,"Tests\\Unit\\admin\\Controllers\\UsersControllerTest::testHasTwofaMethod":0,"Tests\\Unit\\admin\\Controllers\\UsersControllerTest::testHasLoginFormMethod":0,"Tests\\Unit\\admin\\Controllers\\UsersControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\UsersControllerTest::testConstructorRequiresUserRepository":0,"Tests\\Unit\\admin\\Controllers\\UsersControllerTest::testNormalizeUserReturnsDefaultsForNull":0,"Tests\\Unit\\admin\\Controllers\\UsersControllerTest::testNormalizeUserCastsTypes":0,"Tests\\Unit\\admin\\Controllers\\UsersControllerTest::testNormalizeUserHandlesPartialData":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveFilesOrderUpdatesFilesOrder":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveFilesOrderSkipsEmptyValues":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testPagesSummaryForArticlesBuildsLabels":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testUpdateImageAltDelegatesToDatabase":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testMarkFileToDeleteDelegatesToDatabase":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testFindReturnsDefaultCouponForInvalidId":0.001,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testFindNormalizesCouponData":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testSaveInsertsCouponAndReturnsId":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testSaveUpdatesCouponAndReturnsId":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testDeleteReturnsFalseForInvalidId":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testDeleteReturnsTrueWhenDatabaseDeleteSucceeds":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testListForAdminWhitelistsSortAndDirection":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testCategoriesTreeReturnsHierarchy":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testGetSettingsReturnsArray":0.002,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testGetSettingReturnsValue":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testGetSettingReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testSaveSettingUpdatesExistingValue":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testSaveSettingInsertsNewValue":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testInvalidProviderThrowsException":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testLinkProductUpdatesDatabase":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testUnlinkProductClearsFields":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testGetProductSkuReturnsValue":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testGetProductSkuReturnsNullForMissing":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testApiloGetAccessTokenReturnsNullWithoutSettings":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testApiloFetchListThrowsForInvalidType":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testAllPublicMethodsExist":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testSettingsTableMapping":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testShopproProviderWorks":0,"Tests\\Unit\\Domain\\Pages\\PagesRepositoryTest::testMenusListReturnsArray":0.002,"Tests\\Unit\\Domain\\Pages\\PagesRepositoryTest::testMenuDeleteReturnsFalseWhenMenuHasPages":0,"Tests\\Unit\\Domain\\Pages\\PagesRepositoryTest::testGenerateSeoLinkAddsSuffixWhenBaseSlugExists":0,"Tests\\Unit\\Domain\\Pages\\PagesRepositoryTest::testPageUrlPreviewBuildsLanguagePrefixedUrlForNonDefaultLanguage":0,"Tests\\Unit\\Domain\\Promotion\\PromotionRepositoryTest::testFindReturnsDefaultPromotionForInvalidId":0.002,"Tests\\Unit\\Domain\\Promotion\\PromotionRepositoryTest::testSaveInsertsPromotionAndReturnsId":0,"Tests\\Unit\\Domain\\Promotion\\PromotionRepositoryTest::testDeleteReturnsFalseForInvalidId":0,"Tests\\Unit\\Domain\\Promotion\\PromotionRepositoryTest::testDeleteReturnsTrueWhenDatabaseDeleteSucceeds":0,"Tests\\Unit\\Domain\\Promotion\\PromotionRepositoryTest::testListForAdminWhitelistsSortAndDirection":0,"Tests\\Unit\\Domain\\Promotion\\PromotionRepositoryTest::testCategoriesTreeReturnsHierarchy":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testHasImageAltChangeMethod":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testHasFileNameChangeMethod":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testHasImageDeleteMethod":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testHasFileDeleteMethod":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testImageAltChangeMethodReturnType":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testFileNameChangeMethodReturnType":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testImageDeleteMethodReturnType":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testFileDeleteMethodReturnType":0,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testConstructorAcceptsDependencies":0.003,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testConstructorRequiresRepository":0,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testHasAllApiloSettingsMethods":0,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testHasAllApiloDataFetchMethods":0,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testHasAllApiloProductMethods":0,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testHasAllShopproMethods":0,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testApiloSettingsReturnsString":0,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testShopproSettingsReturnsString":0,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testVoidReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testDoesNotHaveSellasistMethods":0,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testDoesNotHaveBaselinkerMethods":0,"Tests\\Unit\\admin\\Controllers\\PagesControllerTest::testConstructorAcceptsRepositories":0.001,"Tests\\Unit\\admin\\Controllers\\PagesControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\admin\\Controllers\\PagesControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\PagesControllerTest::testConstructorRequiresPagesLanguagesAndLayoutsRepositories":0,"Tests\\Unit\\admin\\Controllers\\ShopCouponControllerTest::testConstructorAcceptsRepository":0.002,"Tests\\Unit\\admin\\Controllers\\ShopCouponControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopCouponControllerTest::testHasLegacyAliasMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopCouponControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\ShopCouponControllerTest::testConstructorRequiresCouponRepository":0,"Tests\\Unit\\admin\\Controllers\\ShopPromotionControllerTest::testConstructorAcceptsRepository":0.002,"Tests\\Unit\\admin\\Controllers\\ShopPromotionControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopPromotionControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\ShopPromotionControllerTest::testConstructorRequiresPromotionRepository":0,"Tests\\Unit\\Domain\\ShopStatus\\ShopStatusRepositoryTest::testFindReturnsNullForNegativeId":0.001,"Tests\\Unit\\Domain\\ShopStatus\\ShopStatusRepositoryTest::testFindReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\ShopStatus\\ShopStatusRepositoryTest::testFindReturnsStatusWithIdZero":0,"Tests\\Unit\\Domain\\ShopStatus\\ShopStatusRepositoryTest::testFindNormalizesNullApiloStatusId":0,"Tests\\Unit\\Domain\\ShopStatus\\ShopStatusRepositoryTest::testSaveUpdatesColorAndApiloStatusId":0,"Tests\\Unit\\Domain\\ShopStatus\\ShopStatusRepositoryTest::testSaveWithIdZeroWorks":0,"Tests\\Unit\\Domain\\ShopStatus\\ShopStatusRepositoryTest::testSaveWithEmptyApiloStatusIdSetsNull":0,"Tests\\Unit\\Domain\\ShopStatus\\ShopStatusRepositoryTest::testSaveRejectsNegativeId":0,"Tests\\Unit\\Domain\\ShopStatus\\ShopStatusRepositoryTest::testGetApiloStatusIdReturnsValue":0,"Tests\\Unit\\Domain\\ShopStatus\\ShopStatusRepositoryTest::testGetApiloStatusIdReturnsNullWhenNotSet":0,"Tests\\Unit\\Domain\\ShopStatus\\ShopStatusRepositoryTest::testGetByIntegrationStatusIdForApilo":0,"Tests\\Unit\\Domain\\ShopStatus\\ShopStatusRepositoryTest::testGetByIntegrationStatusIdReturnsNullForUnknownIntegration":0,"Tests\\Unit\\Domain\\ShopStatus\\ShopStatusRepositoryTest::testAllStatusesReturnsOrderedList":0,"Tests\\Unit\\Domain\\ShopStatus\\ShopStatusRepositoryTest::testListForAdminWhitelistsSortAndDirection":0,"Tests\\Unit\\admin\\Controllers\\ShopStatusesControllerTest::testConstructorAcceptsRepository":0.001,"Tests\\Unit\\admin\\Controllers\\ShopStatusesControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopStatusesControllerTest::testHasNoLegacyAliasMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopStatusesControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\ShopStatusesControllerTest::testConstructorRequiresShopStatusRepository":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testShouldRefreshAccessTokenReturnsFalseForFarFutureDate":0.001,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testShouldRefreshAccessTokenReturnsTrueForNearExpiryDate":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testApiloFetchListResultReturnsDetailedErrorWhenConfigMissing":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testApiloIntegrationStatusReturnsMissingConfigMessage":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testNormalizeApiloMapListRejectsErrorPayload":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testNormalizeApiloMapListAcceptsIdNameList":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testFindReturnsNullForInvalidId":0.001,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testFindReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testFindNormalizesData":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testSaveUpdatesRowAndReturnsId":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testSavePreservesNonNumericApiloPaymentTypeId":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testSaveReturnsNullForInvalidId":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testListForAdminWhitelistsSortAndDirection":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testAllActiveReturnsNormalizedRows":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testAllForAdminReturnsRowsIncludingInactive":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testFindActiveByIdReturnsNullForNotFound":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testFindKeepsNonNumericApiloPaymentTypeId":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testIsActiveNormalizesStatusValue":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testGetApiloPaymentTypeIdHandlesNullAndInt":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testGetApiloPaymentTypeIdReturnsStringForNonNumericValue":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testForTransportReturnsRows":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testFindReturnsNullForInvalidId":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testFindReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testFindNormalizesDataAndIncludesPaymentMethods":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testFindHandlesNullMaxWpAndApiloId":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testSaveInsertReturnsNewId":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testSaveUpdateReturnsExistingId":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testSaveInsertReturnsNullOnFailure":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testSaveResetsDefaultWhenSettingNew":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testSaveSwitchValuesNormalization":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testListForAdminWhitelistsSortColumn":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testAllActiveReturnsNormalizedRows":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testGetApiloCarrierAccountIdReturnsNullForInvalidId":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testGetApiloCarrierAccountIdReturnsIntOrNull":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testGetTransportCostReturnsFloatOrNull":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testAllForAdminReturnsAllTransports":0,"Tests\\Unit\\admin\\Controllers\\ShopPaymentMethodControllerTest::testConstructorAcceptsRepository":0.002,"Tests\\Unit\\admin\\Controllers\\ShopPaymentMethodControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopPaymentMethodControllerTest::testHasNoLegacyAliasMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopPaymentMethodControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\ShopPaymentMethodControllerTest::testConstructorRequiresPaymentMethodRepository":0,"Tests\\Unit\\admin\\Controllers\\ShopTransportControllerTest::testConstructorAcceptsRepositories":0.001,"Tests\\Unit\\admin\\Controllers\\ShopTransportControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopTransportControllerTest::testHasNoLegacyAliasMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopTransportControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\ShopTransportControllerTest::testConstructorRequiresBothRepositories":0,"Tests\\Unit\\Domain\\Attribute\\AttributeRepositoryTest::testFindAttributeReturnsDefaultAttributeForInvalidId":0.003,"Tests\\Unit\\Domain\\Attribute\\AttributeRepositoryTest::testListForAdminWhitelistsSortDirectionAndPerPage":0,"Tests\\Unit\\Domain\\Attribute\\AttributeRepositoryTest::testSaveValuesRemovesObsoleteRowsAndSetsDefault":0,"Tests\\Unit\\Domain\\Attribute\\AttributeRepositoryTest::testSaveValuesDeletesTranslationWhenNameIsEmpty":0,"Tests\\Unit\\Domain\\Attribute\\AttributeRepositoryTest::testGetAttributeValueByIdUsesDefaultLanguageWhenNotProvided":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testSortTypesReturnsExpectedKeys":0.002,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testCategoryDetailsReturnsDefaultForInvalidId":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testCategoryDetailsLoadsTranslations":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testSaveCategoriesOrderReturnsFalseForNonArray":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testSaveCategoriesOrderUpdatesOrderAndParent":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testSaveProductOrderReturnsFalseForInvalidInput":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testSaveProductOrderUpdatesCategoryProductOrder":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testCategoryDeleteReturnsFalseWhenHasChildren":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testCategoryDeleteReturnsTrueWhenDeleted":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testCategoryTitleReturnsEmptyWhenNotFound":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testCategoryTitleReturnsFirstAvailableTitle":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testListForAdminWhitelistsSortAndPagination":0.001,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testOrdersForClientReturnsEmptyOnMissingInput":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testOrdersForClientNormalizesRows":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testTotalsForClientReturnsZeroForMissingInput":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testTotalsForClientReturnsAggregatedValues":0,"Tests\\Unit\\Domain\\Dashboard\\DashboardRepositoryTest::testConstructorAcceptsDb":0.001,"Tests\\Unit\\Domain\\Dashboard\\DashboardRepositoryTest::testHasAllPublicMethods":0,"Tests\\Unit\\Domain\\Dashboard\\DashboardRepositoryTest::testSalesGridReturnsArray":0,"Tests\\Unit\\Domain\\Dashboard\\DashboardRepositoryTest::testLastOrdersReturnsArray":0,"Tests\\Unit\\Domain\\Dashboard\\DashboardRepositoryTest::testMostViewedProductsReturnsArray":0,"Tests\\Unit\\Domain\\Dashboard\\DashboardRepositoryTest::testBestSalesProductsReturnsArray":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testDefaultLanguageReturnsId":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testDefaultLanguageReturnsFallbackWhenEmpty":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testActiveLanguagesReturnsList":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testActiveLanguagesReturnsEmptyArrayWhenNone":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testTranslationsReturnsArray":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testTranslationsDefaultsToPl":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testTranslationsForDifferentLanguage":0,"Tests\\Unit\\Domain\\Newsletter\\NewsletterRepositoryTest::testUnsubscribeReturnsFalseForInvalidHash":0,"Tests\\Unit\\Domain\\Newsletter\\NewsletterRepositoryTest::testUnsubscribeDeletesSubscriber":0,"Tests\\Unit\\Domain\\Newsletter\\NewsletterRepositoryTest::testConfirmSubscriptionReturnsFalseForInvalidHash":0,"Tests\\Unit\\Domain\\Newsletter\\NewsletterRepositoryTest::testConfirmSubscriptionUpdatesStatus":0,"Tests\\Unit\\Domain\\Newsletter\\NewsletterRepositoryTest::testGetHashByEmailReturnsHash":0,"Tests\\Unit\\Domain\\Newsletter\\NewsletterRepositoryTest::testGetHashByEmailReturnsNullForMissing":0,"Tests\\Unit\\Domain\\Newsletter\\NewsletterRepositoryTest::testRemoveByEmailDeletesSubscriber":0,"Tests\\Unit\\Domain\\Newsletter\\NewsletterRepositoryTest::testRemoveByEmailReturnsFalseForMissing":0,"Tests\\Unit\\Domain\\Newsletter\\NewsletterRepositoryTest::testSignupReturnsFalseForExistingEmail":0.001,"Tests\\Unit\\Domain\\Newsletter\\NewsletterRepositoryTest::testConstructorAcceptsOptionalDependencies":0.003,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testOrderStatusesReturnsMappedArray":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testNextAndPrevOrderIdReturnNullForInvalidInput":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testListForAdminReturnsItemsAndTotal":0,"Tests\\Unit\\Domain\\Producer\\ProducerRepositoryTest::testFindReturnsDefaultProducerForInvalidId":0.001,"Tests\\Unit\\Domain\\Producer\\ProducerRepositoryTest::testFindNormalizesProducerData":0,"Tests\\Unit\\Domain\\Producer\\ProducerRepositoryTest::testSaveInsertsNewProducer":0,"Tests\\Unit\\Domain\\Producer\\ProducerRepositoryTest::testSaveUpdatesExistingProducer":0,"Tests\\Unit\\Domain\\Producer\\ProducerRepositoryTest::testDeleteReturnsFalseForInvalidId":0,"Tests\\Unit\\Domain\\Producer\\ProducerRepositoryTest::testDeleteReturnsTrueOnSuccess":0,"Tests\\Unit\\Domain\\Producer\\ProducerRepositoryTest::testListForAdminWhitelistsSortAndPagination":0,"Tests\\Unit\\Domain\\Producer\\ProducerRepositoryTest::testAllProducersReturnsFormattedList":0,"Tests\\Unit\\Domain\\Producer\\ProducerRepositoryTest::testProducerProductsReturnsPaginatedResults":0,"Tests\\Unit\\Domain\\ProductSet\\ProductSetRepositoryTest::testFindReturnsDefaultSetForInvalidId":0.001,"Tests\\Unit\\Domain\\ProductSet\\ProductSetRepositoryTest::testFindNormalizesSetData":0,"Tests\\Unit\\Domain\\ProductSet\\ProductSetRepositoryTest::testSaveInsertsNewSetAndSyncsProducts":0,"Tests\\Unit\\Domain\\ProductSet\\ProductSetRepositoryTest::testSaveUpdatesExistingSet":0,"Tests\\Unit\\Domain\\ProductSet\\ProductSetRepositoryTest::testDeleteReturnsFalseForInvalidId":0,"Tests\\Unit\\Domain\\ProductSet\\ProductSetRepositoryTest::testListForAdminWhitelistsSortAndPagination":0,"Tests\\Unit\\Domain\\ProductSet\\ProductSetRepositoryTest::testAllSetsReturnsFormattedList":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testAllProductsForMassEditReturnsMap":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testAllProductsForMassEditEmptyWhenNoProducts":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetProductsByCategoryReturnsList":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetProductsByCategoryReturnsEmptyArray":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testApplyDiscountPercentReturnsNullForInvalidProduct":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testApplyDiscountPercentReturnsCorrectPrices":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testApplyDiscountPercentZeroPercentNullsPromo":0,"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testAllSettingsReturnsAssociativeArray":0,"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testAllSettingsReturnsEmptyArrayWhenNoSettings":0,"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testAllSettingsHandlesNullFromDb":0,"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testGetSingleValueReturnsCorrectParam":0,"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testGetSingleValueUsesParamNotHardcoded":0,"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testGetSingleValueReturnsEmptyStringWhenNotFound":0,"Tests\\Unit\\Domain\\Update\\UpdateRepositoryTest::testConstructorAcceptsDb":0.002,"Tests\\Unit\\Domain\\Update\\UpdateRepositoryTest::testHasUpdateMethod":0,"Tests\\Unit\\Domain\\Update\\UpdateRepositoryTest::testUpdateReturnsArray":0,"Tests\\Unit\\Domain\\Update\\UpdateRepositoryTest::testHasRunPendingMigrationsMethod":0,"Tests\\Unit\\Domain\\Update\\UpdateRepositoryTest::testRunPendingMigrationsWithNoResults":0,"Tests\\Unit\\Domain\\Update\\UpdateRepositoryTest::testHasPrivateHelperMethods":0,"Tests\\Unit\\admin\\Controllers\\DashboardControllerTest::testConstructorAcceptsRepositories":0.002,"Tests\\Unit\\admin\\Controllers\\DashboardControllerTest::testHasMainViewMethod":0,"Tests\\Unit\\admin\\Controllers\\DashboardControllerTest::testMainViewReturnsString":0,"Tests\\Unit\\admin\\Controllers\\DashboardControllerTest::testConstructorRequiresRepositories":0,"Tests\\Unit\\admin\\Controllers\\ShopAttributeControllerTest::testConstructorAcceptsRepositories":0.003,"Tests\\Unit\\admin\\Controllers\\ShopAttributeControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopAttributeControllerTest::testHasNoLegacyAliasMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopAttributeControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\ShopAttributeControllerTest::testConstructorRequiresBothRepositories":0,"Tests\\Unit\\admin\\Controllers\\ShopAttributeControllerTest::testValidateValuesRowsReturnsErrorsForMissingDefaultLanguageAndDefaultSelection":0,"Tests\\Unit\\admin\\Controllers\\ShopAttributeControllerTest::testValidateValuesRowsReturnsEmptyArrayForValidRows":0,"Tests\\Unit\\admin\\Controllers\\ShopCategoryControllerTest::testConstructorAcceptsDependencies":0.003,"Tests\\Unit\\admin\\Controllers\\ShopCategoryControllerTest::testHasExpectedActionMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopCategoryControllerTest::testViewActionsReturnString":0,"Tests\\Unit\\admin\\Controllers\\ShopCategoryControllerTest::testMutationActionsReturnVoid":0,"Tests\\Unit\\admin\\Controllers\\ShopCategoryControllerTest::testConstructorRequiresCategoryAndLanguagesRepositories":0,"Tests\\Unit\\admin\\Controllers\\ShopClientsControllerTest::testConstructorAcceptsRepository":0.002,"Tests\\Unit\\admin\\Controllers\\ShopClientsControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopClientsControllerTest::testHasLegacyAliasMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopClientsControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\ShopClientsControllerTest::testConstructorRequiresClientRepository":0,"Tests\\Unit\\admin\\Controllers\\ShopOrderControllerTest::testConstructorAcceptsService":0.003,"Tests\\Unit\\admin\\Controllers\\ShopOrderControllerTest::testHasExpectedActionMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopOrderControllerTest::testViewActionsReturnString":0,"Tests\\Unit\\admin\\Controllers\\ShopOrderControllerTest::testMutationActionsReturnVoid":0,"Tests\\Unit\\admin\\Controllers\\ShopOrderControllerTest::testConstructorRequiresOrderAdminService":0,"Tests\\Unit\\admin\\Controllers\\ShopProducerControllerTest::testConstructorAcceptsRepositories":0.002,"Tests\\Unit\\admin\\Controllers\\ShopProducerControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopProducerControllerTest::testHasLegacyAliasMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopProducerControllerTest::testActionMethodReturnTypes":0.001,"Tests\\Unit\\admin\\Controllers\\ShopProducerControllerTest::testConstructorRequiresBothRepositories":0,"Tests\\Unit\\admin\\Controllers\\ShopProductControllerTest::testConstructorAcceptsRepositories":0.003,"Tests\\Unit\\admin\\Controllers\\ShopProductControllerTest::testHasMassEditActionMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopProductControllerTest::testHasViewListMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopProductControllerTest::testHasEditAndSaveMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopProductControllerTest::testHasOperationMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopProductControllerTest::testHasCombinationMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopProductControllerTest::testHasImageAndFileMethods":0.001,"Tests\\Unit\\admin\\Controllers\\ShopProductControllerTest::testMassEditReturnsString":0,"Tests\\Unit\\admin\\Controllers\\ShopProductControllerTest::testMassEditSaveReturnsVoid":0,"Tests\\Unit\\admin\\Controllers\\ShopProductControllerTest::testGetProductsByCategoryReturnsVoid":0,"Tests\\Unit\\admin\\Controllers\\ShopProductControllerTest::testConstructorRequiresRepositories":0,"Tests\\Unit\\admin\\Controllers\\ShopProductControllerTest::testHasFormBuildingHelpers":0,"Tests\\Unit\\admin\\Controllers\\ShopProductControllerTest::testSaveMethodReturnsVoid":0,"Tests\\Unit\\admin\\Controllers\\ShopProductSetsControllerTest::testConstructorAcceptsRepository":0.002,"Tests\\Unit\\admin\\Controllers\\ShopProductSetsControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopProductSetsControllerTest::testHasLegacyAliasMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopProductSetsControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\ShopProductSetsControllerTest::testConstructorRequiresProductSetRepository":0,"Tests\\Unit\\admin\\Controllers\\UpdateControllerTest::testConstructorAcceptsRepository":0.001,"Tests\\Unit\\admin\\Controllers\\UpdateControllerTest::testHasMainViewMethod":0,"Tests\\Unit\\admin\\Controllers\\UpdateControllerTest::testMainViewReturnsString":0,"Tests\\Unit\\admin\\Controllers\\UpdateControllerTest::testHasUpdateMethod":0,"Tests\\Unit\\admin\\Controllers\\UpdateControllerTest::testHasUpdateAllMethod":0,"Tests\\Unit\\admin\\Controllers\\UpdateControllerTest::testConstructorRequiresRepository":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testArticleDetailsFrontendReturnsArticleWithRelations":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testArticleDetailsFrontendReturnsNullForMissing":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testArticleDetailsFrontendCopyFromFallback":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testArticlesIdsReturnsSortedIds":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testArticlesIdsReturnsNullForEmpty":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testPageArticlesCountReturnsInt":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testPageArticlesCountReturnsZeroForEmpty":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testPageArticlesPagination":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testArticleNoindexReturnsBool":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testArticleNoindexReturnsFalseForNonNoindex":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testNewsReturnsArticlesArray":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testTopArticlesOrderByViews":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testNewsListArticlesOrderByDateDesc":0,"Tests\\Unit\\Domain\\Banner\\BannerRepositoryTest::testBannersReturnsActiveBannersWithFlatLanguages":0,"Tests\\Unit\\Domain\\Banner\\BannerRepositoryTest::testBannersReturnsNullWhenNoBanners":0,"Tests\\Unit\\Domain\\Banner\\BannerRepositoryTest::testMainBannerReturnsActiveBannerWithFlatLanguages":0,"Tests\\Unit\\Domain\\Banner\\BannerRepositoryTest::testMainBannerReturnsNullWhenNoBanner":0,"Tests\\Unit\\Domain\\Layouts\\LayoutsRepositoryTest::testCategoryDefaultLayoutIdReturnsId":0,"Tests\\Unit\\Domain\\Layouts\\LayoutsRepositoryTest::testCategoryDefaultLayoutIdReturnsNullWhenNone":0,"Tests\\Unit\\Domain\\Layouts\\LayoutsRepositoryTest::testGetDefaultLayoutReturnsLayoutFromDb":0,"Tests\\Unit\\Domain\\Layouts\\LayoutsRepositoryTest::testGetDefaultLayoutReturnsNullWhenNoLayout":0,"Tests\\Unit\\Domain\\Layouts\\LayoutsRepositoryTest::testGetArticleLayoutReturnsLayoutFromDb":0,"Tests\\Unit\\Domain\\Layouts\\LayoutsRepositoryTest::testGetArticleLayoutReturnsNullWhenNoLayout":0,"Tests\\Unit\\Domain\\Layouts\\LayoutsRepositoryTest::testGetActiveLayoutFallsBackToDefault":0,"Tests\\Unit\\Domain\\Layouts\\LayoutsRepositoryTest::testGetActiveLayoutReturnsNullWhenNothingFound":0,"Tests\\Unit\\Domain\\Pages\\PagesRepositoryTest::testFrontPageDetailsReturnsPageWithLanguage":0,"Tests\\Unit\\Domain\\Pages\\PagesRepositoryTest::testFrontPageDetailsReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\Pages\\PagesRepositoryTest::testFrontMainPageIdReturnsStartPage":0,"Tests\\Unit\\Domain\\Pages\\PagesRepositoryTest::testFrontMainPageIdFallsBackToFirstActive":0,"Tests\\Unit\\Domain\\Pages\\PagesRepositoryTest::testFrontPageSortReturnsValue":0,"Tests\\Unit\\Domain\\Pages\\PagesRepositoryTest::testFrontMenuDetailsReturnsMenuWithPages":0,"Tests\\Unit\\Domain\\Pages\\PagesRepositoryTest::testFrontMenuDetailsReturnsNullForInvalidMenu":0,"Tests\\Unit\\Domain\\Pages\\PagesRepositoryTest::testFrontMenuPagesReturnsEmptyForNoPages":0,"Tests\\Unit\\Domain\\Scontainers\\ScontainersRepositoryTest::testFrontScontainerDetailsReturnsContainerWithLanguage":0,"Tests\\Unit\\Domain\\Scontainers\\ScontainersRepositoryTest::testFrontScontainerDetailsReturnsFallbackForNotFound":0,"Tests\\Unit\\Domain\\Attribute\\AttributeRepositoryTest::testFrontAttributeDetailsReturnsAttributeWithLanguage":0,"Tests\\Unit\\Domain\\Attribute\\AttributeRepositoryTest::testFrontAttributeDetailsReturnsFallbackForNotFound":0,"Tests\\Unit\\Domain\\Attribute\\AttributeRepositoryTest::testFrontValueDetailsReturnsValueWithLanguage":0,"Tests\\Unit\\Domain\\Attribute\\AttributeRepositoryTest::testFrontValueDetailsReturnsFallbackForNotFound":0,"Tests\\Unit\\Domain\\Basket\\BasketCalculatorTest::testSummaryWpCalculatesTotal":0.001,"Tests\\Unit\\Domain\\Basket\\BasketCalculatorTest::testSummaryWpReturnsZeroForEmptyBasket":0,"Tests\\Unit\\Domain\\Basket\\BasketCalculatorTest::testCountProductsSumsQuantities":0,"Tests\\Unit\\Domain\\Basket\\BasketCalculatorTest::testCountProductsReturnsZeroForEmptyBasket":0,"Tests\\Unit\\Domain\\Basket\\BasketCalculatorTest::testCountProductsTextSingular":0,"Tests\\Unit\\Domain\\Basket\\BasketCalculatorTest::testCountProductsTextPlural2to4":0,"Tests\\Unit\\Domain\\Basket\\BasketCalculatorTest::testCountProductsTextPlural5Plus":0,"Tests\\Unit\\Domain\\Basket\\BasketCalculatorTest::testCountProductsTextCastsToInt":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testGetCategorySortReturnsZeroForInvalidId":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testGetCategorySortReturnsSortType":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testCategoryNameReturnsEmptyForInvalidInput":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testCategoryNameReturnsTitle":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testCategoryNameReturnsEmptyWhenNotFound":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testFrontCategoryDetailsReturnsEmptyForInvalidId":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testFrontCategoryDetailsReturnsCategoryWithLanguage":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testFrontCategoryDetailsReturnsEmptyWhenCategoryNotFound":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testCategoriesTreeReturnsEmptyWhenNoCategories":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testCategoryProductsCountReturnsZeroForInvalidInput":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testCategoryProductsCountReturnsCount":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testProductsIdReturnsEmptyForInvalidInput":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testProductsIdReturnsProductIds":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testBlogCategoryProductsReturnsEmptyForInvalidInput":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testBlogCategoryProductsReturnsIds":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testPaginatedCategoryProductsReturnsEmptyWhenNoProducts":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testPaginatedCategoryProductsClampsPage":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testClientDetailsReturnsNullForInvalidId":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testClientDetailsReturnsRowOnSuccess":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testClientDetailsReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testClientEmailReturnsNullForInvalidId":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testClientEmailReturnsStringOnSuccess":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testClientEmailReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testClientAddressesReturnsEmptyForInvalidId":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testClientAddressesReturnsRows":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testClientAddressesHandlesFalseFromDb":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testAddressDetailsReturnsNullForInvalidId":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testAddressDetailsReturnsRow":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testAddressDeleteReturnsFalseForInvalidId":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testAddressDeleteReturnsTrueOnSuccess":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testAddressSaveReturnsFalseForInvalidClientId":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testAddressSaveInsertsNewAddress":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testAddressSaveUpdatesExistingAddress":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testMarkAddressAsCurrentReturnsFalseForInvalidIds":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testMarkAddressAsCurrentResetsAndSets":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testAuthenticateReturnsErrorOnEmptyInput":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testAuthenticateReturnsErrorWhenClientNotFound":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testAuthenticateReturnsInactiveForUnconfirmedAccount":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testAuthenticateReturnsErrorOnWrongPassword":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testAuthenticateReturnsOkOnSuccess":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testCreateClientReturnsNullOnEmptyInput":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testCreateClientReturnsNullWhenEmailTaken":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testCreateClientReturnsIdAndHashOnSuccess":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testConfirmRegistrationReturnsNullOnEmptyHash":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testConfirmRegistrationReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testConfirmRegistrationActivatesAndReturnsEmail":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testGenerateNewPasswordReturnsNullOnEmptyHash":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testGenerateNewPasswordReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testGenerateNewPasswordReturnsEmailAndPassword":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testInitiatePasswordRecoveryReturnsNullOnEmptyEmail":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testInitiatePasswordRecoveryReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testInitiatePasswordRecoverySetsRecoveryFlagAndReturnsHash":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testClientOrdersReturnsEmptyForInvalidId":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testFindByNameReturnsObjectWhenFound":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testFindByNameReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testFindByNameReturnsNullForEmptyName":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testIsAvailableReturnsTrueForActiveCoupon":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testIsAvailableReturnsFalseForUsedCoupon":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testIsAvailableReturnsFalseForInactiveCoupon":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testIsAvailableReturnsFalseForNullCoupon":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testIsAvailableWorksWithArray":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testMarkAsUsedCallsUpdate":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testMarkAsUsedSkipsInvalidId":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testIncrementUsedCountCallsUpdate":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testIncrementUsedCountSkipsInvalidId":0,"Tests\\Unit\\front\\Controllers\\ShopCouponControllerTest::testConstructorAcceptsRepository":0.001,"Tests\\Unit\\front\\Controllers\\ShopCouponControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\front\\Controllers\\ShopCouponControllerTest::testConstructorRequiresCouponRepository":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testFindIdByHashReturnsIdWhenFound":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testFindIdByHashReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testFindIdByHashReturnsNullForEmptyHash":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testFindHashByIdReturnsHashWhenFound":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testFindHashByIdReturnsNullForInvalidId":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testOrderDetailsFrontendByIdReturnsArrayWithProducts":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testOrderDetailsFrontendByHashReturnsArrayWithProducts":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testOrderDetailsFrontendReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testGenerateOrderNumberFormatsCorrectly":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testGenerateOrderNumberStartsAt001":0,"Tests\\Unit\\Domain\\Producer\\ProducerRepositoryTest::testAllActiveProducersReturnsFullData":0,"Tests\\Unit\\Domain\\Producer\\ProducerRepositoryTest::testAllActiveProducersReturnsEmptyOnNull":0,"Tests\\Unit\\Domain\\Producer\\ProducerRepositoryTest::testFindForFrontendReturnsNullForInvalidId":0,"Tests\\Unit\\Domain\\Producer\\ProducerRepositoryTest::testFindForFrontendReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\Producer\\ProducerRepositoryTest::testFindForFrontendReturnsProducerWithLanguage":0,"Tests\\Unit\\front\\Controllers\\ShopOrderControllerTest::testConstructorAcceptsRepository":0.001,"Tests\\Unit\\front\\Controllers\\ShopOrderControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\front\\Controllers\\ShopOrderControllerTest::testConstructorRequiresOrderRepository":0,"Tests\\Unit\\front\\Controllers\\ShopProducerControllerTest::testConstructorAcceptsRepository":0.001,"Tests\\Unit\\front\\Controllers\\ShopProducerControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\front\\Controllers\\ShopProducerControllerTest::testConstructorRequiresProducerRepository":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testIsActiveReturnsOneForActivePayment":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testIsActiveReturnsZeroForInvalidId":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testFindActiveByIdReturnsNormalizedData":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testFindActiveByIdReturnsNullForInvalidId":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testAllActiveReturnsEmptyOnNull":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testGetApiloPaymentTypeIdReturnsNullForInvalidId":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testForTransportReturnsEmptyForInvalidId":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetSkuWithFallbackReturnsSku":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetSkuWithFallbackFromParent":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetSkuWithFallbackReturnsNullForInvalidId":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetEanWithFallbackReturnsEan":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetEanWithFallbackFromParent":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testIsProductActiveCachedReturnsOneForActive":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testIsProductActiveCachedReturnsZeroForInactive":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testIsProductActiveCachedReturnsZeroForInvalidId":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testProductCategoriesFrontReturnsCategories":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testProductCategoriesFrontUsesParentId":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testProductCategoriesFrontReturnsEmptyForInvalidId":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetWarehouseMessageZeroReturnsMessage":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetWarehouseMessageZeroReturnsNullForInvalidId":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetWarehouseMessageNonzeroReturnsMessage":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetWarehouseMessageNonzeroReturnsNullForInvalidId":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testTopProductIdsReturnsActiveProducts":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testNewProductIdsReturnsProductIds":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testNewProductIdsReturnsEmptyWhenNoProducts":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testPromotedProductIdsCachedReturnsIds":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testPromotedProductIdsCachedReturnsEmptyWhenNone":0,"Tests\\Unit\\Domain\\Promotion\\PromotionRepositoryTest::testApplyTypeWholeBasketAppliesDiscountToAll":0,"Tests\\Unit\\Domain\\Promotion\\PromotionRepositoryTest::testApplyTypeCategoriesOrAppliesDiscountToMatchingCategories":0,"Tests\\Unit\\Domain\\Promotion\\PromotionRepositoryTest::testApplyTypeCategoryConditionAppliesWhenConditionMet":0,"Tests\\Unit\\Domain\\Promotion\\PromotionRepositoryTest::testApplyTypeCategoryConditionNoDiscountWhenConditionNotMet":0,"Tests\\Unit\\Domain\\Promotion\\PromotionRepositoryTest::testApplyTypeCategoriesAndAppliesWhenBothConditionsMet":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testTransportCostCachedReturnsCost":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testFindActiveByIdCachedReturnsTransport":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testFindActiveByIdCachedReturnsNullForInvalid":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testForPaymentMethodReturnsTransports":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testForPaymentMethodReturnsEmptyForInvalidId":0,"Tests\\Unit\\Domain\\Basket\\BasketCalculatorTest::testCheckProductQuantityInStockReturnsFalseOnNullBasket":0,"Tests\\Unit\\Domain\\Basket\\BasketCalculatorTest::testCheckProductQuantityInStockReturnsFalseOnEmptyBasket":0,"Tests\\Unit\\Domain\\Basket\\BasketCalculatorTest::testValidateBasketReturnsEmptyArrayOnNull":0,"Tests\\Unit\\Domain\\Basket\\BasketCalculatorTest::testValidateBasketReturnsBasketArrayAsIs":0,"Tests\\Unit\\Domain\\Order\\OrderAdminServiceTest::testConstructorAcceptsOnlyOrderRepository":0.006,"Tests\\Unit\\Domain\\Order\\OrderAdminServiceTest::testConstructorAcceptsAllDependencies":0.012,"Tests\\Unit\\Domain\\Order\\OrderAdminServiceTest::testSearchProductsReturnsEmptyForEmptyQuery":0,"Tests\\Unit\\Domain\\Order\\OrderAdminServiceTest::testSearchProductsReturnsEmptyWithoutProductRepo":0,"Tests\\Unit\\Domain\\Order\\OrderAdminServiceTest::testSearchProductsReturnsFormattedResults":0,"Tests\\Unit\\Domain\\Order\\OrderAdminServiceTest::testSaveOrderProductsReturnsFalseForInvalidOrderId":0,"Tests\\Unit\\Domain\\Order\\OrderAdminServiceTest::testSaveOrderProductsDeletesRemovedProducts":0,"Tests\\Unit\\Domain\\Order\\OrderAdminServiceTest::testSaveOrderProductsUpdatesQuantityAndAdjustsStock":0,"Tests\\Unit\\Domain\\Order\\OrderAdminServiceTest::testSaveOrderProductsAddsNewProductAndDecreasesStock":0,"Tests\\Unit\\Domain\\Order\\OrderAdminServiceTest::testGetFreeDeliveryThresholdReturnsZeroWithoutSettingsRepo":0,"Tests\\Unit\\Domain\\Order\\OrderAdminServiceTest::testGetFreeDeliveryThresholdReturnsValue":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testGetOrderProductReturnsNullForInvalidId":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testGetOrderProductReturnsArray":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testAddOrderProductReturnsNullForInvalidOrderId":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testAddOrderProductInsertsAndReturnsId":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testUpdateOrderProductReturnsFalseForInvalidId":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testUpdateOrderProductUpdatesFields":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testUpdateOrderProductReturnsFalseForEmptyData":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testDeleteOrderProductReturnsFalseForInvalidId":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testDeleteOrderProductCallsDelete":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testUpdateTransportCostDoesNothingForInvalidId":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testUpdateTransportCostUpdatesOrder":0,"Tests\\Unit\\api\\ApiRouterTest::testHandleReturns401WhenNoApiKey":0.001,"Tests\\Unit\\api\\ApiRouterTest::testHandleReturns401WhenWrongApiKey":0,"Tests\\Unit\\api\\ApiRouterTest::testHandleReturns401WhenStoredKeyEmpty":0,"Tests\\Unit\\api\\ApiRouterTest::testHandleReturns400WhenMissingEndpoint":0,"Tests\\Unit\\api\\ApiRouterTest::testHandleReturns400WhenMissingAction":0,"Tests\\Unit\\api\\ApiRouterTest::testHandleReturns404ForUnknownEndpoint":0,"Tests\\Unit\\api\\ApiRouterTest::testSendSuccessOutputsCorrectJson":0,"Tests\\Unit\\api\\ApiRouterTest::testSendErrorOutputsCorrectJson":0,"Tests\\Unit\\api\\ApiRouterTest::testRequireMethodReturnsTrueForMatchingMethod":0,"Tests\\Unit\\api\\ApiRouterTest::testRequireMethodReturnsFalseAndSendsErrorForMismatch":0,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testStatusesReturnsFormattedList":0.001,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testStatusesRejectsPostMethod":0,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testTransportsReturnsFormattedList":0,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testTransportsRejectsPostMethod":0,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testPaymentMethodsReturnsFormattedList":0,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testPaymentMethodsRejectsPostMethod":0,"Tests\\Unit\\api\\Controllers\\OrdersApiControllerTest::testListReturnsOrders":0.001,"Tests\\Unit\\api\\Controllers\\OrdersApiControllerTest::testListRejectsPostMethod":0,"Tests\\Unit\\api\\Controllers\\OrdersApiControllerTest::testListPassesFiltersToRepository":0,"Tests\\Unit\\api\\Controllers\\OrdersApiControllerTest::testGetReturnsOrder":0,"Tests\\Unit\\api\\Controllers\\OrdersApiControllerTest::testGetReturns404WhenOrderNotFound":0,"Tests\\Unit\\api\\Controllers\\OrdersApiControllerTest::testGetReturns400WhenMissingId":0,"Tests\\Unit\\api\\Controllers\\OrdersApiControllerTest::testChangeStatusUpdatesOrder":0,"Tests\\Unit\\api\\Controllers\\OrdersApiControllerTest::testChangeStatusReturns400WhenMissingId":0,"Tests\\Unit\\api\\Controllers\\OrdersApiControllerTest::testChangeStatusRejectsGetMethod":0,"Tests\\Unit\\api\\Controllers\\OrdersApiControllerTest::testSetPaidReturns404WhenOrderNotFound":0,"Tests\\Unit\\api\\Controllers\\OrdersApiControllerTest::testSetPaidReturns400WhenMissingId":0,"Tests\\Unit\\api\\Controllers\\OrdersApiControllerTest::testSetPaidCallsServiceWhenOrderExists":0,"Tests\\Unit\\api\\Controllers\\OrdersApiControllerTest::testSetUnpaidReturns404WhenOrderNotFound":0,"Tests\\Unit\\api\\Controllers\\OrdersApiControllerTest::testSetUnpaidCallsServiceWhenOrderExists":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testListReturnsProducts":0.002,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testListRejectsPostMethod":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testListPassesFiltersToRepository":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testListDefaultPagination":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testListClampsPerPageTo100":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testGetReturnsProduct":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testGetReturns404WhenProductNotFound":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testGetReturns400WhenMissingId":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testGetRejectsPostMethod":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testCreateRejectsGetMethod":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testCreateReturns400WhenNoBody":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testUpdateRejectsGetMethod":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testUpdateReturns400WhenMissingId":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testUpdateReturns404WhenProductNotFound":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testUpdateReturns400WhenNoBody":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testMapApiToFormDataConvertsStatusToCheckbox":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testMapApiToFormDataMapsLanguages":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testMapApiToFormDataMapsNumericFields":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testMapApiToFormDataMapsCategories":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testMapApiToFormDataPartialUpdatePreservesExisting":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testMapApiToFormDataMapsForeignKeys":0,"Tests\\Unit\\Domain\\Update\\UpdateRepositoryTest::testVerifyChecksumValidFormat":0.013,"Tests\\Unit\\Domain\\Update\\UpdateRepositoryTest::testVerifyChecksumInvalidHash":0.011,"Tests\\Unit\\Domain\\Update\\UpdateRepositoryTest::testVerifyChecksumInvalidFormat":0.001,"Tests\\Unit\\Domain\\Update\\UpdateRepositoryTest::testCreateBackupWithEmptyManifest":0,"Tests\\Unit\\Domain\\Update\\UpdateRepositoryTest::testDownloadManifestReturnsNullForInvalidUrl":0.281,"Tests\\Unit\\Domain\\Attribute\\AttributeRepositoryTest::testListForApiReturnsActiveAttributesWithValues":0,"Tests\\Unit\\Domain\\Attribute\\AttributeRepositoryTest::testListForApiReturnsEmptyWhenNoAttributes":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testFindVariantsForApiReturnsVariants":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testFindVariantsForApiReturnsEmptyWhenNoVariants":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testFindVariantForApiReturnsVariant":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testFindVariantForApiReturnsNullForNonVariant":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testFindVariantForApiReturnsNullForNonexistent":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testCreateVariantForApiSuccess":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testCreateVariantForApiReturnsNullForArchivedParent":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testCreateVariantForApiReturnsNullWhenParentIsVariant":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testCreateVariantForApiReturnsNullForEmptyAttributes":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testCreateVariantForApiReturnsNullForDuplicateHash":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testUpdateVariantForApiSuccess":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testUpdateVariantForApiReturnsFalseForNonVariant":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testUpdateVariantForApiReturnsFalseForNonexistent":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testUpdateVariantForApiFiltersUnallowedFields":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testDeleteVariantForApiSuccess":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testDeleteVariantForApiReturnsFalseForNonVariant":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testDeleteVariantForApiReturnsFalseForNonexistent":0,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testAttributesReturnsFormattedList":0,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testAttributesRejectsPostMethod":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testVariantsReturnsVariantsList":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testVariantsReturns400WhenMissingId":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testVariantsReturns404WhenProductNotFound":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testVariantsReturns400ForVariantProduct":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testVariantsRejectsPostMethod":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testCreateVariantRejectsGetMethod":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testCreateVariantReturns400WhenMissingId":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testCreateVariantReturns400WhenNoBody":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testUpdateVariantRejectsGetMethod":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testUpdateVariantReturns400WhenMissingId":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testUpdateVariantReturns400WhenNoBody":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testDeleteVariantRejectsGetMethod":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testDeleteVariantReturns400WhenMissingId":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testDeleteVariantReturns404WhenNotFound":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testDeleteVariantSuccess":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testListPassesAttributeFilters":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testUpdateVariantForApiCastsTypes":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testSavePersistsMinMaxOrderAmount":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testSaveConvertsEmptyMinMaxToNull":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testFindNormalizesMinMaxOrderAmount":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testFindNormalizesNullMinMaxOrderAmount":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testTransportMethodsFrontHidesTransportWhenAllPaymentsExceedMaxAmount":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testTransportMethodsFrontHidesTransportWhenAllPaymentsBelowMinAmount":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testTransportMethodsFrontHidesTransportWithNoPaymentMethods":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testTransportMethodsFrontKeepsBothTransportsWhenPaymentsAvailable":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testTransportMethodsFrontKeepsTransportIfAtLeastOnePaymentAvailable":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testOrderStatusDataReturnsBothNamesAndColors":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testOrderStatusDataFiltersInvalidHexColors":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testOrderStatusDataReturnsEmptyOnDbFailure":0,"Tests\\Unit\\admin\\Controllers\\ShopOrderControllerTest::testContrastTextColorReturnsBlackForLightColor":0,"Tests\\Unit\\admin\\Controllers\\ShopOrderControllerTest::testContrastTextColorReturnsWhiteForDarkColor":0,"Tests\\Unit\\admin\\Controllers\\ShopOrderControllerTest::testContrastTextColorHandlesShortHex":0,"Tests\\Unit\\admin\\Controllers\\ShopOrderControllerTest::testContrastTextColorDefaultsToWhiteForInvalidHex":0,"Tests\\Unit\\admin\\Controllers\\ShopOrderControllerTest::testSanitizeInlineHtmlStripsDisallowedTags":0,"Tests\\Unit\\admin\\Controllers\\ShopOrderControllerTest::testSanitizeInlineHtmlStripsAttributesFromAllowedTags":0,"Tests\\Unit\\admin\\Controllers\\ShopOrderControllerTest::testSanitizeInlineHtmlPreservesCleanTags":0,"Tests\\Unit\\admin\\Controllers\\ShopOrderControllerTest::testSanitizeInlineHtmlHandlesPlainText":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testGetLogsReturnsItemsAndTotal":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testGetLogsReturnsEmptyWhenNoResults":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testGetLogsHandlesNullFromSelect":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testDeleteLogCallsDelete":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testClearLogsDeletesAll":0,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testHasLogsMethods":0,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testLogsReturnsString":0,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testLogsClearReturnsVoid":0,"Tests\\Unit\\Domain\\Order\\OrderAdminServiceTest::testProcessApiloSyncQueueKeepsTaskWhenApiloOrderIdIsNull":0.023,"Tests\\Unit\\Domain\\Order\\OrderAdminServiceTest::testProcessApiloSyncQueueRemovesTaskAfterMaxAttempts":0.014,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testMapApiToFormDataPreservesZeroBasePriceForSaveProduct":0,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testEnsureAttributeRejectsGetMethod":0,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testEnsureAttributeReturns400WhenNoBody":0,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testEnsureAttributeValueRejectsGetMethod":0,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testEnsureAttributeValueReturns400WhenNoBody":0,"Tests\\Unit\\Domain\\CronJob\\CronJobProcessorTest::testRegisterHandlerAndProcessJob":0.003,"Tests\\Unit\\Domain\\CronJob\\CronJobProcessorTest::testProcessQueueReturnsEmptyStatsWhenNoJobs":0,"Tests\\Unit\\Domain\\CronJob\\CronJobProcessorTest::testProcessQueueHandlerReturnsFalse":0,"Tests\\Unit\\Domain\\CronJob\\CronJobProcessorTest::testProcessQueueHandlerThrowsException":0,"Tests\\Unit\\Domain\\CronJob\\CronJobProcessorTest::testProcessQueueNoHandlerRegistered":0,"Tests\\Unit\\Domain\\CronJob\\CronJobProcessorTest::testProcessQueueHandlerReturnsArray":0,"Tests\\Unit\\Domain\\CronJob\\CronJobProcessorTest::testProcessQueuePassesPayloadToHandler":0,"Tests\\Unit\\Domain\\CronJob\\CronJobProcessorTest::testProcessQueueMultipleJobs":0,"Tests\\Unit\\Domain\\CronJob\\CronJobProcessorTest::testCreateScheduledJobsFromDueSchedules":0,"Tests\\Unit\\Domain\\CronJob\\CronJobProcessorTest::testCreateScheduledJobsSkipsDuplicates":0,"Tests\\Unit\\Domain\\CronJob\\CronJobProcessorTest::testCreateScheduledJobsWithPayload":0,"Tests\\Unit\\Domain\\CronJob\\CronJobProcessorTest::testCreateScheduledJobsReturnsZeroWhenNoSchedules":0,"Tests\\Unit\\Domain\\CronJob\\CronJobProcessorTest::testRunExecutesFullPipeline":0,"Tests\\Unit\\Domain\\CronJob\\CronJobProcessorTest::testRunReturnsScheduledCount":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testEnqueueInsertsJobAndReturnsId":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testEnqueueWithPayloadEncodesJson":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testEnqueueWithoutPayloadDoesNotSetPayloadKey":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testEnqueueWithScheduledAt":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testEnqueueReturnsNullOnFailure":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testFetchNextReturnsEmptyArrayWhenNoJobs":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testFetchNextUpdatesStatusToProcessing":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testFetchNextDecodesPayloadJson":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testMarkCompletedUpdatesStatus":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testMarkCompletedWithResult":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testMarkFailedWithRetriesLeft":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testMarkFailedWhenMaxAttemptsReached":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testMarkFailedTruncatesErrorTo500Chars":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testHasPendingJobReturnsTrueWhenExists":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testHasPendingJobReturnsFalseWhenNone":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testHasPendingJobWithPayloadMatch":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testCleanupDeletesOldCompletedJobs":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testRecoverStuckResetsProcessingJobs":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testGetDueSchedulesReturnsEnabledSchedules":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testTouchScheduleUpdatesTimestamps":0,"Tests\\Unit\\Domain\\CronJob\\CronJobTypeTest::testAllTypesReturnsAllJobTypes":0,"Tests\\Unit\\Domain\\CronJob\\CronJobTypeTest::testAllStatusesReturnsAllStatuses":0,"Tests\\Unit\\Domain\\CronJob\\CronJobTypeTest::testPriorityConstants":0,"Tests\\Unit\\Domain\\CronJob\\CronJobTypeTest::testCalculateBackoffExponential":0,"Tests\\Unit\\Domain\\CronJob\\CronJobTypeTest::testCalculateBackoffCapsAtMax":0,"Tests\\Unit\\Domain\\CronJob\\CronJobTypeTest::testJobTypeConstantsMatchStrings":0,"Tests\\Unit\\Domain\\CronJob\\CronJobTypeTest::testStatusConstantsMatchStrings":0,"Tests\\Unit\\Domain\\Order\\OrderAdminServiceTest::testConstructorAcceptsCronJobRepo":0,"Tests\\Unit\\admin\\Controllers\\ProductArchiveControllerTest::testHasBulkDeletePermanentMethod":0,"Tests\\Unit\\admin\\Controllers\\ProductArchiveControllerTest::testBulkDeletePermanentMethodReturnType":0}} \ No newline at end of file +{"version":1,"defects":{"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveCreatesNewArticle":3,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveUpdatesExistingArticle":3,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveTranslationsInsertsForNewArticle":4,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveTranslationsUpsertsForExistingArticle":4,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSavePagesForNewArticle":4,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveDeletesMarkedImagesOnUpdate":4,"Tests\\Unit\\Domain\\Scontainers\\ScontainersRepositoryTest::testFrontScontainerDetailsReturnsContainerWithLanguage":3,"Tests\\Unit\\front\\Controllers\\ShopCouponControllerTest::testConstructorAcceptsRepository":4,"Tests\\Unit\\front\\Controllers\\ShopCouponControllerTest::testHasMainActionMethods":4,"Tests\\Unit\\front\\Controllers\\ShopCouponControllerTest::testConstructorRequiresCouponRepository":4,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testArticleDetailsFrontendReturnsArticleWithRelations":3,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testArticleDetailsFrontendCopyFromFallback":4,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testTopArticlesOrderByViews":4,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testNewsListArticlesOrderByDateDesc":4,"Tests\\Unit\\api\\ApiRouterTest::testHandleReturns401WhenNoApiKey":4,"Tests\\Unit\\api\\ApiRouterTest::testHandleReturns401WhenWrongApiKey":4,"Tests\\Unit\\api\\ApiRouterTest::testHandleReturns401WhenStoredKeyEmpty":4,"Tests\\Unit\\api\\ApiRouterTest::testHandleReturns400WhenMissingEndpoint":4,"Tests\\Unit\\api\\ApiRouterTest::testHandleReturns400WhenMissingAction":4,"Tests\\Unit\\api\\ApiRouterTest::testHandleReturns404ForUnknownEndpoint":4,"Tests\\Unit\\Domain\\Order\\OrderAdminServiceTest::testProcessApiloSyncQueueKeepsTaskWhenApiloOrderIdIsNull":3,"Tests\\Unit\\Domain\\Order\\OrderAdminServiceTest::testProcessApiloSyncQueueRemovesTaskAfterMaxAttempts":3,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testStatusesReturnsFormattedList":4,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testStatusesRejectsPostMethod":4,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testTransportsReturnsFormattedList":4,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testTransportsRejectsPostMethod":4,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testPaymentMethodsReturnsFormattedList":4,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testPaymentMethodsRejectsPostMethod":4,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testAttributesReturnsFormattedList":4,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testAttributesRejectsPostMethod":4,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testEnsureAttributeRejectsGetMethod":4,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testEnsureAttributeReturns400WhenNoBody":4,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testEnsureAttributeValueRejectsGetMethod":4,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testEnsureAttributeValueReturns400WhenNoBody":4,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testFetchNextUpdatesStatusToProcessing":3,"Tests\\Unit\\admin\\Controllers\\UpdateControllerTest::testConstructorAcceptsRepository":4,"Tests\\Unit\\admin\\Controllers\\UpdateControllerTest::testHasMainViewMethod":4,"Tests\\Unit\\admin\\Controllers\\UpdateControllerTest::testMainViewReturnsString":4,"Tests\\Unit\\admin\\Controllers\\UpdateControllerTest::testHasUpdateMethod":4,"Tests\\Unit\\admin\\Controllers\\UpdateControllerTest::testHasUpdateAllMethod":4,"Tests\\Unit\\admin\\Controllers\\UpdateControllerTest::testConstructorRequiresRepository":4,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testDeletePermanentlyRemovesArticleAndRelations":3,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testCategoryDeleteReturnsTrueWhenDeleted":3},"times":{"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetQuantityReturnsCorrectValue":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetQuantityReturnsNullWhenProductNotFound":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testFindReturnsProductData":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testUpdateQuantitySuccess":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetPriceReturnsRegularPrice":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetPriceReturnsPromoPrice":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetPriceReturnsRegularWhenPromoIsHigher":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetPriceReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetNameReturnsProductName":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetNameReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetQuantityReturnsInteger":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testUnarchiveUpdatesProductAndChildren":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testArchiveUpdatesProductAndChildren":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testUnarchiveReturnsBool":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testArchiveReturnsBool":0,"Tests\\Unit\\admin\\Controllers\\ProductArchiveControllerTest::testConstructorAcceptsRepository":0.001,"Tests\\Unit\\admin\\Controllers\\ProductArchiveControllerTest::testHasListMethod":0,"Tests\\Unit\\admin\\Controllers\\ProductArchiveControllerTest::testHasUnarchiveMethod":0,"Tests\\Unit\\admin\\Controllers\\ProductArchiveControllerTest::testListMethodReturnType":0,"Tests\\Unit\\admin\\Controllers\\ProductArchiveControllerTest::testUnarchiveMethodReturnType":0,"Tests\\Unit\\admin\\Controllers\\ProductArchiveControllerTest::testConstructorRequiresProductRepository":0,"Tests\\Unit\\Domain\\Banner\\BannerRepositoryTest::testFindReturnsBannerWithTranslations":0.001,"Tests\\Unit\\Domain\\Banner\\BannerRepositoryTest::testFindReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\Banner\\BannerRepositoryTest::testDeleteReturnsTrue":0.002,"Tests\\Unit\\Domain\\Banner\\BannerRepositoryTest::testSaveInsertsNewBanner":0,"Tests\\Unit\\Domain\\Cache\\CacheRepositoryTest::testClearCacheWithRedis":0.001,"Tests\\Unit\\Domain\\Cache\\CacheRepositoryTest::testClearCacheRedisUnavailable":0,"Tests\\Unit\\Domain\\Cache\\CacheRepositoryTest::testClearCacheWithoutRedis":0,"Tests\\Unit\\Domain\\Cache\\CacheRepositoryTest::testClearCacheReturnStructure":0,"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testCanBeInstantiated":0,"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testHasSaveSettingsMethod":0,"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testHasGetSettingsMethod":0,"Tests\\Unit\\admin\\Controllers\\SettingsControllerTest::testConstructorAcceptsRepository":0.001,"Tests\\Unit\\admin\\Controllers\\SettingsControllerTest::testHasClearCacheMethod":0,"Tests\\Unit\\admin\\Controllers\\SettingsControllerTest::testHasClearCacheAjaxMethod":0,"Tests\\Unit\\admin\\Controllers\\SettingsControllerTest::testHasSaveMethod":0,"Tests\\Unit\\admin\\Controllers\\SettingsControllerTest::testHasViewMethod":0,"Tests\\Unit\\admin\\Controllers\\SettingsControllerTest::testIsNotAbstract":0,"Tests\\Unit\\admin\\Controllers\\SettingsControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testCanCreateController":0.004,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testHasListMethod":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testListMethodReturnType":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testHasEditMethod":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testEditMethodReturnType":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testConstructorAcceptsRepository":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testConstructorRequiresArticleRepository":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testFindReturnsArticleWithRelations":0.005,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testFindReturnsNullWhenArticleDoesNotExist":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testDeleteNonassignedFilesDeletesDbRows":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testDeleteNonassignedImagesDeletesDbRows":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveCreatesNewArticle":0.001,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveReturnsZeroWhenInsertFails":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveUpdatesExistingArticle":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveTranslationsInsertsForNewArticle":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveTranslationsUpsertsForExistingArticle":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSavePagesForNewArticle":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveDeletesMarkedImagesOnUpdate":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testArchiveSetsStatusToMinusOne":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testArchiveReturnsFalseWhenUpdateFails":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveGalleryOrderUpdatesImageOrder":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveGalleryOrderSkipsEmptyValues":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testHasBrowseListMethod":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testHasGalleryOrderSaveMethod":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testBrowseListMethodReturnType":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testGalleryOrderSaveMethodReturnType":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testListForAdminWhitelistsSortAndDirection":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testListForAdminUsesBoundParamsForTitleFilter":0,"Tests\\Unit\\Domain\\Banner\\BannerRepositoryTest::testSaveWithLegacyFormat":0,"Tests\\Unit\\Domain\\Banner\\BannerRepositoryTest::testSaveUpdatesExistingTranslationsByBannerAndLang":0.001,"Tests\\Unit\\Domain\\Banner\\BannerRepositoryTest::testListForAdminIncludesThumbnailSrc":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testRestoreSetsStatusToZero":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testDeletePermanentlyRemovesArticleAndRelations":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testListArchivedForAdminWhitelistsSortAndDirection":0,"Tests\\Unit\\Domain\\Dictionaries\\DictionariesRepositoryTest::testFindReturnsUnitWithTranslations":0.001,"Tests\\Unit\\Domain\\Dictionaries\\DictionariesRepositoryTest::testFindReturnsNullWhenUnitNotFound":0,"Tests\\Unit\\Domain\\Dictionaries\\DictionariesRepositoryTest::testSaveInsertsNewUnitAndTranslationsForStringLanguageId":0,"Tests\\Unit\\Domain\\Dictionaries\\DictionariesRepositoryTest::testDeleteRemovesUnitAndTranslations":0,"Tests\\Unit\\Domain\\Dictionaries\\DictionariesRepositoryTest::testGetUnitNameByIdReturnsTextFromDatabase":0,"Tests\\Unit\\Domain\\Dictionaries\\DictionariesRepositoryTest::testGetUnitNameByIdSupportsStringLanguageId":0,"Tests\\Unit\\Domain\\Dictionaries\\DictionariesRepositoryTest::testAllUnitsReturnsArrayIndexedById":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testLanguageDetailsReturnsArrayOrNull":0.002,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testLanguagesListReturnsArray":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testSaveLanguageRejectsInvalidLanguageId":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testSaveTranslationInsertsNewTranslationAndReturnsId":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testDeleteTranslationReturnsBoolean":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testListForAdminReturnsItemsAndTotal":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testDefaultLanguageIdReturnsLanguageWithStartFlag":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testDefaultLanguageIdFallsBackToFirstLanguageOrPl":0,"Tests\\Unit\\Domain\\Layouts\\LayoutsRepositoryTest::testFindReturnsLayoutWithRelations":0.002,"Tests\\Unit\\Domain\\Layouts\\LayoutsRepositoryTest::testDeleteReturnsFalseWhenOnlyOneLayoutExists":0,"Tests\\Unit\\Domain\\Layouts\\LayoutsRepositoryTest::testFindReturnsDefaultLayoutWhenRecordDoesNotExist":0,"Tests\\Unit\\Domain\\Layouts\\LayoutsRepositoryTest::testSaveInsertsNewLayoutAndReturnsId":0,"Tests\\Unit\\Domain\\Layouts\\LayoutsRepositoryTest::testListAllReturnsArray":0,"Tests\\Unit\\Domain\\Newsletter\\NewsletterRepositoryTest::testTemplateDetailsReturnsNullForInvalidId":0.002,"Tests\\Unit\\Domain\\Newsletter\\NewsletterRepositoryTest::testTemplateDetailsReturnsArray":0,"Tests\\Unit\\Domain\\Newsletter\\NewsletterRepositoryTest::testSaveSettingsUpdatesHeaderAndFooter":0,"Tests\\Unit\\Domain\\Newsletter\\NewsletterRepositoryTest::testDeleteTemplateReturnsFalseForAdminTemplate":0,"Tests\\Unit\\Domain\\Newsletter\\NewsletterRepositoryTest::testTemplateByNameReturnsText":0,"Tests\\Unit\\Domain\\Scontainers\\ScontainersRepositoryTest::testFindReturnsDefaultContainerForInvalidId":0.001,"Tests\\Unit\\Domain\\Scontainers\\ScontainersRepositoryTest::testDeleteReturnsFalseForInvalidId":0,"Tests\\Unit\\Domain\\Scontainers\\ScontainersRepositoryTest::testFindReturnsContainerWithTranslations":0,"Tests\\Unit\\Domain\\Scontainers\\ScontainersRepositoryTest::testDetailsForLanguageReturnsNullForInvalidData":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testFindReturnsUserWhenExists":0.001,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testFindReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testCheckLoginReturnsErrorWhenLoginIsTaken":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testCheckLoginReturnsOkWhenAvailable":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testSaveReturnsErrorForTooShortPasswordOnCreate":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testSaveReturnsErrorForMismatchedPasswordsOnCreate":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testSaveCreatesUserWithNormalizedSwitches":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testSaveUpdatesExistingUserWithPassword":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testSaveUpdatesExistingUserWithoutPassword":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testSaveReturnsErrorForTooShortPasswordOnUpdate":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testSaveReturnsErrorForMismatchedPasswordsOnUpdate":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testDeleteReturnsTrue":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testDeleteReturnsFalseOnFailure":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testDetailsReturnsUserByLogin":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testDetailsReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testLogonReturnsSuccessForValidCredentials":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testLogonReturnsZeroForNonexistentUser":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testLogonReturnsNegativeOneForBlockedUser":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testVerifyTwofaCodeReturnsFalseForNonexistentUser":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testVerifyTwofaCodeReturnsFalseAfterMaxAttempts":0.078,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testVerifyTwofaCodeReturnsFalseForExpiredCode":0.077,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testVerifyTwofaCodeReturnsTrueForValidCode":0.152,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testSendTwofaCodeReturnsFalseWhen2FADisabled":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testSendTwofaCodeReturnsFalseForInvalidEmail":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testUpdateByIdCallsDbUpdate":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testListForAdminReturnsItemsAndTotal":0,"Tests\\Unit\\admin\\Controllers\\ArticlesArchiveControllerTest::testConstructorAcceptsRepository":0.001,"Tests\\Unit\\admin\\Controllers\\ArticlesArchiveControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\admin\\Controllers\\ArticlesArchiveControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\ArticlesArchiveControllerTest::testConstructorRequiresArticleRepository":0,"Tests\\Unit\\admin\\Controllers\\DictionariesControllerTest::testConstructorAcceptsRepository":0.002,"Tests\\Unit\\admin\\Controllers\\DictionariesControllerTest::testHasListMethod":0,"Tests\\Unit\\admin\\Controllers\\DictionariesControllerTest::testHasEditMethod":0,"Tests\\Unit\\admin\\Controllers\\DictionariesControllerTest::testHasSaveMethod":0,"Tests\\Unit\\admin\\Controllers\\DictionariesControllerTest::testHasDeleteMethod":0,"Tests\\Unit\\admin\\Controllers\\DictionariesControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\DictionariesControllerTest::testConstructorRequiresDictionariesRepository":0,"Tests\\Unit\\admin\\Controllers\\LanguagesControllerTest::testConstructorAcceptsRepository":0.001,"Tests\\Unit\\admin\\Controllers\\LanguagesControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\admin\\Controllers\\LanguagesControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\LanguagesControllerTest::testConstructorRequiresLanguagesRepository":0,"Tests\\Unit\\admin\\Controllers\\LayoutsControllerTest::testConstructorAcceptsRepository":0.001,"Tests\\Unit\\admin\\Controllers\\LayoutsControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\admin\\Controllers\\LayoutsControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\LayoutsControllerTest::testConstructorRequiresLayoutsRepository":0,"Tests\\Unit\\admin\\Controllers\\NewsletterControllerTest::testConstructorAcceptsDependencies":0.002,"Tests\\Unit\\admin\\Controllers\\NewsletterControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\admin\\Controllers\\NewsletterControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\NewsletterControllerTest::testConstructorRequiresRepositoryAndRenderer":0,"Tests\\Unit\\admin\\Controllers\\ScontainersControllerTest::testConstructorAcceptsDependencies":0.001,"Tests\\Unit\\admin\\Controllers\\ScontainersControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\admin\\Controllers\\ScontainersControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\ScontainersControllerTest::testConstructorRequiresRepositoryAndLanguagesRepository":0,"Tests\\Unit\\admin\\Controllers\\UsersControllerTest::testConstructorAcceptsRepository":0.002,"Tests\\Unit\\admin\\Controllers\\UsersControllerTest::testHasViewListMethod":0,"Tests\\Unit\\admin\\Controllers\\UsersControllerTest::testHasUserEditMethod":0,"Tests\\Unit\\admin\\Controllers\\UsersControllerTest::testHasUserSaveMethod":0,"Tests\\Unit\\admin\\Controllers\\UsersControllerTest::testHasUserDeleteMethod":0,"Tests\\Unit\\admin\\Controllers\\UsersControllerTest::testHasTwofaMethod":0,"Tests\\Unit\\admin\\Controllers\\UsersControllerTest::testHasLoginFormMethod":0,"Tests\\Unit\\admin\\Controllers\\UsersControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\UsersControllerTest::testConstructorRequiresUserRepository":0,"Tests\\Unit\\admin\\Controllers\\UsersControllerTest::testNormalizeUserReturnsDefaultsForNull":0,"Tests\\Unit\\admin\\Controllers\\UsersControllerTest::testNormalizeUserCastsTypes":0,"Tests\\Unit\\admin\\Controllers\\UsersControllerTest::testNormalizeUserHandlesPartialData":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveFilesOrderUpdatesFilesOrder":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveFilesOrderSkipsEmptyValues":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testPagesSummaryForArticlesBuildsLabels":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testUpdateImageAltDelegatesToDatabase":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testMarkFileToDeleteDelegatesToDatabase":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testFindReturnsDefaultCouponForInvalidId":0.001,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testFindNormalizesCouponData":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testSaveInsertsCouponAndReturnsId":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testSaveUpdatesCouponAndReturnsId":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testDeleteReturnsFalseForInvalidId":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testDeleteReturnsTrueWhenDatabaseDeleteSucceeds":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testListForAdminWhitelistsSortAndDirection":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testCategoriesTreeReturnsHierarchy":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testGetSettingsReturnsArray":0.003,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testGetSettingReturnsValue":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testGetSettingReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testSaveSettingUpdatesExistingValue":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testSaveSettingInsertsNewValue":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testInvalidProviderThrowsException":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testLinkProductUpdatesDatabase":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testUnlinkProductClearsFields":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testGetProductSkuReturnsValue":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testGetProductSkuReturnsNullForMissing":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testApiloGetAccessTokenReturnsNullWithoutSettings":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testApiloFetchListThrowsForInvalidType":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testAllPublicMethodsExist":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testSettingsTableMapping":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testShopproProviderWorks":0,"Tests\\Unit\\Domain\\Pages\\PagesRepositoryTest::testMenusListReturnsArray":0.002,"Tests\\Unit\\Domain\\Pages\\PagesRepositoryTest::testMenuDeleteReturnsFalseWhenMenuHasPages":0,"Tests\\Unit\\Domain\\Pages\\PagesRepositoryTest::testGenerateSeoLinkAddsSuffixWhenBaseSlugExists":0,"Tests\\Unit\\Domain\\Pages\\PagesRepositoryTest::testPageUrlPreviewBuildsLanguagePrefixedUrlForNonDefaultLanguage":0,"Tests\\Unit\\Domain\\Promotion\\PromotionRepositoryTest::testFindReturnsDefaultPromotionForInvalidId":0.002,"Tests\\Unit\\Domain\\Promotion\\PromotionRepositoryTest::testSaveInsertsPromotionAndReturnsId":0,"Tests\\Unit\\Domain\\Promotion\\PromotionRepositoryTest::testDeleteReturnsFalseForInvalidId":0,"Tests\\Unit\\Domain\\Promotion\\PromotionRepositoryTest::testDeleteReturnsTrueWhenDatabaseDeleteSucceeds":0,"Tests\\Unit\\Domain\\Promotion\\PromotionRepositoryTest::testListForAdminWhitelistsSortAndDirection":0,"Tests\\Unit\\Domain\\Promotion\\PromotionRepositoryTest::testCategoriesTreeReturnsHierarchy":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testHasImageAltChangeMethod":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testHasFileNameChangeMethod":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testHasImageDeleteMethod":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testHasFileDeleteMethod":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testImageAltChangeMethodReturnType":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testFileNameChangeMethodReturnType":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testImageDeleteMethodReturnType":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testFileDeleteMethodReturnType":0,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testConstructorAcceptsDependencies":0.002,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testConstructorRequiresRepository":0,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testHasAllApiloSettingsMethods":0,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testHasAllApiloDataFetchMethods":0,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testHasAllApiloProductMethods":0,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testHasAllShopproMethods":0,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testApiloSettingsReturnsString":0,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testShopproSettingsReturnsString":0,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testVoidReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testDoesNotHaveSellasistMethods":0,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testDoesNotHaveBaselinkerMethods":0,"Tests\\Unit\\admin\\Controllers\\PagesControllerTest::testConstructorAcceptsRepositories":0.001,"Tests\\Unit\\admin\\Controllers\\PagesControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\admin\\Controllers\\PagesControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\PagesControllerTest::testConstructorRequiresPagesLanguagesAndLayoutsRepositories":0,"Tests\\Unit\\admin\\Controllers\\ShopCouponControllerTest::testConstructorAcceptsRepository":0.001,"Tests\\Unit\\admin\\Controllers\\ShopCouponControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopCouponControllerTest::testHasLegacyAliasMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopCouponControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\ShopCouponControllerTest::testConstructorRequiresCouponRepository":0,"Tests\\Unit\\admin\\Controllers\\ShopPromotionControllerTest::testConstructorAcceptsRepository":0.001,"Tests\\Unit\\admin\\Controllers\\ShopPromotionControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopPromotionControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\ShopPromotionControllerTest::testConstructorRequiresPromotionRepository":0,"Tests\\Unit\\Domain\\ShopStatus\\ShopStatusRepositoryTest::testFindReturnsNullForNegativeId":0.001,"Tests\\Unit\\Domain\\ShopStatus\\ShopStatusRepositoryTest::testFindReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\ShopStatus\\ShopStatusRepositoryTest::testFindReturnsStatusWithIdZero":0,"Tests\\Unit\\Domain\\ShopStatus\\ShopStatusRepositoryTest::testFindNormalizesNullApiloStatusId":0,"Tests\\Unit\\Domain\\ShopStatus\\ShopStatusRepositoryTest::testSaveUpdatesColorAndApiloStatusId":0,"Tests\\Unit\\Domain\\ShopStatus\\ShopStatusRepositoryTest::testSaveWithIdZeroWorks":0,"Tests\\Unit\\Domain\\ShopStatus\\ShopStatusRepositoryTest::testSaveWithEmptyApiloStatusIdSetsNull":0,"Tests\\Unit\\Domain\\ShopStatus\\ShopStatusRepositoryTest::testSaveRejectsNegativeId":0,"Tests\\Unit\\Domain\\ShopStatus\\ShopStatusRepositoryTest::testGetApiloStatusIdReturnsValue":0,"Tests\\Unit\\Domain\\ShopStatus\\ShopStatusRepositoryTest::testGetApiloStatusIdReturnsNullWhenNotSet":0,"Tests\\Unit\\Domain\\ShopStatus\\ShopStatusRepositoryTest::testGetByIntegrationStatusIdForApilo":0,"Tests\\Unit\\Domain\\ShopStatus\\ShopStatusRepositoryTest::testGetByIntegrationStatusIdReturnsNullForUnknownIntegration":0,"Tests\\Unit\\Domain\\ShopStatus\\ShopStatusRepositoryTest::testAllStatusesReturnsOrderedList":0,"Tests\\Unit\\Domain\\ShopStatus\\ShopStatusRepositoryTest::testListForAdminWhitelistsSortAndDirection":0,"Tests\\Unit\\admin\\Controllers\\ShopStatusesControllerTest::testConstructorAcceptsRepository":0.001,"Tests\\Unit\\admin\\Controllers\\ShopStatusesControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopStatusesControllerTest::testHasNoLegacyAliasMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopStatusesControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\ShopStatusesControllerTest::testConstructorRequiresShopStatusRepository":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testShouldRefreshAccessTokenReturnsFalseForFarFutureDate":0.001,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testShouldRefreshAccessTokenReturnsTrueForNearExpiryDate":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testApiloFetchListResultReturnsDetailedErrorWhenConfigMissing":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testApiloIntegrationStatusReturnsMissingConfigMessage":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testNormalizeApiloMapListRejectsErrorPayload":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testNormalizeApiloMapListAcceptsIdNameList":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testFindReturnsNullForInvalidId":0.001,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testFindReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testFindNormalizesData":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testSaveUpdatesRowAndReturnsId":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testSavePreservesNonNumericApiloPaymentTypeId":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testSaveReturnsNullForInvalidId":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testListForAdminWhitelistsSortAndDirection":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testAllActiveReturnsNormalizedRows":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testAllForAdminReturnsRowsIncludingInactive":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testFindActiveByIdReturnsNullForNotFound":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testFindKeepsNonNumericApiloPaymentTypeId":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testIsActiveNormalizesStatusValue":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testGetApiloPaymentTypeIdHandlesNullAndInt":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testGetApiloPaymentTypeIdReturnsStringForNonNumericValue":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testForTransportReturnsRows":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testFindReturnsNullForInvalidId":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testFindReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testFindNormalizesDataAndIncludesPaymentMethods":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testFindHandlesNullMaxWpAndApiloId":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testSaveInsertReturnsNewId":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testSaveUpdateReturnsExistingId":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testSaveInsertReturnsNullOnFailure":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testSaveResetsDefaultWhenSettingNew":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testSaveSwitchValuesNormalization":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testListForAdminWhitelistsSortColumn":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testAllActiveReturnsNormalizedRows":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testGetApiloCarrierAccountIdReturnsNullForInvalidId":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testGetApiloCarrierAccountIdReturnsIntOrNull":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testGetTransportCostReturnsFloatOrNull":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testAllForAdminReturnsAllTransports":0,"Tests\\Unit\\admin\\Controllers\\ShopPaymentMethodControllerTest::testConstructorAcceptsRepository":0.002,"Tests\\Unit\\admin\\Controllers\\ShopPaymentMethodControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopPaymentMethodControllerTest::testHasNoLegacyAliasMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopPaymentMethodControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\ShopPaymentMethodControllerTest::testConstructorRequiresPaymentMethodRepository":0,"Tests\\Unit\\admin\\Controllers\\ShopTransportControllerTest::testConstructorAcceptsRepositories":0.001,"Tests\\Unit\\admin\\Controllers\\ShopTransportControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopTransportControllerTest::testHasNoLegacyAliasMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopTransportControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\ShopTransportControllerTest::testConstructorRequiresBothRepositories":0,"Tests\\Unit\\Domain\\Attribute\\AttributeRepositoryTest::testFindAttributeReturnsDefaultAttributeForInvalidId":0.002,"Tests\\Unit\\Domain\\Attribute\\AttributeRepositoryTest::testListForAdminWhitelistsSortDirectionAndPerPage":0,"Tests\\Unit\\Domain\\Attribute\\AttributeRepositoryTest::testSaveValuesRemovesObsoleteRowsAndSetsDefault":0,"Tests\\Unit\\Domain\\Attribute\\AttributeRepositoryTest::testSaveValuesDeletesTranslationWhenNameIsEmpty":0,"Tests\\Unit\\Domain\\Attribute\\AttributeRepositoryTest::testGetAttributeValueByIdUsesDefaultLanguageWhenNotProvided":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testSortTypesReturnsExpectedKeys":0.002,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testCategoryDetailsReturnsDefaultForInvalidId":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testCategoryDetailsLoadsTranslations":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testSaveCategoriesOrderReturnsFalseForNonArray":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testSaveCategoriesOrderUpdatesOrderAndParent":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testSaveProductOrderReturnsFalseForInvalidInput":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testSaveProductOrderUpdatesCategoryProductOrder":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testCategoryDeleteReturnsFalseWhenHasChildren":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testCategoryDeleteReturnsTrueWhenDeleted":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testCategoryTitleReturnsEmptyWhenNotFound":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testCategoryTitleReturnsFirstAvailableTitle":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testListForAdminWhitelistsSortAndPagination":0.001,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testOrdersForClientReturnsEmptyOnMissingInput":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testOrdersForClientNormalizesRows":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testTotalsForClientReturnsZeroForMissingInput":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testTotalsForClientReturnsAggregatedValues":0,"Tests\\Unit\\Domain\\Dashboard\\DashboardRepositoryTest::testConstructorAcceptsDb":0.001,"Tests\\Unit\\Domain\\Dashboard\\DashboardRepositoryTest::testHasAllPublicMethods":0,"Tests\\Unit\\Domain\\Dashboard\\DashboardRepositoryTest::testSalesGridReturnsArray":0,"Tests\\Unit\\Domain\\Dashboard\\DashboardRepositoryTest::testLastOrdersReturnsArray":0,"Tests\\Unit\\Domain\\Dashboard\\DashboardRepositoryTest::testMostViewedProductsReturnsArray":0,"Tests\\Unit\\Domain\\Dashboard\\DashboardRepositoryTest::testBestSalesProductsReturnsArray":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testDefaultLanguageReturnsId":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testDefaultLanguageReturnsFallbackWhenEmpty":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testActiveLanguagesReturnsList":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testActiveLanguagesReturnsEmptyArrayWhenNone":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testTranslationsReturnsArray":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testTranslationsDefaultsToPl":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testTranslationsForDifferentLanguage":0,"Tests\\Unit\\Domain\\Newsletter\\NewsletterRepositoryTest::testUnsubscribeReturnsFalseForInvalidHash":0,"Tests\\Unit\\Domain\\Newsletter\\NewsletterRepositoryTest::testUnsubscribeDeletesSubscriber":0,"Tests\\Unit\\Domain\\Newsletter\\NewsletterRepositoryTest::testConfirmSubscriptionReturnsFalseForInvalidHash":0,"Tests\\Unit\\Domain\\Newsletter\\NewsletterRepositoryTest::testConfirmSubscriptionUpdatesStatus":0,"Tests\\Unit\\Domain\\Newsletter\\NewsletterRepositoryTest::testGetHashByEmailReturnsHash":0,"Tests\\Unit\\Domain\\Newsletter\\NewsletterRepositoryTest::testGetHashByEmailReturnsNullForMissing":0,"Tests\\Unit\\Domain\\Newsletter\\NewsletterRepositoryTest::testRemoveByEmailDeletesSubscriber":0,"Tests\\Unit\\Domain\\Newsletter\\NewsletterRepositoryTest::testRemoveByEmailReturnsFalseForMissing":0,"Tests\\Unit\\Domain\\Newsletter\\NewsletterRepositoryTest::testSignupReturnsFalseForExistingEmail":0.001,"Tests\\Unit\\Domain\\Newsletter\\NewsletterRepositoryTest::testConstructorAcceptsOptionalDependencies":0.003,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testOrderStatusesReturnsMappedArray":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testNextAndPrevOrderIdReturnNullForInvalidInput":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testListForAdminReturnsItemsAndTotal":0,"Tests\\Unit\\Domain\\Producer\\ProducerRepositoryTest::testFindReturnsDefaultProducerForInvalidId":0.001,"Tests\\Unit\\Domain\\Producer\\ProducerRepositoryTest::testFindNormalizesProducerData":0,"Tests\\Unit\\Domain\\Producer\\ProducerRepositoryTest::testSaveInsertsNewProducer":0,"Tests\\Unit\\Domain\\Producer\\ProducerRepositoryTest::testSaveUpdatesExistingProducer":0,"Tests\\Unit\\Domain\\Producer\\ProducerRepositoryTest::testDeleteReturnsFalseForInvalidId":0,"Tests\\Unit\\Domain\\Producer\\ProducerRepositoryTest::testDeleteReturnsTrueOnSuccess":0,"Tests\\Unit\\Domain\\Producer\\ProducerRepositoryTest::testListForAdminWhitelistsSortAndPagination":0,"Tests\\Unit\\Domain\\Producer\\ProducerRepositoryTest::testAllProducersReturnsFormattedList":0,"Tests\\Unit\\Domain\\Producer\\ProducerRepositoryTest::testProducerProductsReturnsPaginatedResults":0,"Tests\\Unit\\Domain\\ProductSet\\ProductSetRepositoryTest::testFindReturnsDefaultSetForInvalidId":0.001,"Tests\\Unit\\Domain\\ProductSet\\ProductSetRepositoryTest::testFindNormalizesSetData":0,"Tests\\Unit\\Domain\\ProductSet\\ProductSetRepositoryTest::testSaveInsertsNewSetAndSyncsProducts":0,"Tests\\Unit\\Domain\\ProductSet\\ProductSetRepositoryTest::testSaveUpdatesExistingSet":0,"Tests\\Unit\\Domain\\ProductSet\\ProductSetRepositoryTest::testDeleteReturnsFalseForInvalidId":0,"Tests\\Unit\\Domain\\ProductSet\\ProductSetRepositoryTest::testListForAdminWhitelistsSortAndPagination":0,"Tests\\Unit\\Domain\\ProductSet\\ProductSetRepositoryTest::testAllSetsReturnsFormattedList":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testAllProductsForMassEditReturnsMap":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testAllProductsForMassEditEmptyWhenNoProducts":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetProductsByCategoryReturnsList":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetProductsByCategoryReturnsEmptyArray":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testApplyDiscountPercentReturnsNullForInvalidProduct":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testApplyDiscountPercentReturnsCorrectPrices":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testApplyDiscountPercentZeroPercentNullsPromo":0,"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testAllSettingsReturnsAssociativeArray":0,"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testAllSettingsReturnsEmptyArrayWhenNoSettings":0,"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testAllSettingsHandlesNullFromDb":0,"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testGetSingleValueReturnsCorrectParam":0,"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testGetSingleValueUsesParamNotHardcoded":0,"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testGetSingleValueReturnsEmptyStringWhenNotFound":0,"Tests\\Unit\\Domain\\Update\\UpdateRepositoryTest::testConstructorAcceptsDb":0.001,"Tests\\Unit\\Domain\\Update\\UpdateRepositoryTest::testHasUpdateMethod":0,"Tests\\Unit\\Domain\\Update\\UpdateRepositoryTest::testUpdateReturnsArray":0,"Tests\\Unit\\Domain\\Update\\UpdateRepositoryTest::testHasRunPendingMigrationsMethod":0,"Tests\\Unit\\Domain\\Update\\UpdateRepositoryTest::testRunPendingMigrationsWithNoResults":0,"Tests\\Unit\\Domain\\Update\\UpdateRepositoryTest::testHasPrivateHelperMethods":0,"Tests\\Unit\\admin\\Controllers\\DashboardControllerTest::testConstructorAcceptsRepositories":0.001,"Tests\\Unit\\admin\\Controllers\\DashboardControllerTest::testHasMainViewMethod":0,"Tests\\Unit\\admin\\Controllers\\DashboardControllerTest::testMainViewReturnsString":0,"Tests\\Unit\\admin\\Controllers\\DashboardControllerTest::testConstructorRequiresRepositories":0,"Tests\\Unit\\admin\\Controllers\\ShopAttributeControllerTest::testConstructorAcceptsRepositories":0.002,"Tests\\Unit\\admin\\Controllers\\ShopAttributeControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopAttributeControllerTest::testHasNoLegacyAliasMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopAttributeControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\ShopAttributeControllerTest::testConstructorRequiresBothRepositories":0,"Tests\\Unit\\admin\\Controllers\\ShopAttributeControllerTest::testValidateValuesRowsReturnsErrorsForMissingDefaultLanguageAndDefaultSelection":0,"Tests\\Unit\\admin\\Controllers\\ShopAttributeControllerTest::testValidateValuesRowsReturnsEmptyArrayForValidRows":0,"Tests\\Unit\\admin\\Controllers\\ShopCategoryControllerTest::testConstructorAcceptsDependencies":0.002,"Tests\\Unit\\admin\\Controllers\\ShopCategoryControllerTest::testHasExpectedActionMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopCategoryControllerTest::testViewActionsReturnString":0,"Tests\\Unit\\admin\\Controllers\\ShopCategoryControllerTest::testMutationActionsReturnVoid":0,"Tests\\Unit\\admin\\Controllers\\ShopCategoryControllerTest::testConstructorRequiresCategoryAndLanguagesRepositories":0,"Tests\\Unit\\admin\\Controllers\\ShopClientsControllerTest::testConstructorAcceptsRepository":0.002,"Tests\\Unit\\admin\\Controllers\\ShopClientsControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopClientsControllerTest::testHasLegacyAliasMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopClientsControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\ShopClientsControllerTest::testConstructorRequiresClientRepository":0,"Tests\\Unit\\admin\\Controllers\\ShopOrderControllerTest::testConstructorAcceptsService":0.002,"Tests\\Unit\\admin\\Controllers\\ShopOrderControllerTest::testHasExpectedActionMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopOrderControllerTest::testViewActionsReturnString":0,"Tests\\Unit\\admin\\Controllers\\ShopOrderControllerTest::testMutationActionsReturnVoid":0,"Tests\\Unit\\admin\\Controllers\\ShopOrderControllerTest::testConstructorRequiresOrderAdminService":0,"Tests\\Unit\\admin\\Controllers\\ShopProducerControllerTest::testConstructorAcceptsRepositories":0.003,"Tests\\Unit\\admin\\Controllers\\ShopProducerControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopProducerControllerTest::testHasLegacyAliasMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopProducerControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\ShopProducerControllerTest::testConstructorRequiresBothRepositories":0,"Tests\\Unit\\admin\\Controllers\\ShopProductControllerTest::testConstructorAcceptsRepositories":0.002,"Tests\\Unit\\admin\\Controllers\\ShopProductControllerTest::testHasMassEditActionMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopProductControllerTest::testHasViewListMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopProductControllerTest::testHasEditAndSaveMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopProductControllerTest::testHasOperationMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopProductControllerTest::testHasCombinationMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopProductControllerTest::testHasImageAndFileMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopProductControllerTest::testMassEditReturnsString":0,"Tests\\Unit\\admin\\Controllers\\ShopProductControllerTest::testMassEditSaveReturnsVoid":0,"Tests\\Unit\\admin\\Controllers\\ShopProductControllerTest::testGetProductsByCategoryReturnsVoid":0,"Tests\\Unit\\admin\\Controllers\\ShopProductControllerTest::testConstructorRequiresRepositories":0,"Tests\\Unit\\admin\\Controllers\\ShopProductControllerTest::testHasFormBuildingHelpers":0,"Tests\\Unit\\admin\\Controllers\\ShopProductControllerTest::testSaveMethodReturnsVoid":0,"Tests\\Unit\\admin\\Controllers\\ShopProductSetsControllerTest::testConstructorAcceptsRepository":0.001,"Tests\\Unit\\admin\\Controllers\\ShopProductSetsControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopProductSetsControllerTest::testHasLegacyAliasMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopProductSetsControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\ShopProductSetsControllerTest::testConstructorRequiresProductSetRepository":0,"Tests\\Unit\\admin\\Controllers\\UpdateControllerTest::testConstructorAcceptsRepository":0.001,"Tests\\Unit\\admin\\Controllers\\UpdateControllerTest::testHasMainViewMethod":0,"Tests\\Unit\\admin\\Controllers\\UpdateControllerTest::testMainViewReturnsString":0,"Tests\\Unit\\admin\\Controllers\\UpdateControllerTest::testHasUpdateMethod":0,"Tests\\Unit\\admin\\Controllers\\UpdateControllerTest::testHasUpdateAllMethod":0,"Tests\\Unit\\admin\\Controllers\\UpdateControllerTest::testConstructorRequiresRepository":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testArticleDetailsFrontendReturnsArticleWithRelations":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testArticleDetailsFrontendReturnsNullForMissing":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testArticleDetailsFrontendCopyFromFallback":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testArticlesIdsReturnsSortedIds":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testArticlesIdsReturnsNullForEmpty":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testPageArticlesCountReturnsInt":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testPageArticlesCountReturnsZeroForEmpty":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testPageArticlesPagination":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testArticleNoindexReturnsBool":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testArticleNoindexReturnsFalseForNonNoindex":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testNewsReturnsArticlesArray":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testTopArticlesOrderByViews":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testNewsListArticlesOrderByDateDesc":0,"Tests\\Unit\\Domain\\Banner\\BannerRepositoryTest::testBannersReturnsActiveBannersWithFlatLanguages":0,"Tests\\Unit\\Domain\\Banner\\BannerRepositoryTest::testBannersReturnsNullWhenNoBanners":0,"Tests\\Unit\\Domain\\Banner\\BannerRepositoryTest::testMainBannerReturnsActiveBannerWithFlatLanguages":0,"Tests\\Unit\\Domain\\Banner\\BannerRepositoryTest::testMainBannerReturnsNullWhenNoBanner":0,"Tests\\Unit\\Domain\\Layouts\\LayoutsRepositoryTest::testCategoryDefaultLayoutIdReturnsId":0,"Tests\\Unit\\Domain\\Layouts\\LayoutsRepositoryTest::testCategoryDefaultLayoutIdReturnsNullWhenNone":0,"Tests\\Unit\\Domain\\Layouts\\LayoutsRepositoryTest::testGetDefaultLayoutReturnsLayoutFromDb":0,"Tests\\Unit\\Domain\\Layouts\\LayoutsRepositoryTest::testGetDefaultLayoutReturnsNullWhenNoLayout":0,"Tests\\Unit\\Domain\\Layouts\\LayoutsRepositoryTest::testGetArticleLayoutReturnsLayoutFromDb":0,"Tests\\Unit\\Domain\\Layouts\\LayoutsRepositoryTest::testGetArticleLayoutReturnsNullWhenNoLayout":0,"Tests\\Unit\\Domain\\Layouts\\LayoutsRepositoryTest::testGetActiveLayoutFallsBackToDefault":0,"Tests\\Unit\\Domain\\Layouts\\LayoutsRepositoryTest::testGetActiveLayoutReturnsNullWhenNothingFound":0,"Tests\\Unit\\Domain\\Pages\\PagesRepositoryTest::testFrontPageDetailsReturnsPageWithLanguage":0,"Tests\\Unit\\Domain\\Pages\\PagesRepositoryTest::testFrontPageDetailsReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\Pages\\PagesRepositoryTest::testFrontMainPageIdReturnsStartPage":0,"Tests\\Unit\\Domain\\Pages\\PagesRepositoryTest::testFrontMainPageIdFallsBackToFirstActive":0,"Tests\\Unit\\Domain\\Pages\\PagesRepositoryTest::testFrontPageSortReturnsValue":0,"Tests\\Unit\\Domain\\Pages\\PagesRepositoryTest::testFrontMenuDetailsReturnsMenuWithPages":0,"Tests\\Unit\\Domain\\Pages\\PagesRepositoryTest::testFrontMenuDetailsReturnsNullForInvalidMenu":0,"Tests\\Unit\\Domain\\Pages\\PagesRepositoryTest::testFrontMenuPagesReturnsEmptyForNoPages":0,"Tests\\Unit\\Domain\\Scontainers\\ScontainersRepositoryTest::testFrontScontainerDetailsReturnsContainerWithLanguage":0,"Tests\\Unit\\Domain\\Scontainers\\ScontainersRepositoryTest::testFrontScontainerDetailsReturnsFallbackForNotFound":0,"Tests\\Unit\\Domain\\Attribute\\AttributeRepositoryTest::testFrontAttributeDetailsReturnsAttributeWithLanguage":0,"Tests\\Unit\\Domain\\Attribute\\AttributeRepositoryTest::testFrontAttributeDetailsReturnsFallbackForNotFound":0,"Tests\\Unit\\Domain\\Attribute\\AttributeRepositoryTest::testFrontValueDetailsReturnsValueWithLanguage":0,"Tests\\Unit\\Domain\\Attribute\\AttributeRepositoryTest::testFrontValueDetailsReturnsFallbackForNotFound":0,"Tests\\Unit\\Domain\\Basket\\BasketCalculatorTest::testSummaryWpCalculatesTotal":0.001,"Tests\\Unit\\Domain\\Basket\\BasketCalculatorTest::testSummaryWpReturnsZeroForEmptyBasket":0,"Tests\\Unit\\Domain\\Basket\\BasketCalculatorTest::testCountProductsSumsQuantities":0,"Tests\\Unit\\Domain\\Basket\\BasketCalculatorTest::testCountProductsReturnsZeroForEmptyBasket":0,"Tests\\Unit\\Domain\\Basket\\BasketCalculatorTest::testCountProductsTextSingular":0,"Tests\\Unit\\Domain\\Basket\\BasketCalculatorTest::testCountProductsTextPlural2to4":0,"Tests\\Unit\\Domain\\Basket\\BasketCalculatorTest::testCountProductsTextPlural5Plus":0,"Tests\\Unit\\Domain\\Basket\\BasketCalculatorTest::testCountProductsTextCastsToInt":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testGetCategorySortReturnsZeroForInvalidId":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testGetCategorySortReturnsSortType":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testCategoryNameReturnsEmptyForInvalidInput":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testCategoryNameReturnsTitle":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testCategoryNameReturnsEmptyWhenNotFound":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testFrontCategoryDetailsReturnsEmptyForInvalidId":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testFrontCategoryDetailsReturnsCategoryWithLanguage":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testFrontCategoryDetailsReturnsEmptyWhenCategoryNotFound":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testCategoriesTreeReturnsEmptyWhenNoCategories":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testCategoryProductsCountReturnsZeroForInvalidInput":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testCategoryProductsCountReturnsCount":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testProductsIdReturnsEmptyForInvalidInput":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testProductsIdReturnsProductIds":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testBlogCategoryProductsReturnsEmptyForInvalidInput":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testBlogCategoryProductsReturnsIds":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testPaginatedCategoryProductsReturnsEmptyWhenNoProducts":0,"Tests\\Unit\\Domain\\Category\\CategoryRepositoryTest::testPaginatedCategoryProductsClampsPage":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testClientDetailsReturnsNullForInvalidId":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testClientDetailsReturnsRowOnSuccess":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testClientDetailsReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testClientEmailReturnsNullForInvalidId":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testClientEmailReturnsStringOnSuccess":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testClientEmailReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testClientAddressesReturnsEmptyForInvalidId":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testClientAddressesReturnsRows":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testClientAddressesHandlesFalseFromDb":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testAddressDetailsReturnsNullForInvalidId":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testAddressDetailsReturnsRow":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testAddressDeleteReturnsFalseForInvalidId":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testAddressDeleteReturnsTrueOnSuccess":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testAddressSaveReturnsFalseForInvalidClientId":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testAddressSaveInsertsNewAddress":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testAddressSaveUpdatesExistingAddress":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testMarkAddressAsCurrentReturnsFalseForInvalidIds":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testMarkAddressAsCurrentResetsAndSets":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testAuthenticateReturnsErrorOnEmptyInput":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testAuthenticateReturnsErrorWhenClientNotFound":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testAuthenticateReturnsInactiveForUnconfirmedAccount":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testAuthenticateReturnsErrorOnWrongPassword":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testAuthenticateReturnsOkOnSuccess":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testCreateClientReturnsNullOnEmptyInput":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testCreateClientReturnsNullWhenEmailTaken":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testCreateClientReturnsIdAndHashOnSuccess":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testConfirmRegistrationReturnsNullOnEmptyHash":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testConfirmRegistrationReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testConfirmRegistrationActivatesAndReturnsEmail":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testGenerateNewPasswordReturnsNullOnEmptyHash":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testGenerateNewPasswordReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testGenerateNewPasswordReturnsEmailAndPassword":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testInitiatePasswordRecoveryReturnsNullOnEmptyEmail":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testInitiatePasswordRecoveryReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testInitiatePasswordRecoverySetsRecoveryFlagAndReturnsHash":0,"Tests\\Unit\\Domain\\Client\\ClientRepositoryTest::testClientOrdersReturnsEmptyForInvalidId":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testFindByNameReturnsObjectWhenFound":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testFindByNameReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testFindByNameReturnsNullForEmptyName":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testIsAvailableReturnsTrueForActiveCoupon":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testIsAvailableReturnsFalseForUsedCoupon":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testIsAvailableReturnsFalseForInactiveCoupon":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testIsAvailableReturnsFalseForNullCoupon":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testIsAvailableWorksWithArray":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testMarkAsUsedCallsUpdate":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testMarkAsUsedSkipsInvalidId":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testIncrementUsedCountCallsUpdate":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testIncrementUsedCountSkipsInvalidId":0,"Tests\\Unit\\front\\Controllers\\ShopCouponControllerTest::testConstructorAcceptsRepository":0.001,"Tests\\Unit\\front\\Controllers\\ShopCouponControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\front\\Controllers\\ShopCouponControllerTest::testConstructorRequiresCouponRepository":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testFindIdByHashReturnsIdWhenFound":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testFindIdByHashReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testFindIdByHashReturnsNullForEmptyHash":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testFindHashByIdReturnsHashWhenFound":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testFindHashByIdReturnsNullForInvalidId":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testOrderDetailsFrontendByIdReturnsArrayWithProducts":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testOrderDetailsFrontendByHashReturnsArrayWithProducts":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testOrderDetailsFrontendReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testGenerateOrderNumberFormatsCorrectly":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testGenerateOrderNumberStartsAt001":0,"Tests\\Unit\\Domain\\Producer\\ProducerRepositoryTest::testAllActiveProducersReturnsFullData":0,"Tests\\Unit\\Domain\\Producer\\ProducerRepositoryTest::testAllActiveProducersReturnsEmptyOnNull":0,"Tests\\Unit\\Domain\\Producer\\ProducerRepositoryTest::testFindForFrontendReturnsNullForInvalidId":0,"Tests\\Unit\\Domain\\Producer\\ProducerRepositoryTest::testFindForFrontendReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\Producer\\ProducerRepositoryTest::testFindForFrontendReturnsProducerWithLanguage":0,"Tests\\Unit\\front\\Controllers\\ShopOrderControllerTest::testConstructorAcceptsRepository":0.001,"Tests\\Unit\\front\\Controllers\\ShopOrderControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\front\\Controllers\\ShopOrderControllerTest::testConstructorRequiresOrderRepository":0,"Tests\\Unit\\front\\Controllers\\ShopProducerControllerTest::testConstructorAcceptsRepository":0,"Tests\\Unit\\front\\Controllers\\ShopProducerControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\front\\Controllers\\ShopProducerControllerTest::testConstructorRequiresProducerRepository":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testIsActiveReturnsOneForActivePayment":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testIsActiveReturnsZeroForInvalidId":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testFindActiveByIdReturnsNormalizedData":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testFindActiveByIdReturnsNullForInvalidId":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testAllActiveReturnsEmptyOnNull":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testGetApiloPaymentTypeIdReturnsNullForInvalidId":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testForTransportReturnsEmptyForInvalidId":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetSkuWithFallbackReturnsSku":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetSkuWithFallbackFromParent":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetSkuWithFallbackReturnsNullForInvalidId":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetEanWithFallbackReturnsEan":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetEanWithFallbackFromParent":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testIsProductActiveCachedReturnsOneForActive":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testIsProductActiveCachedReturnsZeroForInactive":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testIsProductActiveCachedReturnsZeroForInvalidId":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testProductCategoriesFrontReturnsCategories":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testProductCategoriesFrontUsesParentId":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testProductCategoriesFrontReturnsEmptyForInvalidId":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetWarehouseMessageZeroReturnsMessage":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetWarehouseMessageZeroReturnsNullForInvalidId":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetWarehouseMessageNonzeroReturnsMessage":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetWarehouseMessageNonzeroReturnsNullForInvalidId":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testTopProductIdsReturnsActiveProducts":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testNewProductIdsReturnsProductIds":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testNewProductIdsReturnsEmptyWhenNoProducts":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testPromotedProductIdsCachedReturnsIds":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testPromotedProductIdsCachedReturnsEmptyWhenNone":0,"Tests\\Unit\\Domain\\Promotion\\PromotionRepositoryTest::testApplyTypeWholeBasketAppliesDiscountToAll":0,"Tests\\Unit\\Domain\\Promotion\\PromotionRepositoryTest::testApplyTypeCategoriesOrAppliesDiscountToMatchingCategories":0,"Tests\\Unit\\Domain\\Promotion\\PromotionRepositoryTest::testApplyTypeCategoryConditionAppliesWhenConditionMet":0,"Tests\\Unit\\Domain\\Promotion\\PromotionRepositoryTest::testApplyTypeCategoryConditionNoDiscountWhenConditionNotMet":0,"Tests\\Unit\\Domain\\Promotion\\PromotionRepositoryTest::testApplyTypeCategoriesAndAppliesWhenBothConditionsMet":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testTransportCostCachedReturnsCost":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testFindActiveByIdCachedReturnsTransport":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testFindActiveByIdCachedReturnsNullForInvalid":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testForPaymentMethodReturnsTransports":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testForPaymentMethodReturnsEmptyForInvalidId":0,"Tests\\Unit\\Domain\\Basket\\BasketCalculatorTest::testCheckProductQuantityInStockReturnsFalseOnNullBasket":0,"Tests\\Unit\\Domain\\Basket\\BasketCalculatorTest::testCheckProductQuantityInStockReturnsFalseOnEmptyBasket":0,"Tests\\Unit\\Domain\\Basket\\BasketCalculatorTest::testValidateBasketReturnsEmptyArrayOnNull":0,"Tests\\Unit\\Domain\\Basket\\BasketCalculatorTest::testValidateBasketReturnsBasketArrayAsIs":0,"Tests\\Unit\\Domain\\Order\\OrderAdminServiceTest::testConstructorAcceptsOnlyOrderRepository":0.005,"Tests\\Unit\\Domain\\Order\\OrderAdminServiceTest::testConstructorAcceptsAllDependencies":0.013,"Tests\\Unit\\Domain\\Order\\OrderAdminServiceTest::testSearchProductsReturnsEmptyForEmptyQuery":0,"Tests\\Unit\\Domain\\Order\\OrderAdminServiceTest::testSearchProductsReturnsEmptyWithoutProductRepo":0,"Tests\\Unit\\Domain\\Order\\OrderAdminServiceTest::testSearchProductsReturnsFormattedResults":0,"Tests\\Unit\\Domain\\Order\\OrderAdminServiceTest::testSaveOrderProductsReturnsFalseForInvalidOrderId":0,"Tests\\Unit\\Domain\\Order\\OrderAdminServiceTest::testSaveOrderProductsDeletesRemovedProducts":0,"Tests\\Unit\\Domain\\Order\\OrderAdminServiceTest::testSaveOrderProductsUpdatesQuantityAndAdjustsStock":0,"Tests\\Unit\\Domain\\Order\\OrderAdminServiceTest::testSaveOrderProductsAddsNewProductAndDecreasesStock":0,"Tests\\Unit\\Domain\\Order\\OrderAdminServiceTest::testGetFreeDeliveryThresholdReturnsZeroWithoutSettingsRepo":0,"Tests\\Unit\\Domain\\Order\\OrderAdminServiceTest::testGetFreeDeliveryThresholdReturnsValue":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testGetOrderProductReturnsNullForInvalidId":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testGetOrderProductReturnsArray":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testAddOrderProductReturnsNullForInvalidOrderId":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testAddOrderProductInsertsAndReturnsId":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testUpdateOrderProductReturnsFalseForInvalidId":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testUpdateOrderProductUpdatesFields":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testUpdateOrderProductReturnsFalseForEmptyData":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testDeleteOrderProductReturnsFalseForInvalidId":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testDeleteOrderProductCallsDelete":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testUpdateTransportCostDoesNothingForInvalidId":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testUpdateTransportCostUpdatesOrder":0,"Tests\\Unit\\api\\ApiRouterTest::testHandleReturns401WhenNoApiKey":0.001,"Tests\\Unit\\api\\ApiRouterTest::testHandleReturns401WhenWrongApiKey":0,"Tests\\Unit\\api\\ApiRouterTest::testHandleReturns401WhenStoredKeyEmpty":0,"Tests\\Unit\\api\\ApiRouterTest::testHandleReturns400WhenMissingEndpoint":0,"Tests\\Unit\\api\\ApiRouterTest::testHandleReturns400WhenMissingAction":0,"Tests\\Unit\\api\\ApiRouterTest::testHandleReturns404ForUnknownEndpoint":0,"Tests\\Unit\\api\\ApiRouterTest::testSendSuccessOutputsCorrectJson":0,"Tests\\Unit\\api\\ApiRouterTest::testSendErrorOutputsCorrectJson":0,"Tests\\Unit\\api\\ApiRouterTest::testRequireMethodReturnsTrueForMatchingMethod":0,"Tests\\Unit\\api\\ApiRouterTest::testRequireMethodReturnsFalseAndSendsErrorForMismatch":0,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testStatusesReturnsFormattedList":0.001,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testStatusesRejectsPostMethod":0,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testTransportsReturnsFormattedList":0,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testTransportsRejectsPostMethod":0,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testPaymentMethodsReturnsFormattedList":0,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testPaymentMethodsRejectsPostMethod":0,"Tests\\Unit\\api\\Controllers\\OrdersApiControllerTest::testListReturnsOrders":0.001,"Tests\\Unit\\api\\Controllers\\OrdersApiControllerTest::testListRejectsPostMethod":0,"Tests\\Unit\\api\\Controllers\\OrdersApiControllerTest::testListPassesFiltersToRepository":0,"Tests\\Unit\\api\\Controllers\\OrdersApiControllerTest::testGetReturnsOrder":0,"Tests\\Unit\\api\\Controllers\\OrdersApiControllerTest::testGetReturns404WhenOrderNotFound":0,"Tests\\Unit\\api\\Controllers\\OrdersApiControllerTest::testGetReturns400WhenMissingId":0,"Tests\\Unit\\api\\Controllers\\OrdersApiControllerTest::testChangeStatusUpdatesOrder":0,"Tests\\Unit\\api\\Controllers\\OrdersApiControllerTest::testChangeStatusReturns400WhenMissingId":0,"Tests\\Unit\\api\\Controllers\\OrdersApiControllerTest::testChangeStatusRejectsGetMethod":0,"Tests\\Unit\\api\\Controllers\\OrdersApiControllerTest::testSetPaidReturns404WhenOrderNotFound":0,"Tests\\Unit\\api\\Controllers\\OrdersApiControllerTest::testSetPaidReturns400WhenMissingId":0,"Tests\\Unit\\api\\Controllers\\OrdersApiControllerTest::testSetPaidCallsServiceWhenOrderExists":0,"Tests\\Unit\\api\\Controllers\\OrdersApiControllerTest::testSetUnpaidReturns404WhenOrderNotFound":0,"Tests\\Unit\\api\\Controllers\\OrdersApiControllerTest::testSetUnpaidCallsServiceWhenOrderExists":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testListReturnsProducts":0.001,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testListRejectsPostMethod":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testListPassesFiltersToRepository":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testListDefaultPagination":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testListClampsPerPageTo100":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testGetReturnsProduct":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testGetReturns404WhenProductNotFound":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testGetReturns400WhenMissingId":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testGetRejectsPostMethod":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testCreateRejectsGetMethod":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testCreateReturns400WhenNoBody":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testUpdateRejectsGetMethod":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testUpdateReturns400WhenMissingId":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testUpdateReturns404WhenProductNotFound":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testUpdateReturns400WhenNoBody":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testMapApiToFormDataConvertsStatusToCheckbox":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testMapApiToFormDataMapsLanguages":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testMapApiToFormDataMapsNumericFields":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testMapApiToFormDataMapsCategories":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testMapApiToFormDataPartialUpdatePreservesExisting":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testMapApiToFormDataMapsForeignKeys":0,"Tests\\Unit\\Domain\\Update\\UpdateRepositoryTest::testVerifyChecksumValidFormat":0.012,"Tests\\Unit\\Domain\\Update\\UpdateRepositoryTest::testVerifyChecksumInvalidHash":0.009,"Tests\\Unit\\Domain\\Update\\UpdateRepositoryTest::testVerifyChecksumInvalidFormat":0.001,"Tests\\Unit\\Domain\\Update\\UpdateRepositoryTest::testCreateBackupWithEmptyManifest":0,"Tests\\Unit\\Domain\\Update\\UpdateRepositoryTest::testDownloadManifestReturnsNullForInvalidUrl":0.041,"Tests\\Unit\\Domain\\Attribute\\AttributeRepositoryTest::testListForApiReturnsActiveAttributesWithValues":0,"Tests\\Unit\\Domain\\Attribute\\AttributeRepositoryTest::testListForApiReturnsEmptyWhenNoAttributes":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testFindVariantsForApiReturnsVariants":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testFindVariantsForApiReturnsEmptyWhenNoVariants":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testFindVariantForApiReturnsVariant":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testFindVariantForApiReturnsNullForNonVariant":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testFindVariantForApiReturnsNullForNonexistent":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testCreateVariantForApiSuccess":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testCreateVariantForApiReturnsNullForArchivedParent":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testCreateVariantForApiReturnsNullWhenParentIsVariant":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testCreateVariantForApiReturnsNullForEmptyAttributes":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testCreateVariantForApiReturnsNullForDuplicateHash":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testUpdateVariantForApiSuccess":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testUpdateVariantForApiReturnsFalseForNonVariant":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testUpdateVariantForApiReturnsFalseForNonexistent":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testUpdateVariantForApiFiltersUnallowedFields":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testDeleteVariantForApiSuccess":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testDeleteVariantForApiReturnsFalseForNonVariant":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testDeleteVariantForApiReturnsFalseForNonexistent":0,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testAttributesReturnsFormattedList":0,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testAttributesRejectsPostMethod":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testVariantsReturnsVariantsList":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testVariantsReturns400WhenMissingId":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testVariantsReturns404WhenProductNotFound":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testVariantsReturns400ForVariantProduct":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testVariantsRejectsPostMethod":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testCreateVariantRejectsGetMethod":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testCreateVariantReturns400WhenMissingId":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testCreateVariantReturns400WhenNoBody":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testUpdateVariantRejectsGetMethod":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testUpdateVariantReturns400WhenMissingId":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testUpdateVariantReturns400WhenNoBody":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testDeleteVariantRejectsGetMethod":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testDeleteVariantReturns400WhenMissingId":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testDeleteVariantReturns404WhenNotFound":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testDeleteVariantSuccess":0,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testListPassesAttributeFilters":0,"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testUpdateVariantForApiCastsTypes":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testSavePersistsMinMaxOrderAmount":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testSaveConvertsEmptyMinMaxToNull":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testFindNormalizesMinMaxOrderAmount":0,"Tests\\Unit\\Domain\\PaymentMethod\\PaymentMethodRepositoryTest::testFindNormalizesNullMinMaxOrderAmount":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testTransportMethodsFrontHidesTransportWhenAllPaymentsExceedMaxAmount":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testTransportMethodsFrontHidesTransportWhenAllPaymentsBelowMinAmount":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testTransportMethodsFrontHidesTransportWithNoPaymentMethods":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testTransportMethodsFrontKeepsBothTransportsWhenPaymentsAvailable":0,"Tests\\Unit\\Domain\\Transport\\TransportRepositoryTest::testTransportMethodsFrontKeepsTransportIfAtLeastOnePaymentAvailable":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testOrderStatusDataReturnsBothNamesAndColors":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testOrderStatusDataFiltersInvalidHexColors":0,"Tests\\Unit\\Domain\\Order\\OrderRepositoryTest::testOrderStatusDataReturnsEmptyOnDbFailure":0,"Tests\\Unit\\admin\\Controllers\\ShopOrderControllerTest::testContrastTextColorReturnsBlackForLightColor":0,"Tests\\Unit\\admin\\Controllers\\ShopOrderControllerTest::testContrastTextColorReturnsWhiteForDarkColor":0,"Tests\\Unit\\admin\\Controllers\\ShopOrderControllerTest::testContrastTextColorHandlesShortHex":0,"Tests\\Unit\\admin\\Controllers\\ShopOrderControllerTest::testContrastTextColorDefaultsToWhiteForInvalidHex":0,"Tests\\Unit\\admin\\Controllers\\ShopOrderControllerTest::testSanitizeInlineHtmlStripsDisallowedTags":0,"Tests\\Unit\\admin\\Controllers\\ShopOrderControllerTest::testSanitizeInlineHtmlStripsAttributesFromAllowedTags":0,"Tests\\Unit\\admin\\Controllers\\ShopOrderControllerTest::testSanitizeInlineHtmlPreservesCleanTags":0,"Tests\\Unit\\admin\\Controllers\\ShopOrderControllerTest::testSanitizeInlineHtmlHandlesPlainText":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testGetLogsReturnsItemsAndTotal":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testGetLogsReturnsEmptyWhenNoResults":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testGetLogsHandlesNullFromSelect":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testDeleteLogCallsDelete":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testClearLogsDeletesAll":0,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testHasLogsMethods":0,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testLogsReturnsString":0,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testLogsClearReturnsVoid":0,"Tests\\Unit\\Domain\\Order\\OrderAdminServiceTest::testProcessApiloSyncQueueKeepsTaskWhenApiloOrderIdIsNull":0.023,"Tests\\Unit\\Domain\\Order\\OrderAdminServiceTest::testProcessApiloSyncQueueRemovesTaskAfterMaxAttempts":0.014,"Tests\\Unit\\api\\Controllers\\ProductsApiControllerTest::testMapApiToFormDataPreservesZeroBasePriceForSaveProduct":0,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testEnsureAttributeRejectsGetMethod":0,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testEnsureAttributeReturns400WhenNoBody":0,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testEnsureAttributeValueRejectsGetMethod":0,"Tests\\Unit\\api\\Controllers\\DictionariesApiControllerTest::testEnsureAttributeValueReturns400WhenNoBody":0,"Tests\\Unit\\Domain\\CronJob\\CronJobProcessorTest::testRegisterHandlerAndProcessJob":0.002,"Tests\\Unit\\Domain\\CronJob\\CronJobProcessorTest::testProcessQueueReturnsEmptyStatsWhenNoJobs":0,"Tests\\Unit\\Domain\\CronJob\\CronJobProcessorTest::testProcessQueueHandlerReturnsFalse":0,"Tests\\Unit\\Domain\\CronJob\\CronJobProcessorTest::testProcessQueueHandlerThrowsException":0,"Tests\\Unit\\Domain\\CronJob\\CronJobProcessorTest::testProcessQueueNoHandlerRegistered":0,"Tests\\Unit\\Domain\\CronJob\\CronJobProcessorTest::testProcessQueueHandlerReturnsArray":0,"Tests\\Unit\\Domain\\CronJob\\CronJobProcessorTest::testProcessQueuePassesPayloadToHandler":0,"Tests\\Unit\\Domain\\CronJob\\CronJobProcessorTest::testProcessQueueMultipleJobs":0,"Tests\\Unit\\Domain\\CronJob\\CronJobProcessorTest::testCreateScheduledJobsFromDueSchedules":0,"Tests\\Unit\\Domain\\CronJob\\CronJobProcessorTest::testCreateScheduledJobsSkipsDuplicates":0,"Tests\\Unit\\Domain\\CronJob\\CronJobProcessorTest::testCreateScheduledJobsWithPayload":0,"Tests\\Unit\\Domain\\CronJob\\CronJobProcessorTest::testCreateScheduledJobsReturnsZeroWhenNoSchedules":0,"Tests\\Unit\\Domain\\CronJob\\CronJobProcessorTest::testRunExecutesFullPipeline":0,"Tests\\Unit\\Domain\\CronJob\\CronJobProcessorTest::testRunReturnsScheduledCount":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testEnqueueInsertsJobAndReturnsId":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testEnqueueWithPayloadEncodesJson":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testEnqueueWithoutPayloadDoesNotSetPayloadKey":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testEnqueueWithScheduledAt":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testEnqueueReturnsNullOnFailure":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testFetchNextReturnsEmptyArrayWhenNoJobs":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testFetchNextUpdatesStatusToProcessing":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testFetchNextDecodesPayloadJson":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testMarkCompletedUpdatesStatus":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testMarkCompletedWithResult":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testMarkFailedWithRetriesLeft":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testMarkFailedWhenMaxAttemptsReached":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testMarkFailedTruncatesErrorTo500Chars":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testHasPendingJobReturnsTrueWhenExists":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testHasPendingJobReturnsFalseWhenNone":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testHasPendingJobWithPayloadMatch":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testCleanupDeletesOldCompletedJobs":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testRecoverStuckResetsProcessingJobs":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testGetDueSchedulesReturnsEnabledSchedules":0,"Tests\\Unit\\Domain\\CronJob\\CronJobRepositoryTest::testTouchScheduleUpdatesTimestamps":0,"Tests\\Unit\\Domain\\CronJob\\CronJobTypeTest::testAllTypesReturnsAllJobTypes":0,"Tests\\Unit\\Domain\\CronJob\\CronJobTypeTest::testAllStatusesReturnsAllStatuses":0,"Tests\\Unit\\Domain\\CronJob\\CronJobTypeTest::testPriorityConstants":0,"Tests\\Unit\\Domain\\CronJob\\CronJobTypeTest::testCalculateBackoffExponential":0,"Tests\\Unit\\Domain\\CronJob\\CronJobTypeTest::testCalculateBackoffCapsAtMax":0,"Tests\\Unit\\Domain\\CronJob\\CronJobTypeTest::testJobTypeConstantsMatchStrings":0,"Tests\\Unit\\Domain\\CronJob\\CronJobTypeTest::testStatusConstantsMatchStrings":0,"Tests\\Unit\\Domain\\Order\\OrderAdminServiceTest::testConstructorAcceptsCronJobRepo":0,"Tests\\Unit\\admin\\Controllers\\ProductArchiveControllerTest::testHasBulkDeletePermanentMethod":0,"Tests\\Unit\\admin\\Controllers\\ProductArchiveControllerTest::testBulkDeletePermanentMethodReturnType":0}} \ No newline at end of file diff --git a/.serena/project.yml b/.serena/project.yml index 5fc4073..a29cfe0 100644 --- a/.serena/project.yml +++ b/.serena/project.yml @@ -123,3 +123,7 @@ symbol_info_budget: # Note: the backend is fixed at startup. If a project with a different backend # is activated post-init, an error will be returned. language_backend: + +# list of regex patterns which, when matched, mark a memory entry as read‑only. +# Extends the list from the global configuration, merging the two lists. +read_only_memory_patterns: [] diff --git a/AGENTS.md b/AGENTS.md index addcb36..aa464f7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,30 +1,246 @@ -# Workflow +# CLAUDE.md -## KONIEC PRACY +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. -Gdy użytkownik napisze `KONIEC PRACY`, wykonaj kolejno: +## Project Overview -1. Przeprowadzenie testów. -2. Aktualizacja dokumentacji technicznej, jeśli zmiany tego wymagają: - - `docs/DATABASE_STRUCTURE.md` - - `docs/PROJECT_STRUCTURE.md` - - `docs/FORM_EDIT_SYSTEM.md` - - `docs/CHANGELOG.md` - - `docs/TESTING.md` -3. Migracje SQL (jeśli były zmiany w bazie danych): - - Plik: `migrations/{version}.sql` (np. `migrations/0.304.sql`) - - **NIE** w `updates/` — build script sam wczyta z `migrations/` - - Sprawdź czy plik istnieje i jest poprawnie nazwany przed commitem -4. Commit. -5. Push. +shopPRO is a PHP e-commerce platform with an admin panel and customer-facing storefront. It uses Medoo ORM (`$mdb`), Redis caching, and a Domain-Driven Design architecture with Dependency Injection (migration from legacy architecture complete). -## PRZED ROZPOCZĘCIEM PRACY +## Zasady pisania kodu +- Kod ma być czytelny „dla obcego”: jasne nazwy, mało magii +- Brak „skrótów na szybko” typu logika w widokach, copy-paste, losowe helpery bez spójności +- Każda funkcja/klasa ma mieć jedną odpowiedzialność, zwykle do 30–50 linii (jeśli dłuższe – dzielić) +- max 3 poziomy zagnieżdżeń (if/foreach), reszta do osobnych metod +- Nazewnictwo: + - klasy: PascalCase + - metody/zmienne: camelCase + - stałe: UPPER_SNAKE_CASE +- Zero „skrótologii” w nazwach (np. $d, $tmp, $x1) poza pętlami 2–3 linijki +- medoo + prepared statements bez wyjątków (żadnego sklejania SQL stringiem) +- XSS: escape w widokach (np. helper e()) +- CSRF dla formularzy, sensowna obsługa sesji +- Kod ma mieć komentarze tylko tam, gdzie wyjaśniają „dlaczego”, nie „co” -Przed rozpoczęciem implementacji sprawdź aktualną zawartość: +## PHP Version Constraint -- `docs/DATABASE_STRUCTURE.md` -- `docs/PROJECT_STRUCTURE.md` -- `docs/CHANGELOG.md` -- `docs/TESTING.md` +**Production runs PHP < 8.0.** Do NOT use: +- `match` expressions (use ternary operators or if/else) +- Named arguments +- Union types (`int|string`) +- `str_contains()`, `str_starts_with()`, `str_ends_with()` +- Other PHP 8.0+ syntax -To ma pomóc zachować spójność zmian i dokumentacji. +`composer.json` requires `>=7.4`. + +## Commands + +### Running Tests +```bash +# Full suite (recommended — PowerShell, auto-finds php) +./test.ps1 + +# Specific file +./test.ps1 tests/Unit/Domain/Product/ProductRepositoryTest.php + +# Specific test method +./test.ps1 --filter testGetQuantityReturnsCorrectValue + +# Alternative +composer test +``` + +PHPUnit 9.6 via `phpunit.phar`. Bootstrap: `tests/bootstrap.php`. Config: `phpunit.xml`. + +Current suite: **805 tests, 2253 assertions**. + +### Creating Updates +See `docs/UPDATE_INSTRUCTIONS.md` for the full procedure. Updates are ZIP packages in `updates/0.XX/`. Never include `*.md` files, `updates/changelog.php`, or root `.htaccess` in update ZIPs. + +## Architecture + +### Directory Structure +``` +shopPRO/ +├── autoload/ # Autoloaded classes (core codebase) +│ ├── Domain/ # Business logic repositories (\Domain\) +│ ├── Shared/ # Shared utilities (\Shared\) +│ │ ├── Cache/ # CacheHandler, RedisConnection +│ │ ├── Email/ # Email (PHPMailer wrapper) +│ │ ├── Helpers/ # Helpers (formerly class.S.php) +│ │ ├── Html/ # Html utility +│ │ ├── Image/ # ImageManipulator +│ │ └── Tpl/ # Template engine +│ ├── api/ # REST API layer (\api\) +│ │ ├── ApiRouter.php # API router (\api\ApiRouter) +│ │ └── Controllers/ # API controllers (\api\Controllers\) +│ ├── admin/ # Admin panel layer +│ │ ├── App.php # Admin router (\admin\App) +│ │ ├── Controllers/ # DI controllers (\admin\Controllers\) — 28 controllers +│ │ ├── Support/ # TableListRequestFactory, Forms/FormRequestHandler, Forms/FormFieldRenderer +│ │ ├── Validation/ # FormValidator +│ │ └── ViewModels/ # Forms/ (FormEditViewModel, FormField, FormTab, FormAction, FormFieldType), Common/ (PaginatedTableViewModel) +│ └── front/ # Frontend layer +│ ├── App.php # Frontend router (\front\App) +│ ├── LayoutEngine.php # Layout engine (\front\LayoutEngine) +│ ├── Controllers/ # DI controllers (\front\Controllers\) — 8 controllers +│ └── Views/ # Static views (\front\Views\) — 11 view classes +├── admin/ # Admin panel +│ ├── templates/ # Admin view templates +│ └── layout/ # Admin CSS/JS/icons +├── templates/ # Frontend view templates +├── libraries/ # Third-party libraries (Medoo, RedBeanPHP, PHPMailer) +├── tests/ # PHPUnit tests +│ ├── bootstrap.php +│ ├── stubs/ # Test stubs (CacheHandler, Helpers, ShopProduct) +│ └── Unit/ +│ ├── Domain/ # Repository tests +│ ├── admin/Controllers/ # Controller tests +│ └── api/ # API tests +├── updates/ # Update packages for clients +├── docs/ # Technical documentation +├── config.php # Database/Redis config (not in repo) +├── index.php # Frontend entry point +├── ajax.php # Frontend AJAX handler +├── admin/index.php # Admin entry point +├── admin/ajax.php # Admin AJAX handler +├── cron.php # CRON jobs (Apilo sync) +└── api.php # REST API (ordersPRO + Ekomi) +``` + +### Autoloader + +Custom autoloader in each entry point (not Composer autoload at runtime). Tries two filename conventions: +1. `autoload/{namespace}/class.{ClassName}.php` (legacy) +2. `autoload/{namespace}/{ClassName}.php` (PSR-4 style, fallback) + +### Namespace Conventions (case-sensitive on Linux!) +- `\Domain\` → `autoload/Domain/` (uppercase D) +- `\admin\Controllers\` → `autoload/admin/Controllers/` (lowercase a) +- `\Shared\` → `autoload/Shared/` +- `\api\` → `autoload/api/` +- Do NOT use `\Admin\` (uppercase A) — the server directory is `admin/` (lowercase) +- `\shop\` namespace is **deleted** — all 12 legacy classes migrated to `\Domain\`, `autoload/shop/` directory removed + +### Domain-Driven Architecture (migration complete) + +All legacy directories (`admin/controls/`, `admin/factory/`, `admin/view/`, `front/controls/`, `front/view/`, `front/factory/`, `shop/`) have been deleted. All modules now use this pattern: + +**Domain Layer** (`autoload/Domain/{Module}/`): +- `{Module}Repository.php` — data access, business logic, Redis caching +- Constructor DI with `$db` (Medoo instance) +- Methods serve both admin and frontend (shared Domain, no separate services) + +**Domain Modules**: Article, Attribute, Banner, Basket, Cache, Category, Client, Coupon, CronJob, Dashboard, Dictionaries, Integrations, Languages, Layouts, Newsletter, Order, Pages, PaymentMethod, Producer, Product, ProductSet, Promotion, Scontainers, Settings, ShopStatus, Transport, Update, User + +**Admin Controllers** (`autoload/admin/Controllers/`): +- DI via constructor (repositories injected) +- Wired in `admin\App::getControllerFactories()` + +**Frontend Controllers** (`autoload/front/Controllers/`): +- DI via constructor +- Wired in `front\App::getControllerFactories()` + +**Frontend Views** (`autoload/front/Views/`): +- Static classes, no state, no DI — pure rendering + +**API Controllers** (`autoload/api/Controllers/`): +- DI via constructor, stateless (no session) +- Wired in `api\ApiRouter::getControllerFactories()` +- Auth: `X-Api-Key` header vs `pp_settings.api_key` + +### Key Classes +| Class | Purpose | +|-------|---------| +| `\admin\App` | Admin router — maps URL segments to controllers | +| `\front\App` | Frontend router — `route()`, `checkUrlParams()` | +| `\front\LayoutEngine` | Frontend layout engine — `show()`, tag replacement | +| `\Shared\Helpers\Helpers` | Utility methods (SEO, email, cache clearing) | +| `\Shared\Tpl\Tpl` | Template engine — `render()`, `set()` | +| `\Shared\Cache\CacheHandler` | Redis cache — `get()`, `set()`, `delete()`, `deletePattern()` | +| `\api\ApiRouter` | REST API router — auth, routing, response helpers | + +### Database +- ORM: Medoo (`$mdb` global variable, injected via DI in new code) +- Table prefix: `pp_` +- Key tables: `pp_shop_products`, `pp_shop_orders`, `pp_shop_categories`, `pp_shop_clients` +- Full schema: `docs/DATABASE_STRUCTURE.md` + +### Form Edit System +Universal form system for admin edit views. Docs: `docs/FORM_EDIT_SYSTEM.md`. +- **ViewModels** (`admin\ViewModels\Forms\`): `FormEditViewModel`, `FormField`, `FormTab`, `FormAction`, `FormFieldType` +- **Validation**: `admin\Validation\FormValidator` +- **Rendering**: `admin\Support\Forms\FormFieldRenderer`, `admin\Support\Forms\FormRequestHandler` +- **Template**: `admin/templates/components/form-edit.php` +- **Table lists**: `admin\Support\TableListRequestFactory` + `admin\ViewModels\Common\PaginatedTableViewModel` + +### Caching +- Redis via `\Shared\Cache\CacheHandler` (singleton `RedisConnection`) +- Key pattern for products: `shop\product:{id}:{lang}:{permutation_hash}` +- Clear product cache: `\Shared\Helpers\Helpers::clear_product_cache($id)` +- Pattern delete: `CacheHandler::deletePattern("shop\\product:{$id}:*")` +- Default TTL: 86400 (24h) +- Data is serialized — requires `unserialize()` after `get()` +- Config: `config.php` (`$config['redis']`) + +## Code Patterns + +### New code should follow DI pattern +```php +// Repository with constructor DI +class ExampleRepository { + private $db; + public function __construct($db) { + $this->db = $db; + } + public function find(int $id): ?array { + return $this->db->get('pp_table', '*', ['id' => $id]); + } +} + +// Controller wiring (in admin\App or front\App) +$repo = new \Domain\Example\ExampleRepository($mdb); +$controller = new \admin\Controllers\ExampleController($repo); +``` + +### Medoo ORM pitfalls +- `$mdb->delete($table, $where)` takes **2 arguments**, NOT 3 — has caused bugs +- `$mdb->get()` returns `null` when no record, NOT `false` +- After `$mdb->insert()`, check `$mdb->id()` to confirm success + +### File naming +- New classes: `ClassName.php` (no `class.` prefix) +- Legacy classes: `class.ClassName.php` (leave until migrated) + +### Test conventions +- Extend `PHPUnit\Framework\TestCase` +- Mock Medoo: `$this->createMock(\medoo::class)` +- AAA pattern: Arrange, Act, Assert +- Tests mirror source structure: `tests/Unit/Domain/{Module}/{Class}Test.php` + +## Workflow (AGENTS.md) + +When user says **"KONIEC PRACY"**, execute in order: +1. Run tests +2. Update documentation if needed: `docs/DATABASE_STRUCTURE.md`, `docs/PROJECT_STRUCTURE.md`, `docs/FORM_EDIT_SYSTEM.md`, `docs/CHANGELOG.md`, `docs/TESTING.md` +3. SQL migrations (if DB changes): place in `migrations/{version}.sql` (e.g. `migrations/0.304.sql`). **NOT** in `updates/` — build script reads from `migrations/` automatically +4. Commit +5. Push +6. Build update package: `git tag v0.XXX && powershell.exe -ExecutionPolicy Bypass -File build-update.ps1 -FromTag v0.PREV -ToTag v0.XXX -ChangelogEntry "opis"` — skrypt automatycznie aktualizuje `versions.php` +7. Commit i push plików paczki (`updates/0.30/ver_0.XXX.zip`, `ver_0.XXX_manifest.json`, `updates/versions.php`, `updates/changelog-data.html`) + +Before starting implementation, review current state of docs (see AGENTS.md for full list). + +## Key Documentation +- `docs/MEMORY.md` — project memory: known issues, confirmed patterns, ORM pitfalls, caching conventions +- `docs/PROJECT_STRUCTURE.md` — current architecture, layers, cache, entry points, integrations +- `docs/DATABASE_STRUCTURE.md` — full database schema +- `docs/TESTING.md` — test suite guide and structure +- `docs/FORM_EDIT_SYSTEM.md` — form system architecture +- `docs/CHANGELOG.md` — version history +- `api-docs/api-reference.json` — REST API documentation (ordersPRO) +- `api-docs/index.html` — REST API documentation (ordersPRO) +- `docs/UPDATE_INSTRUCTIONS.md` — how to build client update packages + +## Za każdym razem jak próbujesz sprawdzić jakiś plik z logami spróbuj go najpierw pobrać z serwera FTP + +## Wszystkie pliki które tworzysz jako pomocnicze, np build_0330.ps1 czy build-update.ps1 twórz w folderze temp \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index a72ab9e..aa464f7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,6 +6,21 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co shopPRO is a PHP e-commerce platform with an admin panel and customer-facing storefront. It uses Medoo ORM (`$mdb`), Redis caching, and a Domain-Driven Design architecture with Dependency Injection (migration from legacy architecture complete). +## Zasady pisania kodu +- Kod ma być czytelny „dla obcego”: jasne nazwy, mało magii +- Brak „skrótów na szybko” typu logika w widokach, copy-paste, losowe helpery bez spójności +- Każda funkcja/klasa ma mieć jedną odpowiedzialność, zwykle do 30–50 linii (jeśli dłuższe – dzielić) +- max 3 poziomy zagnieżdżeń (if/foreach), reszta do osobnych metod +- Nazewnictwo: + - klasy: PascalCase + - metody/zmienne: camelCase + - stałe: UPPER_SNAKE_CASE +- Zero „skrótologii” w nazwach (np. $d, $tmp, $x1) poza pętlami 2–3 linijki +- medoo + prepared statements bez wyjątków (żadnego sklejania SQL stringiem) +- XSS: escape w widokach (np. helper e()) +- CSRF dla formularzy, sensowna obsługa sesji +- Kod ma mieć komentarze tylko tam, gdzie wyjaśniają „dlaczego”, nie „co” + ## PHP Version Constraint **Production runs PHP < 8.0.** Do NOT use: @@ -222,7 +237,8 @@ Before starting implementation, review current state of docs (see AGENTS.md for - `docs/TESTING.md` — test suite guide and structure - `docs/FORM_EDIT_SYSTEM.md` — form system architecture - `docs/CHANGELOG.md` — version history -- `docs/API.md` — REST API documentation (ordersPRO) +- `api-docs/api-reference.json` — REST API documentation (ordersPRO) +- `api-docs/index.html` — REST API documentation (ordersPRO) - `docs/UPDATE_INSTRUCTIONS.md` — how to build client update packages ## Za każdym razem jak próbujesz sprawdzić jakiś plik z logami spróbuj go najpierw pobrać z serwera FTP diff --git a/api-docs/api-reference.json b/api-docs/api-reference.json new file mode 100644 index 0000000..0496cb5 --- /dev/null +++ b/api-docs/api-reference.json @@ -0,0 +1,292 @@ +{ + "name": "shopPRO API", + "version": "1.0.0", + "entrypoint": "/api.php", + "authentication": { + "type": "header", + "header": "X-Api-Key", + "required": true, + "description": "API key stored in pp_settings.param=api_key" + }, + "response_format": { + "success": { + "status": "ok", + "data": {} + }, + "error": { + "status": "error", + "code": "BAD_REQUEST", + "message": "Human-readable error message" + }, + "error_codes": [ + { "code": "UNAUTHORIZED", "http": 401 }, + { "code": "BAD_REQUEST", "http": 400 }, + { "code": "NOT_FOUND", "http": 404 }, + { "code": "METHOD_NOT_ALLOWED", "http": 405 }, + { "code": "INTERNAL_ERROR", "http": 500 } + ] + }, + "endpoints": [ + { + "group": "orders", + "action": "list", + "method": "GET", + "url_template": "/api.php?endpoint=orders&action=list", + "query_params": [ + { "name": "status", "type": "string", "required": false }, + { "name": "paid", "type": "string", "required": false }, + { "name": "date_from", "type": "string", "required": false, "format": "YYYY-MM-DD" }, + { "name": "date_to", "type": "string", "required": false, "format": "YYYY-MM-DD" }, + { "name": "updated_since", "type": "string", "required": false, "format": "YYYY-MM-DD HH:MM:SS" }, + { "name": "number", "type": "string", "required": false }, + { "name": "client", "type": "string", "required": false }, + { "name": "page", "type": "integer", "required": false, "default": 1, "min": 1 }, + { "name": "per_page", "type": "integer", "required": false, "default": 50, "min": 1, "max": 100 } + ] + }, + { + "group": "orders", + "action": "get", + "method": "GET", + "url_template": "/api.php?endpoint=orders&action=get&id={order_id}", + "query_params": [ + { "name": "id", "type": "integer", "required": true, "min": 1 } + ] + }, + { + "group": "orders", + "action": "change_status", + "method": "PUT", + "url_template": "/api.php?endpoint=orders&action=change_status&id={order_id}", + "query_params": [ + { "name": "id", "type": "integer", "required": true, "min": 1 } + ], + "json_body": { + "required_fields": ["status_id"], + "fields": { + "status_id": { "type": "integer" }, + "send_email": { "type": "boolean", "required": false } + } + } + }, + { + "group": "orders", + "action": "set_paid", + "method": "PUT", + "url_template": "/api.php?endpoint=orders&action=set_paid&id={order_id}", + "query_params": [ + { "name": "id", "type": "integer", "required": true, "min": 1 } + ], + "json_body": { + "required_fields": [], + "fields": { + "send_email": { "type": "boolean", "required": false } + } + } + }, + { + "group": "orders", + "action": "set_unpaid", + "method": "PUT", + "url_template": "/api.php?endpoint=orders&action=set_unpaid&id={order_id}", + "query_params": [ + { "name": "id", "type": "integer", "required": true, "min": 1 } + ] + }, + { + "group": "products", + "action": "list", + "method": "GET", + "url_template": "/api.php?endpoint=products&action=list", + "query_params": [ + { "name": "search", "type": "string", "required": false }, + { "name": "status", "type": "string", "required": false }, + { "name": "promoted", "type": "string", "required": false }, + { "name": "attribute_{id}", "type": "integer", "required": false, "description": "e.g. attribute_5=12" }, + { "name": "sort", "type": "string", "required": false, "default": "id", "allowed": ["id", "name", "price_brutto", "status", "promoted", "quantity"] }, + { "name": "sort_dir", "type": "string", "required": false, "default": "DESC", "allowed": ["ASC", "DESC"] }, + { "name": "page", "type": "integer", "required": false, "default": 1, "min": 1 }, + { "name": "per_page", "type": "integer", "required": false, "default": 50, "min": 1, "max": 100 } + ] + }, + { + "group": "products", + "action": "get", + "method": "GET", + "url_template": "/api.php?endpoint=products&action=get&id={product_id}", + "query_params": [ + { "name": "id", "type": "integer", "required": true, "min": 1 } + ] + }, + { + "group": "products", + "action": "create", + "method": "POST", + "url_template": "/api.php?endpoint=products&action=create", + "json_body": { + "required_fields": ["languages", "price_brutto"], + "rules": [ + "languages must be an object with at least one language entry containing name", + "price_brutto must be numeric and >= 0" + ] + } + }, + { + "group": "products", + "action": "update", + "method": "PUT", + "url_template": "/api.php?endpoint=products&action=update&id={product_id}", + "query_params": [ + { "name": "id", "type": "integer", "required": true, "min": 1 } + ], + "json_body": { + "required_fields": [], + "rules": ["partial update; only changed fields are needed"] + } + }, + { + "group": "products", + "action": "variants", + "method": "GET", + "url_template": "/api.php?endpoint=products&action=variants&id={product_id}", + "query_params": [ + { "name": "id", "type": "integer", "required": true, "min": 1 } + ] + }, + { + "group": "products", + "action": "create_variant", + "method": "POST", + "url_template": "/api.php?endpoint=products&action=create_variant&id={product_id}", + "query_params": [ + { "name": "id", "type": "integer", "required": true, "min": 1 } + ], + "json_body": { + "required_fields": ["attributes"], + "fields": { + "attributes": { "type": "object", "description": "Map attribute_id -> value_id" } + } + } + }, + { + "group": "products", + "action": "update_variant", + "method": "PUT", + "url_template": "/api.php?endpoint=products&action=update_variant&id={variant_id}", + "query_params": [ + { "name": "id", "type": "integer", "required": true, "min": 1 } + ], + "json_body": { + "required_fields": [], + "rules": ["partial update of variant fields"] + } + }, + { + "group": "products", + "action": "delete_variant", + "method": "DELETE", + "url_template": "/api.php?endpoint=products&action=delete_variant&id={variant_id}", + "query_params": [ + { "name": "id", "type": "integer", "required": true, "min": 1 } + ] + }, + { + "group": "products", + "action": "upload_image", + "method": "POST", + "url_template": "/api.php?endpoint=products&action=upload_image", + "json_body": { + "required_fields": ["id", "file_name", "content_base64"], + "fields": { + "id": { "type": "integer", "description": "product id" }, + "file_name": { "type": "string" }, + "content_base64": { "type": "string", "description": "base64 payload" }, + "alt": { "type": "string", "required": false }, + "o": { "type": "integer", "required": false, "description": "image position" } + } + } + }, + { + "group": "dictionaries", + "action": "statuses", + "method": "GET", + "url_template": "/api.php?endpoint=dictionaries&action=statuses" + }, + { + "group": "dictionaries", + "action": "transports", + "method": "GET", + "url_template": "/api.php?endpoint=dictionaries&action=transports" + }, + { + "group": "dictionaries", + "action": "payment_methods", + "method": "GET", + "url_template": "/api.php?endpoint=dictionaries&action=payment_methods" + }, + { + "group": "dictionaries", + "action": "attributes", + "method": "GET", + "url_template": "/api.php?endpoint=dictionaries&action=attributes" + }, + { + "group": "dictionaries", + "action": "ensure_attribute", + "method": "POST", + "url_template": "/api.php?endpoint=dictionaries&action=ensure_attribute", + "json_body": { + "required_fields": ["name"], + "fields": { + "name": { "type": "string" }, + "type": { "type": "integer", "required": false, "default": 0 }, + "lang": { "type": "string", "required": false, "default": "pl" } + } + } + }, + { + "group": "dictionaries", + "action": "ensure_attribute_value", + "method": "POST", + "url_template": "/api.php?endpoint=dictionaries&action=ensure_attribute_value", + "json_body": { + "required_fields": ["attribute_id", "name"], + "fields": { + "attribute_id": { "type": "integer" }, + "name": { "type": "string" }, + "lang": { "type": "string", "required": false, "default": "pl" } + } + } + }, + { + "group": "dictionaries", + "action": "ensure_producer", + "method": "POST", + "url_template": "/api.php?endpoint=dictionaries&action=ensure_producer", + "json_body": { + "required_fields": ["name"], + "fields": { + "name": { "type": "string" } + } + } + }, + { + "group": "categories", + "action": "list", + "method": "GET", + "url_template": "/api.php?endpoint=categories&action=list" + } + ], + "examples": { + "curl_list_products": "curl -X GET \"https://twoja-domena.pl/api.php?endpoint=products&action=list&page=1&per_page=20\" -H \"X-Api-Key: TWOJ_KLUCZ\"", + "curl_get_order": "curl -X GET \"https://twoja-domena.pl/api.php?endpoint=orders&action=get&id=42\" -H \"X-Api-Key: TWOJ_KLUCZ\"", + "curl_create_product": "curl -X POST \"https://twoja-domena.pl/api.php?endpoint=products&action=create\" -H \"X-Api-Key: TWOJ_KLUCZ\" -H \"Content-Type: application/json\" -d \"{\\\"price_brutto\\\":99.99,\\\"languages\\\":{\\\"pl\\\":{\\\"name\\\":\\\"Nowy produkt\\\"}}}\"" + }, + "source_of_truth": [ + "autoload/api/ApiRouter.php", + "autoload/api/Controllers/OrdersApiController.php", + "autoload/api/Controllers/ProductsApiController.php", + "autoload/api/Controllers/DictionariesApiController.php", + "autoload/api/Controllers/CategoriesApiController.php" + ] +} diff --git a/api-docs/index.html b/api-docs/index.html new file mode 100644 index 0000000..9cee90a --- /dev/null +++ b/api-docs/index.html @@ -0,0 +1,60 @@ + + + + + + shopPRO API docs + + + +

shopPRO API - public docs

+
Ladowanie...
+

Machine-readable JSON: api-reference.json

+ +

Endpointy

+ + + + + + + + + + +
GroupActionMethodURL template
+ + + + diff --git a/docs/API.md b/docs/API.md deleted file mode 100644 index 5688e19..0000000 --- a/docs/API.md +++ /dev/null @@ -1,597 +0,0 @@ -# shopPRO REST API - -REST API do integracji z ordersPRO i innymi systemami zewnetrznymi. - -## Autentykacja - -Kazde zapytanie wymaga headera `X-Api-Key` z kluczem API. - -``` -X-Api-Key: {klucz_api} -``` - -Klucz przechowywany jest w `pp_settings` jako parametr `api_key`. API jest stateless (bez sesji). - -## Format odpowiedzi - -### Sukces (HTTP 200) -```json -{ - "status": "ok", - "data": { ... } -} -``` - -### Blad -```json -{ - "status": "error", - "code": "UNAUTHORIZED", - "message": "Invalid or missing API key" -} -``` - -Kody bledow: -| Kod | HTTP | Opis | -|-----|------|------| -| `UNAUTHORIZED` | 401 | Brak lub nieprawidlowy klucz API | -| `BAD_REQUEST` | 400 | Brakujace lub niepoprawne parametry | -| `NOT_FOUND` | 404 | Nie znaleziono zasobu/endpointu/akcji | -| `METHOD_NOT_ALLOWED` | 405 | Nieprawidlowa metoda HTTP | -| `INTERNAL_ERROR` | 500 | Blad wewnetrzny serwera | - -## Endpointy - -### Zamowienia - -#### Lista zamowien -``` -GET api.php?endpoint=orders&action=list -``` - -Parametry filtrowania (opcjonalne): -| Parametr | Typ | Opis | -|----------|-----|------| -| `status` | int | Filtruj po statusie zamowienia | -| `paid` | int (0/1) | Filtruj po statusie platnosci | -| `date_from` | date (YYYY-MM-DD) | Zamowienia od daty | -| `date_to` | date (YYYY-MM-DD) | Zamowienia do daty | -| `updated_since` | datetime (YYYY-MM-DD HH:MM:SS) | Zamowienia zmodyfikowane od podanej daty (klucz do pollingu) | -| `number` | string | Szukaj po numerze zamowienia | -| `client` | string | Szukaj po imieniu, nazwisku lub emailu klienta | -| `page` | int | Numer strony (domyslnie 1) | -| `per_page` | int | Wynikow na strone (domyslnie 50, max 100) | - -Odpowiedz: -```json -{ - "status": "ok", - "data": { - "items": [ - { - "id": 42, - "number": "2026/02/001", - "date_order": "2026-02-19 10:30:00", - "updated_at": "2026-02-19 12:00:00", - "status": 4, - "paid": 1, - "client_name": "Jan", - "client_surname": "Kowalski", - "client_email": "jan@example.com", - "client_phone": "111222333", - "client_street": "Testowa 1", - "client_postal_code": "00-000", - "client_city": "Warszawa", - "firm_name": null, - "firm_nip": null, - "transport": "Kurier DPD", - "transport_cost": 15.00, - "payment_method": "Przelew bankowy", - "summary": 150.00 - } - ], - "total": 1, - "page": 1, - "per_page": 50 - } -} -``` - -#### Szczegoly zamowienia -``` -GET api.php?endpoint=orders&action=get&id={order_id} -``` - -Zwraca pelne dane zamowienia z produktami i historia statusow. - -#### Zmiana statusu zamowienia -``` -PUT api.php?endpoint=orders&action=change_status&id={order_id} -Content-Type: application/json - -{ - "status_id": 5, - "send_email": true -} -``` - -Odpowiedz: -```json -{ - "status": "ok", - "data": { - "order_id": 42, - "status_id": 5, - "changed": true - } -} -``` - -#### Oznacz jako oplacone -``` -PUT api.php?endpoint=orders&action=set_paid&id={order_id} -``` - -Opcjonalnie w body: `{"send_email": true}` - -#### Oznacz jako nieoplacone -``` -PUT api.php?endpoint=orders&action=set_unpaid&id={order_id} -``` - -### Produkty - -#### Lista produktow -``` -GET api.php?endpoint=products&action=list -``` - -Parametry filtrowania (opcjonalne): -| Parametr | Typ | Opis | -|----------|-----|------| -| `search` | string | Szukaj po nazwie, EAN lub SKU | -| `status` | int (0/1) | Filtruj po statusie (1 = aktywny, 0 = nieaktywny) | -| `promoted` | int (0/1) | Filtruj po promocji | -| `attribute_{id}` | int | Filtruj po atrybucie — `attribute_1=3` oznacza atrybut 1 = wartosc 3 (wiele filtrow AND) | -| `sort` | string | Sortuj po: id, name, price_brutto, status, promoted, quantity (domyslnie id) | -| `sort_dir` | string | Kierunek: ASC lub DESC (domyslnie DESC) | -| `page` | int | Numer strony (domyslnie 1) | -| `per_page` | int | Wynikow na strone (domyslnie 50, max 100) | - -Odpowiedz: -```json -{ - "status": "ok", - "data": { - "items": [ - { - "id": 1, - "sku": "PROD-001", - "ean": "5901234123457", - "name": "Produkt testowy", - "price_brutto": 99.99, - "price_brutto_promo": null, - "price_netto": 81.29, - "price_netto_promo": null, - "quantity": 10, - "status": 1, - "promoted": 0, - "vat": 23, - "weight": 0.5, - "main_image": "product1.jpg", - "date_add": "2026-01-15 10:00:00", - "date_modify": "2026-02-19 12:00:00" - } - ], - "total": 1, - "page": 1, - "per_page": 50 - } -} -``` - -#### Szczegoly produktu -``` -GET api.php?endpoint=products&action=get&id={product_id} -``` - -Zwraca pelne dane produktu z jezykami, zdjeciami, kategoriami i atrybutami. - -Odpowiedz: -```json -{ - "status": "ok", - "data": { - "id": 1, - "sku": "PROD-001", - "ean": "5901234123457", - "price_brutto": 99.99, - "price_brutto_promo": null, - "price_netto": 81.29, - "price_netto_promo": null, - "quantity": 10, - "status": 1, - "promoted": 0, - "vat": 23, - "weight": 0.5, - "stock_0_buy": 0, - "custom_label_0": null, - "new_to_date": null, - "additional_message": 0, - "additional_message_required": 0, - "additional_message_text": null, - "set_id": null, - "product_unit_id": 1, - "producer_id": 3, - "producer_name": "Nike", - "date_add": "2026-01-15 10:00:00", - "date_modify": "2026-02-19 12:00:00", - "languages": { - "pl": { - "name": "Produkt testowy", - "short_description": "Krotki opis", - "description": "

Pelny opis produktu

", - "meta_description": null, - "meta_keywords": null, - "meta_title": null, - "seo_link": "produkt-testowy", - "copy_from": null, - "warehouse_message_zero": null, - "warehouse_message_nonzero": null, - "tab_name_1": null, - "tab_description_1": null, - "tab_name_2": null, - "tab_description_2": null, - "canonical": null, - "security_information": null - } - }, - "images": [ - {"id": 1, "src": "product1.jpg", "alt": "Zdjecie produktu"} - ], - "categories": [1, 5], - "attributes": [ - { - "attribute_id": 1, - "attribute_type": 1, - "attribute_names": {"pl": "Kolor", "en": "Color"}, - "value_id": 3, - "value_names": {"pl": "Czerwony", "en": "Red"} - } - ], - "custom_fields": [ - {"name": "Napis na koszulce", "type": "text", "is_required": 1} - ], - "variants": [ - { - "id": 101, - "permutation_hash": "1-3|2-5", - "sku": "PROD-001-RED-L", - "ean": null, - "price_brutto": 109.99, - "price_brutto_promo": null, - "price_netto": 89.42, - "price_netto_promo": null, - "quantity": 5, - "stock_0_buy": 0, - "weight": 0.5, - "status": 1, - "attributes": [ - {"attribute_id": 1, "attribute_names": {"pl": "Kolor"}, "value_id": 3, "value_names": {"pl": "Czerwony"}}, - {"attribute_id": 2, "attribute_names": {"pl": "Rozmiar"}, "value_id": 5, "value_names": {"pl": "L"}} - ] - } - ] - } -} -``` - -#### Tworzenie produktu -``` -POST api.php?endpoint=products&action=create -Content-Type: application/json - -{ - "price_brutto": 99.99, - "vat": 23, - "quantity": 10, - "status": 1, - "sku": "PROD-001", - "ean": "5901234123457", - "new_to_date": null, - "additional_message": 0, - "additional_message_required": 0, - "additional_message_text": null, - "weight": 0.5, - "languages": { - "pl": { - "name": "Nowy produkt", - "description": "

Opis produktu

" - } - }, - "categories": [1, 5], - "products_related": [10, 20], - "custom_fields": [ - {"name": "Napis na koszulce", "type": "text", "is_required": 1} - ] -} -``` - -Wymagane: `languages` (min. 1 jezyk z `name`) oraz `price_brutto`. -`custom_fields` — opcjonalne; kazdy element wymaga `name`, `type` (domyslnie `text`), `is_required` (0/1). - -Odpowiedz (HTTP 201): -```json -{ - "status": "ok", - "data": { - "id": 42 - } -} -``` - -#### Aktualizacja produktu -``` -PUT api.php?endpoint=products&action=update&id={product_id} -Content-Type: application/json - -{ - "price_brutto": 129.99, - "status": 1, - "new_to_date": "2026-12-31", - "additional_message": 1, - "additional_message_required": 0, - "additional_message_text": "Dodaj tresc do personalizacji", - "languages": { - "pl": { - "name": "Zaktualizowana nazwa" - } - } -} -``` - -Partial update — wystarczy przeslac tylko zmienione pola. Pola nieprzeslane zachowuja aktualna wartosc. -Pola ustawien produktu obslugiwane przez API: `new_to_date`, `additional_message`, `additional_message_required`, `additional_message_text`. - -Odpowiedz: pelne dane produktu (jak w `get`). - -### Warianty produktow - -#### Lista wariantow produktu -``` -GET api.php?endpoint=products&action=variants&id={product_id} -``` - -Zwraca warianty produktu nadrzednego wraz z dostepnymi atrybutami. - -Odpowiedz: -```json -{ - "status": "ok", - "data": { - "product_id": 1, - "available_attributes": [ - { - "id": 1, - "type": 1, - "status": 1, - "names": {"pl": "Kolor", "en": "Color"}, - "values": [ - {"id": 3, "names": {"pl": "Czerwony", "en": "Red"}, "is_default": 0, "impact_on_the_price": null}, - {"id": 4, "names": {"pl": "Niebieski", "en": "Blue"}, "is_default": 0, "impact_on_the_price": 10.0} - ] - } - ], - "variants": [ - { - "id": 101, - "permutation_hash": "1-3", - "sku": "PROD-001-RED", - "ean": null, - "price_brutto": 109.99, - "price_brutto_promo": null, - "price_netto": 89.42, - "price_netto_promo": null, - "quantity": 5, - "stock_0_buy": 0, - "weight": 0.5, - "status": 1, - "attributes": [ - {"attribute_id": 1, "attribute_names": {"pl": "Kolor"}, "value_id": 3, "value_names": {"pl": "Czerwony"}} - ] - } - ] - } -} -``` - -#### Tworzenie wariantu -``` -POST api.php?endpoint=products&action=create_variant&id={product_id} -Content-Type: application/json - -{ - "attributes": {"1": 3, "2": 5}, - "sku": "PROD-001-RED-L", - "ean": "5901234123458", - "price_brutto": 109.99, - "quantity": 5, - "weight": 0.5 -} -``` - -Wymagane: `attributes` (mapa attribute_id -> value_id, min. 1). Kombinacja atrybutow musi byc unikalna. - -Odpowiedz (HTTP 201): pelne dane wariantu. - -#### Aktualizacja wariantu -``` -PUT api.php?endpoint=products&action=update_variant&id={variant_id} -Content-Type: application/json - -{ - "sku": "PROD-001-RED-XL", - "price_brutto": 119.99, - "quantity": 3 -} -``` - -Partial update — mozna zmienic: sku, ean, price_brutto, price_netto, price_brutto_promo, price_netto_promo, quantity, stock_0_buy, weight, status. - -Odpowiedz: pelne dane wariantu. - -#### Usuwanie wariantu -``` -DELETE api.php?endpoint=products&action=delete_variant&id={variant_id} -``` - -Odpowiedz: -```json -{ - "status": "ok", - "data": {"id": 101, "deleted": true} -} -``` - -### Kategorie - -#### Lista kategorii -``` -GET api.php?endpoint=categories&action=list -``` - -Zwraca plaska liste wszystkich aktywnych kategorii w domyslnym jezyku sklepu. - -Odpowiedz: -```json -{ - "status": "ok", - "data": { - "categories": [ - {"id": 1, "parent_id": null, "title": "Kategoria glowna"}, - {"id": 3, "parent_id": 1, "title": "Podkategoria A"}, - {"id": 5, "parent_id": 1, "title": "Podkategoria B"} - ] - } -} -``` - -Pola odpowiedzi: -| Pole | Typ | Opis | -|------|-----|------| -| `id` | int | ID kategorii | -| `parent_id` | int\|null | ID kategorii nadrzednej (null = kategoria glowna) | -| `title` | string | Nazwa w domyslnym jezyku; fallback na inny jezyk jesli brak tlumaczenia | - ---- - -### Slowniki - -#### Lista statusow zamowien -``` -GET api.php?endpoint=dictionaries&action=statuses -``` - -Odpowiedz: -```json -{ - "status": "ok", - "data": [ - {"id": 0, "name": "Nowe"}, - {"id": 1, "name": "Oplacone"}, - {"id": 4, "name": "W realizacji"}, - {"id": 6, "name": "Wyslane"} - ] -} -``` - -#### Lista metod transportu -``` -GET api.php?endpoint=dictionaries&action=transports -``` - -#### Lista metod platnosci -``` -GET api.php?endpoint=dictionaries&action=payment_methods -``` - -#### Lista atrybutow -``` -GET api.php?endpoint=dictionaries&action=attributes -``` - -Zwraca aktywne atrybuty z wartosciami i wielojezycznymi nazwami. - -#### Znajdz lub utworz producenta -``` -POST api.php?endpoint=dictionaries&action=ensure_producer -Content-Type: application/json - -{ - "name": "Nike" -} -``` - -Zwraca istniejacego producenta po nazwie lub tworzy nowego. Uzyc przed tworzeniem produktu, jesli producent moze nie istniec. - -Odpowiedz: -```json -{ - "status": "ok", - "data": { - "id": 5, - "created": false - } -} -``` - -`created: true` gdy producent zostal nowo dodany, `false` gdy juz istnial. - -Odpowiedz: -```json -{ - "status": "ok", - "data": [ - { - "id": 1, - "type": 1, - "status": 1, - "names": {"pl": "Kolor", "en": "Color"}, - "values": [ - {"id": 3, "names": {"pl": "Czerwony"}, "is_default": 0, "impact_on_the_price": null}, - {"id": 4, "names": {"pl": "Niebieski"}, "is_default": 1, "impact_on_the_price": 10.0} - ] - } - ] -} -``` - -## Polling - -Aby pobierac tylko nowe/zmienione zamowienia, uzyj parametru `updated_since`: - -``` -GET api.php?endpoint=orders&action=list&updated_since=2026-02-19 12:00:00 -``` - -Kolumna `updated_at` w `pp_shop_orders` jest aktualizowana automatycznie przy kazdej modyfikacji zamowienia (zmiana statusu, platnosci, edycja danych, tworzenie zamowienia). - -## Konfiguracja - -Klucz API ustawia sie w panelu admina w ustawieniach sklepu lub bezposrednio w bazie: - -```sql -INSERT INTO pp_settings (param, value) VALUES ('api_key', 'twoj-klucz-api'); --- lub -UPDATE pp_settings SET value = 'twoj-klucz-api' WHERE param = 'api_key'; -``` - -## Architektura - -- Entry point: `api.php` -- Router: `\api\ApiRouter` (`autoload/api/ApiRouter.php`) -- Kontrolery: `autoload/api/Controllers/` - - `OrdersApiController` — zamowienia (5 akcji) - - `ProductsApiController` — produkty (8 akcji: list, get, create, update, variants, create_variant, update_variant, delete_variant) - - `DictionariesApiController` — slowniki (5 akcji: statuses, transports, payment_methods, attributes, ensure_producer) - - `CategoriesApiController` — kategorie (1 akcja: list) diff --git a/docs/TODO.md b/docs/TODO.md index c75bb12..9b54b1a 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -4,3 +4,4 @@ naprawić działanie newslettera i zapis do bazy newslettera program lojalnościowy proponowane produkty w koszyku Do zamówień w statusie: realizowane lub oczekuje na wpłatę. Opcja tylko dla zarejestrowanych klientów. https://royal-stone.pl/pl/order1.html +Dodać możliwość ustawienia limitu znaków w wiadomościach do produktu \ No newline at end of file diff --git a/docs/plans/2026-02-27-htaccess-conf-elimination.md b/docs/plans/2026-02-27-htaccess-conf-elimination.md deleted file mode 100644 index 2024753..0000000 --- a/docs/plans/2026-02-27-htaccess-conf-elimination.md +++ /dev/null @@ -1,658 +0,0 @@ -# htaccess.conf Elimination — Implementation Plan - -> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. - -**Goal:** Eliminate `libraries/htaccess.conf` as a template file and move all remaining hardcoded URL routes into `pp_routes`, leaving only true Apache-level directives in the generated `.htaccess`. - -**Architecture:** `Helpers::htacces()` generates the full `.htaccess` content from PHP strings instead of loading a template. All URL→PHP mappings (static system routes + dynamic per language/producer) are inserted into `pp_routes` with `type='system'`, deleted and reinserted on every `htacces()` call. Apache-level rules (HTTPS redirect, admin routing, thumb.php) stay in `.htaccess` only. - -**Tech Stack:** PHP 7.4, Medoo ORM (`$mdb`), Redis (CacheHandler), PHPUnit 9.6 - ---- - -## Context - -### Current `Helpers::htacces()` structure (before this plan) -1. Loads `libraries/htaccess.conf` template (contains many hardcoded URL routes) -2. Appends language switch rules to `$htaccess_data` -3. Appends newsletter and producer rules to `$htaccess_data` -4. Inserts category/product/page/article routes into `pp_routes` (done in v0.329) -5. Replaces `{HTACCESS_CACHE}` placeholder -6. Appends catch-all, writes files - -### What stays in `.htaccess` after this plan -- `RewriteEngine On`, `RewriteBase /`, `Options` -- www→https redirect -- http→https redirect (with tpay/przelewy24/hotpay exclusion) -- Trailing slash removal (excluding `/admin/`) -- Admin routing: `^admin/([^/]*)/([^/]*)/(.*)$` -- `^admin/$` -- Thumbnail: `^thumb/([0-9]*)/([0-9]*)/(.*)$` → `/libraries/thumb.php` (different PHP file, cannot use pp_routes) -- `THE_REQUEST` index.php redirect -- Cache headers block (gzip/expires or no-cache based on `$settings['htaccess_cache']`) -- File protection: ``, ``, `` -- Start page 301 redirects (generated dynamically in pages loop) -- Custom htaccess from `pp_settings` (param=htaccess) -- Catch-all: `RewriteRule ^ index.php [L]` - -### New `type` column in `pp_routes` -- `NULL` = entity route (product/category/page/article) -- `'system'` = system route (all routes in this plan) -- On every `htacces()` call: `DELETE WHERE type='system'`, then reinsert all - ---- - -## Task 1: Update SQL migration — add `type` column - -**Files:** -- Modify: `migrations/0.329.sql` - -**Step 1: Add `type` column to the migration** - -Open `migrations/0.329.sql` (currently has 4 lines). Append the `type` column: - -```sql -ALTER TABLE pp_routes - ADD COLUMN category_id INT NULL AFTER product_id, - ADD COLUMN page_id INT NULL AFTER category_id, - ADD COLUMN article_id INT NULL AFTER page_id, - ADD COLUMN type VARCHAR(20) NULL AFTER article_id; -``` - -**Step 2: Apply migration on server** - -Run on the production/staging database: -```sql -ALTER TABLE pp_routes ADD COLUMN type VARCHAR(20) NULL AFTER article_id; -``` -(The other 3 columns from 0.329 should already be applied from the previous deployment.) - -**Step 3: No test needed** — pure schema change, verified when routes are inserted in Task 2. - ---- - -## Task 2: Refactor `Helpers::htacces()` — replace template + move all routes to pp_routes - -**Files:** -- Modify: `autoload/Shared/Helpers/Helpers.php` (method `htacces()`, lines ~408–773) - -This is the core task. The entire method is refactored. Here is the complete new body: - -**Step 1: Replace the method body** - -Find the opening of `htacces()` at line ~408. Replace everything from the start of the method body through the end (line ~773) with the code below. - -The key structural changes: -- Remove `file_get_contents(htaccess.conf)` and `str_replace('{PAGE}', ...)` -- Remove `str_replace('{HTACCESS_CACHE}', ...)` — cache block is now inline -- Build `$htaccess_data` directly as PHP string -- Delete all `type='system'` routes, then reinsert static + dynamic ones -- Language switch → `pp_routes` (remove from `$htaccess_data`) -- Newsletter → `pp_routes` (remove from `$htaccess_data`) -- Producenci/producent → `pp_routes` (remove from `$htaccess_data`) - -**New `htacces()` method body** — replace lines 409–773 with: - -```php - { - global $mdb; - - $settings = ( new \Domain\Settings\SettingsRepository( $mdb ) )->allSettings( true ); - - $url = preg_replace( '#^(http(s)?://)?w{3}\.#', '$1', $_SERVER['SERVER_NAME'] ); - - $robots = 'User-agent: *' . PHP_EOL; - $robots .= 'Allow: /' . PHP_EOL; - - $site_map = '' . PHP_EOL; - $site_map .= '' . PHP_EOL; - $site_map .= '' . PHP_EOL; - $site_map .= 'https://' . $url . '' . PHP_EOL; - $site_map .= '' . date( 'Y-m-d' ) . '' . PHP_EOL; - $site_map .= 'daily' . PHP_EOL; - $site_map .= '1' . PHP_EOL; - $site_map .= '' . PHP_EOL; - - // - // SYSTEM ROUTES — delete all and reinsert - // - $mdb->delete( 'pp_routes', [ 'type' => 'system' ] ); - - // Static system routes (hardcoded, never change) - $systemRoutes = [ - // Wyszukiwarka - [ 'pattern' => '^wyszukiwarka/([^/]+)/([0-9]+)$', 'destination' => 'index.php?module=search&action=search_results&query=$1&bs=$2' ], - [ 'pattern' => '^wyszukiwarka/([^/]+)$', 'destination' => 'index.php?module=search&action=search_results&query=$1&bs=1' ], - // Zamowienia - [ 'pattern' => '^zamowienie/([a-zA-Z0-9-]+)$', 'destination' => 'index.php?module=shop_order&action=order_details&order_hash=$1' ], - [ 'pattern' => '^potwierdzenie-platnosci/([a-zA-Z0-9-]+)$', 'destination' => 'index.php?module=shop_order&action=payment_confirmation&order_hash=$1' ], - // Platnosci - [ 'pattern' => '^tpay-status$', 'destination' => 'index.php?module=shop_order&action=payment_status_tpay' ], - [ 'pattern' => '^platnosc-status$', 'destination' => 'index.php?module=shop_order&action=payment_status_hotpay' ], - [ 'pattern' => '^przelewy24-status$', 'destination' => 'index.php?module=shop_order&action=payment_status_przelewy24pl' ], - // Koszyk - [ 'pattern' => '^koszyk$', 'destination' => 'index.php?module=shop_basket&action=main_view' ], - [ 'pattern' => '^koszyk-podsumowanie$', 'destination' => 'index.php?module=shop_basket&action=summary_view' ], - [ 'pattern' => '^zloz-zamowienie$', 'destination' => 'index.php?module=shop_basket&action=basket_save' ], - // Klient - [ 'pattern' => '^rejestracja$', 'destination' => 'index.php?module=shop_client&action=register_form' ], - [ 'pattern' => '^logowanie$', 'destination' => 'index.php?module=shop_client&action=login_form' ], - [ 'pattern' => '^wylogowanie$', 'destination' => 'index.php?module=shop_client&action=logout' ], - [ 'pattern' => '^odzyskiwanie-hasla$', 'destination' => 'index.php?module=shop_client&action=recover_password' ], - [ 'pattern' => '^panel-klienta/zamowienia$', 'destination' => 'index.php?module=shop_client&action=client_orders' ], - [ 'pattern' => '^panel-klienta/adresy$', 'destination' => 'index.php?module=shop_client&action=client_addresses' ], - [ 'pattern' => '^panel-klienta/nowy-adres$', 'destination' => 'index.php?module=shop_client&action=address_edit' ], - [ 'pattern' => '^panel-klienta/edytuj-adres/([0-9]+)$', 'destination' => 'index.php?module=shop_client&action=address_edit&id=$1' ], - [ 'pattern' => '^panel-klienta/usun-adres/([0-9]+)$', 'destination' => 'index.php?module=shop_client&action=address_delete&id=$1' ], - // Newsletter - [ 'pattern' => '^newsletter/signin$', 'destination' => 'index.php?module=newsletter&action=signin' ], - [ 'pattern' => '^newsletter/confirm/hash=(.+)$', 'destination' => 'index.php?module=newsletter&action=confirm&hash=$1' ], - [ 'pattern' => '^newsletter/unsubscribe/hash=(.+)$', 'destination' => 'index.php?module=newsletter&action=unsubscribe&hash=$1' ], - // Moduły AJAX (shopBasket, shopClient, shopProduct, shopCoupon, search) - [ 'pattern' => '^shopBasket/([^/]+)/(.+)$', 'destination' => 'index.php?module=shopBasket&action=$1&$2' ], - [ 'pattern' => '^shopBasket/([^/]+)$', 'destination' => 'index.php?module=shopBasket&action=$1' ], - [ 'pattern' => '^shopClient/([^/]+)/(.+)$', 'destination' => 'index.php?module=shopClient&action=$1&$2' ], - [ 'pattern' => '^shopClient/([^/]+)$', 'destination' => 'index.php?module=shopClient&action=$1' ], - [ 'pattern' => '^shopProduct/([^/]+)/(.+)$', 'destination' => 'index.php?module=shopProduct&action=$1&$2' ], - [ 'pattern' => '^shopProduct/([^/]+)$', 'destination' => 'index.php?module=shopProduct&action=$1' ], - [ 'pattern' => '^shopCoupon/([^/]+)/(.+)$', 'destination' => 'index.php?module=shopCoupon&action=$1&$2' ], - [ 'pattern' => '^shopCoupon/([^/]+)$', 'destination' => 'index.php?module=shopCoupon&action=$1' ], - [ 'pattern' => '^search/([^/]+)/(.+)$', 'destination' => 'index.php?module=search&action=$1&$2' ], - [ 'pattern' => '^search/([^/]+)$', 'destination' => 'index.php?module=search&action=$1' ], - ]; - - foreach ( $systemRoutes as $route ) - { - $mdb->insert( 'pp_routes', [ - 'type' => 'system', - 'lang_id' => 0, - 'pattern' => $route['pattern'], - 'destination' => $route['destination'], - ] ); - } - - // Dynamic system routes — languages - $results = $mdb->select( 'pp_langs', [ 'id' ], [ 'status' => 1, 'ORDER' => [ 'o' => 'ASC' ] ] ); - if ( is_array( $results ) ) foreach ( $results as $row ) - { - $mdb->insert( 'pp_routes', [ - 'type' => 'system', - 'lang_id' => 0, - 'pattern' => '^' . $row['id'] . '$', - 'destination' => 'index.php?a=change_language&id=' . $row['id'], - ] ); - } - - // Dynamic system routes — producenci - $categoryDefaultLayoutId = ( new \Domain\Layouts\LayoutsRepository( $mdb ) )->categoryDefaultLayoutId(); - - $mdb->insert( 'pp_routes', [ - 'type' => 'system', - 'lang_id' => 0, - 'pattern' => '^producenci$', - 'destination' => 'index.php?module=shop_producer&action=list&layout_id=' . $categoryDefaultLayoutId, - ] ); - - $rows = $mdb->select( 'pp_shop_producer', '*', [ 'status' => 1 ] ); - if ( self::is_array_fix( $rows ) ) foreach ( $rows as $row ) - { - $mdb->insert( 'pp_routes', [ - 'type' => 'system', - 'lang_id' => 0, - 'pattern' => '^producent/' . self::seo( $row['name'] ) . '$', - 'destination' => 'index.php?module=shop_producer&action=products&producer_id=' . $row['id'] . '&layout_id=' . $categoryDefaultLayoutId, - ] ); - $mdb->insert( 'pp_routes', [ - 'type' => 'system', - 'lang_id' => 0, - 'pattern' => '^producent/' . self::seo( $row['name'] ) . '/([0-9]+)$', - 'destination' => 'index.php?module=shop_producer&action=products&producer_id=' . $row['id'] . '&layout_id=' . $categoryDefaultLayoutId . '&bs=$1', - ] ); - } - - // - // HTACCESS — generuj z PHP (bez szablonu htaccess.conf) - // - $htaccess_data = 'RewriteEngine On' . PHP_EOL; - $htaccess_data .= 'RewriteBase /' . PHP_EOL; - $htaccess_data .= 'Options +FollowSymlinks' . PHP_EOL; - $htaccess_data .= 'Options -Indexes' . PHP_EOL; - $htaccess_data .= PHP_EOL; - $htaccess_data .= '# Przekierowanie z www na bez www i z http na https w jednym kroku' . PHP_EOL; - $htaccess_data .= 'RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC]' . PHP_EOL; - $htaccess_data .= 'RewriteRule ^ https://%1%{REQUEST_URI} [R=301,L]' . PHP_EOL; - $htaccess_data .= PHP_EOL; - $htaccess_data .= '# Przekierowanie z http na https, jesli nie zawiera www' . PHP_EOL; - $htaccess_data .= 'RewriteCond %{HTTPS} off' . PHP_EOL; - $htaccess_data .= 'RewriteCond %{REQUEST_URI} !^/(tpay-status|platnosc-status|przelewy24-status)$ [NC]' . PHP_EOL; - $htaccess_data .= 'RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]' . PHP_EOL; - $htaccess_data .= PHP_EOL; - $htaccess_data .= '# Usuwanie koncowego slasha dla niekatalogów' . PHP_EOL; - $htaccess_data .= 'RewriteCond %{REQUEST_FILENAME} !-d' . PHP_EOL; - $htaccess_data .= 'RewriteCond %{REQUEST_URI} !^/admin/.*$ [NC]' . PHP_EOL; - $htaccess_data .= 'RewriteCond %{REQUEST_URI} (.+)/$' . PHP_EOL; - $htaccess_data .= 'RewriteRule ^ %1 [R=301,L]' . PHP_EOL; - $htaccess_data .= PHP_EOL; - $htaccess_data .= 'RewriteCond %{REQUEST_URI} !^(.*)/libraries/(.*) [NC]' . PHP_EOL; - $htaccess_data .= 'RewriteCond %{REQUEST_URI} !^(.*)/layout/(.*) [NC]' . PHP_EOL; - $htaccess_data .= 'RewriteRule ^admin/([^/]*)/([^/]*)/(.*)$ admin/index.php?module=$1&action=$2&$3 [L]' . PHP_EOL; - $htaccess_data .= PHP_EOL; - $htaccess_data .= 'RewriteRule ^admin/$ admin/index.php [L]' . PHP_EOL; - $htaccess_data .= PHP_EOL; - $htaccess_data .= 'RewriteRule ^thumb/([0-9]*)/([0-9]*)/(.*)$ /libraries/thumb.php?img=$3&w=$1&h=$2 [L]' . PHP_EOL; - $htaccess_data .= PHP_EOL; - $htaccess_data .= 'RewriteCond %{THE_REQUEST} ^[A-Z]{3,9}\ /index.php' . PHP_EOL; - $htaccess_data .= 'RewriteRule ^ /%1 [R=301,L]' . PHP_EOL; - - /* cache — zastąpienie placeholdera {HTACCESS_CACHE} */ - if ( $settings['htaccess_cache'] ) - { - $htaccess_data .= '' . PHP_EOL - . 'AddOutputFilterByType DEFLATE text/html text/plain text/xml application/xml application/xhtml+xml text/css text/javascript application/javascript application/x-javascript' . PHP_EOL - . '' . PHP_EOL - . '' . PHP_EOL - . 'Header set Access-Control-Allow-Origin "*"' . PHP_EOL - . '' . PHP_EOL - . '' . PHP_EOL - . 'ExpiresActive on' . PHP_EOL - . 'ExpiresDefault "access plus 1 month"' . PHP_EOL - . 'ExpiresByType text/css "access plus 1 year"' . PHP_EOL - . 'ExpiresByType application/json "access plus 0 seconds"' . PHP_EOL - . 'ExpiresByType application/xml "access plus 0 seconds"' . PHP_EOL - . 'ExpiresByType text/xml "access plus 0 seconds"' . PHP_EOL - . 'ExpiresByType image/x-icon "access plus 1 week"' . PHP_EOL - . 'ExpiresByType text/x-component "access plus 1 month"' . PHP_EOL - . 'ExpiresByType text/html "access plus 0 seconds"' . PHP_EOL - . 'ExpiresByType application/javascript "access plus 1 year"' . PHP_EOL - . 'ExpiresByType application/x-web-app-manifest+json "access plus 0 seconds"' . PHP_EOL - . 'ExpiresByType text/cache-manifest "access plus 0 seconds"' . PHP_EOL - . 'ExpiresByType audio/ogg "access plus 1 month"' . PHP_EOL - . 'ExpiresByType image/gif "access plus 1 month"' . PHP_EOL - . 'ExpiresByType image/jpeg "access plus 1 month"' . PHP_EOL - . 'ExpiresByType image/png "access plus 1 month"' . PHP_EOL - . 'ExpiresByType video/mp4 "access plus 1 month"' . PHP_EOL - . 'ExpiresByType video/ogg "access plus 1 month"' . PHP_EOL - . 'ExpiresByType video/webm "access plus 1 month"' . PHP_EOL - . 'ExpiresByType application/atom+xml "access plus 1 hour"' . PHP_EOL - . 'ExpiresByType application/rss+xml "access plus 1 hour"' . PHP_EOL - . 'ExpiresByType application/font-woff "access plus 1 month"' . PHP_EOL - . 'ExpiresByType application/vnd.ms-fontobject "access plus 1 month"' . PHP_EOL - . 'ExpiresByType application/x-font-ttf "access plus 1 month"' . PHP_EOL - . 'ExpiresByType font/opentype "access plus 1 month"' . PHP_EOL - . 'ExpiresByType image/svg+xml "access plus 1 month"' . PHP_EOL - . '' . PHP_EOL; - } - else - { - $htaccess_data .= '' . PHP_EOL - . 'Header set Cache-Control "no-cache, no-store, must-revalidate"' . PHP_EOL - . 'Header set Pragma "no-cache"' . PHP_EOL - . 'Header set Expires 0' . PHP_EOL - . '' . PHP_EOL; - } - - $htaccess_data .= '' . PHP_EOL; - $htaccess_data .= ' Order Deny,Allow' . PHP_EOL; - $htaccess_data .= ' Deny from all' . PHP_EOL; - $htaccess_data .= '' . PHP_EOL; - $htaccess_data .= '' . PHP_EOL; - $htaccess_data .= ' Order Deny,Allow' . PHP_EOL; - $htaccess_data .= ' Deny from all' . PHP_EOL; - $htaccess_data .= '' . PHP_EOL; - $htaccess_data .= '' . PHP_EOL; - $htaccess_data .= ' Order Deny,Allow' . PHP_EOL; - $htaccess_data .= ' Deny from all' . PHP_EOL; - $htaccess_data .= '' . PHP_EOL; - - // - // KATEGORIE — sitemap + pp_routes (bez zmian) - // - $results = $mdb->select( 'pp_langs', [ 'id', 'start' ], [ 'status' => 1, 'ORDER' => [ 'o' => 'ASC' ] ] ); - if ( is_array( $results ) ) foreach ( $results as $row ) - { - !$row['start'] ? $language_link = $row['id'] . '/' : $language_link = ''; - - $results2 = $mdb->select( 'pp_shop_categories_langs', [ '[><]pp_shop_categories' => [ 'category_id' => 'id' ] ], [ 'seo_link', 'title', 'category_id' ], [ 'lang_id' => $row['id'], 'ORDER' => [ 'o' => 'ASC' ] ] ); - if ( is_array( $results2 ) ) foreach ( $results2 as $row2 ) - { - if ( $row2['title'] ) - { - $site_map .= '' . PHP_EOL; - if ( $row2['seo_link'] ) - $site_map .= 'https://' . $url . '/' . $language_link . self::seo( $row2['seo_link'] ) . '' . PHP_EOL; - else - $site_map .= 'https://' . $url . '/' . $language_link . 'k-' . $row2['category_id'] . '-' . self::seo( $row2['title'] ) . '' . PHP_EOL; - $site_map .= '' . date( 'Y-m-d' ) . '' . PHP_EOL; - $site_map .= 'daily' . PHP_EOL; - $site_map .= '1' . PHP_EOL; - $site_map .= '' . PHP_EOL; - - $seoSlug = $row2['seo_link'] ? self::seo( $row2['seo_link'] ) : 'k-' . $row2['category_id'] . '-' . self::seo( $row2['title'] ); - - $mdb->delete( 'pp_routes', [ 'AND' => [ 'category_id' => $row2['category_id'], 'lang_id' => $row['id'] ] ] ); - - $mdb->insert( 'pp_routes', [ - 'category_id' => $row2['category_id'], - 'lang_id' => $row['id'], - 'pattern' => '^' . $language_link . $seoSlug . '$', - 'destination' => 'index.php?category=' . $row2['category_id'] . '&lang=' . $row['id'], - ] ); - $mdb->insert( 'pp_routes', [ - 'category_id' => $row2['category_id'], - 'lang_id' => $row['id'], - 'pattern' => '^' . $language_link . $seoSlug . '/([0-9]+)$', - 'destination' => 'index.php?category=' . $row2['category_id'] . '&lang=' . $row['id'] . '&bs=$1', - ] ); - } - } - } - - // - // PRODUKTY — sitemap + pp_routes (bez zmian) - // - $results = $mdb->select( 'pp_langs', [ 'id', 'start' ], [ 'status' => 1, 'ORDER' => [ 'o' => 'ASC' ] ] ); - if ( is_array( $results ) ) - { - foreach ( $results as $row ) - { - !$row['start'] ? $language_link = $row['id'] . '/' : $language_link = ''; - - $results2 = $mdb->select( 'pp_shop_products_langs', [ '[><]pp_shop_products' => [ 'product_id' => 'id' ] ], [ 'seo_link', 'name', 'product_id' ], [ 'lang_id' => $row['id'], 'ORDER' => [ 'name' => 'ASC' ] ] ); - if ( is_array( $results2 ) ) - { - foreach ( $results2 as $row2 ) - { - $mdb->delete( 'pp_routes', [ 'AND' => [ 'product_id' => $row2['product_id'], 'lang_id' => $row['id'] ] ] ); - - if ( $row2['name'] ) - { - $site_map .= '' . PHP_EOL; - if ( $row2['seo_link'] ) - $site_map .= 'https://' . $url . '/' . $language_link . self::seo( $row2['seo_link'] ) . '' . PHP_EOL; - else - $site_map .= 'https://' . $url . '/' . $language_link . 'p-' . $row2['product_id'] . '-' . self::seo( $row2['name'] ) . '' . PHP_EOL; - $site_map .= '' . date( 'Y-m-d' ) . '' . PHP_EOL; - $site_map .= 'daily' . PHP_EOL; - $site_map .= '1' . PHP_EOL; - $site_map .= '' . PHP_EOL; - - if ( $row2['seo_link'] ) - { - $mdb->insert( 'pp_routes', [ 'product_id' => $row2['product_id'], 'lang_id' => $row['id'], 'pattern' => '^' . $language_link . self::seo( $row2['seo_link'] ) . '$', 'destination' => 'index.php?product=' . $row2['product_id'] ] ); - $mdb->insert( 'pp_routes', [ 'product_id' => $row2['product_id'], 'lang_id' => $row['id'], 'pattern' => '^' . $language_link . self::seo( $row2['seo_link'] ) . '/([0-9-]+)$', 'destination' => 'index.php?product=' . $row2['product_id'] . '&permutation_hash=$1' ] ); - } - else - { - $mdb->insert( 'pp_routes', [ 'product_id' => $row2['product_id'], 'lang_id' => $row['id'], 'pattern' => '^' . $language_link . 'p-' . $row2['product_id'] . '-' . self::seo( $row2['name'] ) . '$', 'destination' => 'index.php?product=' . $row2['product_id'] ] ); - $mdb->insert( 'pp_routes', [ 'product_id' => $row2['product_id'], 'lang_id' => $row['id'], 'pattern' => '^' . $language_link . 'p-' . $row2['product_id'] . '-' . self::seo( $row2['name'] ) . '/([0-9-]+)$', 'destination' => 'index.php?product=' . $row2['product_id'] . '&permutation_hash=$1' ] ); - } - } - } - } - } - } - - // - // STRONY + ARTYKULY — sitemap + pp_routes (bez zmian) - // - $results = $mdb->select( 'pp_langs', [ 'id', 'start' ], [ 'status' => 1, 'ORDER' => [ 'o' => 'ASC' ] ] ); - if ( is_array( $results ) ) - foreach ( $results as $row ) - { - ( !$row['start'] and count( $results ) > 1 ) ? $language_link = $row['id'] . '/' : $language_link = ''; - - $results2 = $mdb->select( 'pp_pages_langs', [ '[><]pp_pages' => [ 'page_id' => 'id' ] ], [ 'seo_link', 'title', 'page_id', 'noindex', 'start', 'link', 'page_type' ], [ 'lang_id' => $row['id'], 'ORDER' => [ 'start' => 'DESC', 'o' => 'ASC' ] ] ); - if ( is_array( $results2 ) ) - foreach ( $results2 as $row2 ) - { - if ( $row2['title'] and $row2['page_type'] != 3 and $row2['page_type'] != 5 ) - { - if ( !$row2['noindex'] ) - { - $site_map .= '' . PHP_EOL; - if ( $row2['seo_link'] ) - $site_map .= 'https://' . $url . '/' . self::seo( $row2['seo_link'] ) . '' . PHP_EOL; - else - $site_map .= 'https://' . $url . '/s-' . $row2['page_id'] . '-' . self::seo( $row2['title'] ) . '' . PHP_EOL; - $site_map .= '' . date( 'Y-m-d' ) . '' . PHP_EOL; - $site_map .= 'daily' . PHP_EOL; - $site_map .= '1' . PHP_EOL; - $site_map .= '' . PHP_EOL; - } - else if ( $row2['noindex'] and $row2['seo_link'] ) - { - $robots .= 'User-agent: GoogleBot' . PHP_EOL; - $robots .= 'Disallow: /' . $row2['seo_link'] . PHP_EOL; - } - - if ( $row2['start'] ) - { - if ( $row2['seo_link'] ) - { - $htaccess_data .= PHP_EOL . 'RewriteCond %{REQUEST_URI} ^/' . self::seo( $row2['seo_link'] ) . '$'; - $htaccess_data .= PHP_EOL . 'RewriteRule ^(.*)$ http://www.' . $url . '/' . $language_link . ' [R=permanent,L]'; - $htaccess_data .= PHP_EOL . 'RewriteCond %{REQUEST_URI} ^/' . self::seo( $row2['seo_link'] ) . '-1$'; - $htaccess_data .= PHP_EOL . 'RewriteRule ^(.*)$ http://www.' . $url . '/' . $language_link . ' [R=permanent,L]'; - } - else - { - $htaccess_data .= PHP_EOL . 'RewriteCond %{REQUEST_URI} ^/s-' . $row2['page_id'] . '-' . self::seo( $row2['title'] ) . '$'; - $htaccess_data .= PHP_EOL . 'RewriteRule ^(.*)$ http://www.' . $url . '/' . $language_link . ' [R=permanent,L]'; - $htaccess_data .= PHP_EOL . 'RewriteCond %{REQUEST_URI} ^/s-' . $row2['page_id'] . '-' . self::seo( $row2['title'] ) . '-1$'; - $htaccess_data .= PHP_EOL . 'RewriteRule ^(.*)$ http://www.' . $url . '/' . $language_link . ' [R=permanent,L]'; - } - $htaccess_data .= PHP_EOL . 'RewriteRule ^$ index.php?a=page&id=' . $row2['page_id'] . '&lang=' . $row['id'] . ' [L]'; - } - - $seoSlug = $row2['seo_link'] ? self::seo( $row2['seo_link'] ) : 's-' . $row2['page_id'] . '-' . self::seo( $row2['title'] ); - $langPrefix = $row2['start'] ? '' : $language_link; - - $mdb->delete( 'pp_routes', [ 'AND' => [ 'page_id' => $row2['page_id'], 'lang_id' => $row['id'] ] ] ); - - $mdb->insert( 'pp_routes', [ - 'page_id' => $row2['page_id'], - 'lang_id' => $row['id'], - 'pattern' => '^' . $langPrefix . $seoSlug . '$', - 'destination' => 'index.php?a=page&id=' . $row2['page_id'] . '&lang=' . $row['id'], - ] ); - $mdb->insert( 'pp_routes', [ - 'page_id' => $row2['page_id'], - 'lang_id' => $row['id'], - 'pattern' => '^' . $langPrefix . $seoSlug . '/([0-9]+)$', - 'destination' => 'index.php?a=page&id=' . $row2['page_id'] . '&lang=' . $row['id'] . '&bs=$1', - ] ); - } - } - - $results2 = $mdb->select( 'pp_articles_langs', [ '[><]pp_articles' => [ 'article_id' => 'id' ] ], [ 'seo_link', 'title', 'article_id', 'noindex', 'copy_from' ], [ 'AND' => [ 'status' => 1, 'lang_id' => $row['id'], 'block_direct_access' => 0 ] ] ); - if ( is_array( $results2 ) ) - foreach ( $results2 as $row2 ) - { - if ( $row2['copy_from'] != null ) - { - $results_tmp = $mdb->get( 'pp_articles_langs', [ 'seo_link', 'title' ], [ 'AND' => [ 'article_id' => $row2['article_id'], 'lang_id' => $row2['copy_from'] ] ] ); - $row2['seo_link'] = $results_tmp['seo_link']; - $row2['title'] = $results_tmp['title']; - } - - if ( !$row2['noindex'] ) - { - $site_map .= '' . PHP_EOL; - if ( $row2['seo_link'] ) - $site_map .= 'https://' . $url . '/' . self::seo( $row2['seo_link'] ) . '' . PHP_EOL; - else - $site_map .= 'https://' . $url . '/a-' . $row2['article_id'] . '-' . self::seo( $row2['title'] ) . '' . PHP_EOL; - $site_map .= '' . date( 'Y-m-d' ) . '' . PHP_EOL; - $site_map .= 'daily' . PHP_EOL; - $site_map .= '1' . PHP_EOL; - $site_map .= '' . PHP_EOL; - } - else if ( $row2['noindex'] and $row2['seo_link'] ) - { - $robots .= 'User-agent: GoogleBot' . PHP_EOL; - $robots .= 'Disallow: /' . $row2['seo_link'] . PHP_EOL; - } - - $mdb->delete( 'pp_routes', [ 'AND' => [ 'article_id' => $row2['article_id'], 'lang_id' => $row['id'] ] ] ); - - if ( $row2['seo_link'] ) - { - $mdb->insert( 'pp_routes', [ - 'article_id' => $row2['article_id'], - 'lang_id' => $row['id'], - 'pattern' => '^' . $language_link . self::seo( $row2['seo_link'] ) . '$', - 'destination' => 'index.php?article=' . $row2['article_id'] . '&lang=' . $row['id'], - ] ); - } - else if ( $row2['title'] != null ) - { - $mdb->insert( 'pp_routes', [ - 'article_id' => $row2['article_id'], - 'lang_id' => $row['id'], - 'pattern' => '^' . $language_link . 'a-' . $row2['article_id'] . '-' . self::seo( $row2['title'] ) . '$', - 'destination' => 'index.php?article=' . $row2['article_id'] . '&lang=' . $row['id'], - ] ); - } - } - } - - // Invalidacja cache tras - try { - ( new \Shared\Cache\CacheHandler() )->delete( 'pp_routes:all' ); - } catch ( \Exception $e ) { - // Redis niedostepny — ignorujemy - } - - $results = $mdb->get( 'pp_settings', 'value', [ 'param' => 'htaccess' ] ); - if ( $results ) - $htaccess_data .= PHP_EOL . $results; - - $results = $mdb->get( 'pp_settings', 'value', [ 'param' => 'robots' ] ); - if ( $results ) - $robots .= PHP_EOL . $results; - - $site_map .= ''; - - $htaccess_data .= PHP_EOL; - $htaccess_data .= 'RewriteCond %{REQUEST_FILENAME} !-f' . PHP_EOL; - $htaccess_data .= 'RewriteCond %{REQUEST_FILENAME} !-d' . PHP_EOL; - $htaccess_data .= 'RewriteRule ^ index.php [L]'; - - // Niektore hostingi blokuja zmiane wersji PHP przez .htaccess. - $htaccess_data = preg_replace( '/^(\\s*)(AddHandler|SetHandler|ForceType)\\b/im', '$1# $2', $htaccess_data ); - - $fp = fopen( $dir . '.htaccess', 'w' ); - fwrite( $fp, $htaccess_data ); - fclose( $fp ); - - $fp = fopen( $dir . 'sitemap.xml', 'w' ); - fwrite( $fp, $site_map ); - fclose( $fp ); - - $fp = fopen( $dir . 'robots.txt', 'w' ); - fwrite( $fp, $robots ); - fclose( $fp ); - } -``` - -**Step 2: Run tests** -``` -php phpunit.phar --configuration phpunit.xml -``` -Expected: all tests pass (htacces() has no unit tests, covered by integration). - ---- - -## Task 3: Delete `libraries/htaccess.conf` - -**Files:** -- Delete: `libraries/htaccess.conf` - -**Step 1: Verify htacces() no longer references the file** - -Search for any remaining `file_get_contents` referencing `htaccess.conf`: -```bash -grep -r "htaccess.conf" autoload/ -``` -Expected: no results. - -**Step 2: Delete the file** -```bash -rm libraries/htaccess.conf -``` - -**Step 3: Run tests** -``` -php phpunit.phar --configuration phpunit.xml -``` -Expected: all tests still pass. - ---- - -## Task 4: Update `docs/DATABASE_STRUCTURE.md` - -**Files:** -- Modify: `docs/DATABASE_STRUCTURE.md` (section `## pp_routes`) - -**Step 1: Add `type` column to the pp_routes table description** - -Find the `## pp_routes` section and add the `type` row to the column table: - -```markdown -| type | Typ trasy: NULL = encja (produkt/kategoria/strona/artykuł), 'system' = trasa systemowa | -``` - -Also update the description paragraph to mention that system routes are managed automatically. - ---- - -## Task 5: Manual integration test on server - -**Step 1: Apply migration** -```sql -ALTER TABLE pp_routes ADD COLUMN type VARCHAR(20) NULL AFTER article_id; -``` - -**Step 2: Trigger `htacces()` regeneration** - -Log in to admin panel → save any product or category → this calls `htacces()`. - -**Step 3: Verify pp_routes has system routes** -```sql -SELECT COUNT(*) FROM pp_routes WHERE type = 'system'; -``` -Expected: ~35+ rows (32 static + language rows + producer rows). - -**Step 4: Verify .htaccess was generated correctly** - -Open `.htaccess` — should NOT contain `RewriteRule ^koszyk$`, `^logowanie$`, etc. Should contain HTTPS redirect, admin routing, thumb routing, cache block. - -**Step 5: Test URLs in browser** -- `/koszyk` → koszyk page ✓ -- `/logowanie` → login page ✓ -- `/wyszukiwarka/test` → search results ✓ -- `/zamowienie/abc123` → order details ✓ -- `/shopClient/confirm/hash=xyz` → client confirm action ✓ -- Category URL → category page ✓ -- Product URL → product page ✓ - -**Step 6: Run full test suite** -``` -php phpunit.phar --configuration phpunit.xml -``` -Expected: 807 tests, all pass. - ---- - -## Task 6: Commit - -**Step 1: Stage and commit** -```bash -git add migrations/0.329.sql -git add autoload/Shared/Helpers/Helpers.php -git add docs/DATABASE_STRUCTURE.md -git add docs/plans/2026-02-27-htaccess-conf-elimination.md -git add docs/plans/2026-02-27-htaccess-to-routes-design.md -git rm libraries/htaccess.conf -git commit -m "feat: eliminate htaccess.conf, move all routes to pp_routes (v0.330)" -``` diff --git a/docs/plans/2026-02-27-htaccess-to-routes-design.md b/docs/plans/2026-02-27-htaccess-to-routes-design.md deleted file mode 100644 index 93bc51b..0000000 --- a/docs/plans/2026-02-27-htaccess-to-routes-design.md +++ /dev/null @@ -1,121 +0,0 @@ -# Design: Eliminacja htaccess.conf i przeniesienie wszystkich tras do pp_routes - -**Data:** 2026-02-27 -**Wersja docelowa:** 0.330 - ---- - -## Cel - -Wyeliminowanie pliku `libraries/htaccess.conf` jako szablonu i przeniesienie wszystkich URL-i, które dotychczas były wpisane na sztywno w `.htaccess`, do tabeli `pp_routes`. Logika generowania `.htaccess` zostaje w całości w `Helpers::htacces()`. - ---- - -## Co zostaje w `.htaccess` (reguły Apache-level) - -Tylko dyrektywy, których PHP nie może obsłużyć: - -- `RewriteEngine On`, `Options` -- Redirect HTTPS/www -- Redirect HTTP→HTTPS (z wyłączeniem tpay-status, platnosc-status, przelewy24-status) -- Usuwanie trailing slash (z wyłączeniem `/admin/`) -- Routing `/admin/` → `admin/index.php` -- `thumb/([0-9]*)/([0-9]*)/(.*)` → `/libraries/thumb.php` (inny plik PHP — niemożliwe przez pp_routes) -- `RewriteCond %{THE_REQUEST} ^[A-Z]{3,9}\ /index.php` — redirect z index.php -- Blok cache headers (gzip, expires) — zależny od `$settings['htaccess_cache']` -- Ochrona plików: ``, ``, `` -- Przekierowania 301 stron startowych (generowane dynamicznie w pętli pages) -- Niestandardowe reguły z `pp_settings` (param=htaccess) -- Catch-all: `RewriteCond !-f`, `!-d`, `RewriteRule ^ index.php [L]` - ---- - -## Co przechodzi do `pp_routes` - -### Statyczne trasy systemowe (hardcoded, niezmienne) - -| Pattern | Destination | -|---------|-------------| -| `^wyszukiwarka/([^/]+)/([0-9]+)$` | `index.php?module=search&action=search_results&query=$1&bs=$2` | -| `^wyszukiwarka/([^/]+)$` | `index.php?module=search&action=search_results&query=$1&bs=1` | -| `^zamowienie/([a-zA-Z0-9-]+)$` | `index.php?module=shop_order&action=order_details&order_hash=$1` | -| `^potwierdzenie-platnosci/([a-zA-Z0-9-]+)$` | `index.php?module=shop_order&action=payment_confirmation&order_hash=$1` | -| `^tpay-status$` | `index.php?module=shop_order&action=payment_status_tpay` | -| `^platnosc-status$` | `index.php?module=shop_order&action=payment_status_hotpay` | -| `^przelewy24-status$` | `index.php?module=shop_order&action=payment_status_przelewy24pl` | -| `^koszyk$` | `index.php?module=shop_basket&action=main_view` | -| `^koszyk-podsumowanie$` | `index.php?module=shop_basket&action=summary_view` | -| `^zloz-zamowienie$` | `index.php?module=shop_basket&action=basket_save` | -| `^rejestracja$` | `index.php?module=shop_client&action=register_form` | -| `^logowanie$` | `index.php?module=shop_client&action=login_form` | -| `^wylogowanie$` | `index.php?module=shop_client&action=logout` | -| `^odzyskiwanie-hasla$` | `index.php?module=shop_client&action=recover_password` | -| `^panel-klienta/zamowienia$` | `index.php?module=shop_client&action=client_orders` | -| `^panel-klienta/adresy$` | `index.php?module=shop_client&action=client_addresses` | -| `^panel-klienta/nowy-adres$` | `index.php?module=shop_client&action=address_edit` | -| `^panel-klienta/edytuj-adres/([0-9]+)$` | `index.php?module=shop_client&action=address_edit&id=$1` | -| `^panel-klienta/usun-adres/([0-9]+)$` | `index.php?module=shop_client&action=address_delete&id=$1` | -| `^newsletter/signin$` | `index.php?module=newsletter&action=signin` | -| `^newsletter/confirm/hash=(.+)$` | `index.php?module=newsletter&action=confirm&hash=$1` | -| `^newsletter/unsubscribe/hash=(.+)$` | `index.php?module=newsletter&action=unsubscribe&hash=$1` | - -### Trasy modułów AJAX (shopBasket, shopClient, shopProduct, shopCoupon, search) - -Dwa wzorce na moduł — 3-segmentowy (z parametrami) i 2-segmentowy: - -| Pattern | Destination | -|---------|-------------| -| `^shopBasket/([^/]+)/(.+)$` | `index.php?module=shopBasket&action=$1&$2` | -| `^shopBasket/([^/]+)$` | `index.php?module=shopBasket&action=$1` | -| `^shopClient/([^/]+)/(.+)$` | `index.php?module=shopClient&action=$1&$2` | -| `^shopClient/([^/]+)$` | `index.php?module=shopClient&action=$1` | -| `^shopProduct/([^/]+)/(.+)$` | `index.php?module=shopProduct&action=$1&$2` | -| `^shopProduct/([^/]+)$` | `index.php?module=shopProduct&action=$1` | -| `^shopCoupon/([^/]+)/(.+)$` | `index.php?module=shopCoupon&action=$1&$2` | -| `^shopCoupon/([^/]+)$` | `index.php?module=shopCoupon&action=$1` | -| `^search/([^/]+)/(.+)$` | `index.php?module=search&action=$1&$2` | -| `^search/([^/]+)$` | `index.php?module=search&action=$1` | - -### Dynamiczne trasy systemowe (wstawiane przy każdym `htacces()`) - -- **Języki:** `^{lang_id}$` → `index.php?a=change_language&id={lang_id}` (per każdy aktywny język) -- **Producenci lista:** `^producenci$` → `index.php?module=shop_producer&action=list&layout_id={id}` -- **Producent detail:** `^producent/{slug}$` i `^producent/{slug}/([0-9]+)$` (per producent z DB) - ---- - -## Nowa kolumna `type` w `pp_routes` - -```sql -ADD COLUMN type VARCHAR(20) NULL AFTER article_id -``` - -| Wartość | Znaczenie | -|---------|-----------| -| `NULL` | Trasa encji (produkt, kategoria, strona, artykuł) | -| `'system'` | Trasa systemowa (wszystkie powyższe) | - -**Zarządzanie:** przy każdym `htacces()`: -```php -$mdb->delete('pp_routes', ['type' => 'system']); // usuń wszystkie -// ... wstaw na nowo (statyczne + dynamiczne) -``` - ---- - -## Eliminacja `htaccess.conf` - -`file_get_contents($dir . 'libraries/htaccess.conf')` zastąpione PHP stringiem z tą samą treścią (tylko Apache-level reguły). Placeholder `{HTACCESS_CACHE}` zastąpiony bezpośrednim `if ($settings['htaccess_cache']) { ... } else { ... }` wbudowanym w odpowiednim miejscu. - -Plik `libraries/htaccess.conf` zostaje usunięty. - ---- - -## Pliki do modyfikacji - -| Plik | Zmiana | -|------|--------| -| `migrations/0.329.sql` | Dodać `ADD COLUMN type VARCHAR(20) NULL` | -| `Helpers::htacces()` | Usunąć `file_get_contents`, wbudować statyczny header, dodać inserty system routes, usunąć htaccess rules dla języków/newsletter/producenci | -| `libraries/htaccess.conf` | Usunąć plik | -| `docs/DATABASE_STRUCTURE.md` | Dodać kolumnę `type` do opisu pp_routes |