diff --git a/.phpunit.result.cache b/.phpunit.result.cache index 228c629..df04dc3 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},"times":{"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetQuantityReturnsCorrectValue":0.001,"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.002,"Tests\\Unit\\Domain\\Banner\\BannerRepositoryTest::testFindReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\Banner\\BannerRepositoryTest::testDeleteReturnsTrue":0.001,"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.004,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testFindReturnsNullWhenArticleDoesNotExist":0.001,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testDeleteNonassignedFilesDeletesDbRows":0.001,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testDeleteNonassignedImagesDeletesDbRows":0.001,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveCreatesNewArticle":0,"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.001,"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.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.08,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testVerifyTwofaCodeReturnsFalseForExpiredCode":0.081,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testVerifyTwofaCodeReturnsTrueForValidCode":0.155,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testSendTwofaCodeReturnsFalseWhen2FADisabled":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testSendTwofaCodeReturnsFalseForInvalidEmail":0.001,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testUpdateByIdCallsDbUpdate":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testListForAdminReturnsItemsAndTotal":0.001,"Tests\\Unit\\admin\\Controllers\\ArticlesArchiveControllerTest::testConstructorAcceptsRepository":0.002,"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.001,"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.001,"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.001,"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}} \ 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},"times":{"Tests\\Unit\\Domain\\Product\\ProductRepositoryTest::testGetQuantityReturnsCorrectValue":0.001,"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.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.001,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveTranslationsInsertsForNewArticle":0.001,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveTranslationsUpsertsForExistingArticle":0.001,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSavePagesForNewArticle":0.001,"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.001,"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.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.077,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testVerifyTwofaCodeReturnsFalseForExpiredCode":0.076,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testVerifyTwofaCodeReturnsTrueForValidCode":0.153,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testSendTwofaCodeReturnsFalseWhen2FADisabled":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testSendTwofaCodeReturnsFalseForInvalidEmail":0.001,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testUpdateByIdCallsDbUpdate":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testListForAdminReturnsItemsAndTotal":0,"Tests\\Unit\\admin\\Controllers\\ArticlesArchiveControllerTest::testConstructorAcceptsRepository":0.002,"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.003,"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.001,"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.001,"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.001,"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.001,"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}} \ No newline at end of file diff --git a/admin/templates/html/input-switch.php b/admin/templates/html/input-switch.php index 0d9aca4..cb3afc7 100644 --- a/admin/templates/html/input-switch.php +++ b/admin/templates/html/input-switch.php @@ -15,7 +15,7 @@ $out .= 'id="' . $this -> params['id'] . '" '; else $out .= 'id="' . $this -> params['name'] . '" '; - $out .= 'name="' . $this -> params['name'] . '" type="checkbox" value="1"'; + $out .= 'name="' . $this -> params['name'] . '" type="checkbox" value="on"'; if ( $this -> params['checked'] ) $out .= 'checked="checked" '; @@ -24,4 +24,4 @@ $out .= ''; $out .= ''; -echo $out; \ No newline at end of file +echo $out; diff --git a/apilo-bck b/apilo-bck deleted file mode 100644 index d31c03f..0000000 --- a/apilo-bck +++ /dev/null @@ -1,339 +0,0 @@ - - - - - - - - - - - - - - -const currentUrl = window.location.href; -console.log(currentUrl); -const patterns = [ - /^https:\/\/projectpro\.apilo\.com\/order\/order\/news\/?$/, - /^https:\/\/projectpro\.apilo\.com\/order\/order\/in-progress\/?$/, - /^https:\/\/projectpro\.apilo\.com\/order\/order\/to-send\/?$/, - /^https:\/\/projectpro\.apilo\.com\/order\/order\/completed\/?$/, - /^https:\/\/projectpro\.apilo\.com\/order\/order\/all\/?$/ -]; -if (patterns.some(pattern => pattern.test(currentUrl))) { - waitForTableAndSetImage(); - attachTableReloadListener(); -} - -function waitForTableAndSetImage() { - const intervalId = setInterval(() => { - let dataTables_scrollBody = document.getElementsByClassName('dataTables_scrollBody'); - if (dataTables_scrollBody.length > 0) { - let dataTables_tbody = dataTables_scrollBody[0].getElementsByTagName('tbody')[0]; - let rows = dataTables_tbody.getElementsByTagName('tr'); - - if (rows.length > 0) { - clearInterval(intervalId); - setImageToProduct(); - } - } - }, 100); -} - -function attachTableReloadListener() { - const table = document.querySelector('.dataTables_scrollBody table'); - if (table) { - const observer = new MutationObserver((mutationsList, observer) => { - for (const mutation of mutationsList) { - if (mutation.type === 'childList') { - waitForTableAndSetImage(); - break; - } - } - }); - - observer.observe(table.querySelector('tbody'), { childList: true }); - } -} - -function setImageToProduct(img = '') { - let dataTables_scrollBody = document.getElementsByClassName('dataTables_scrollBody'); - if (dataTables_scrollBody.length > 0) { - let dataTables_tbody = dataTables_scrollBody[0].getElementsByTagName('tbody')[0]; - let rows = dataTables_tbody.getElementsByTagName('tr'); - - for (let i = 0; i < rows.length; i++) { - let cells = rows[i].getElementsByTagName('td'); - - if (cells.length > 1) { - let secondCellText = cells[1].textContent.trim(); - let domain; - - if (secondCellText.includes('marianek.pl')) { - domain = 'marianek.pl'; - } else if (secondCellText.includes('pomysloweprezenty.pl')) { - domain = 'pomysloweprezenty.pl'; - } else { - continue; - } - - if (cells.length >= 5) { - let fifthCell = cells[4]; - let divsInFifthCell = fifthCell.children; - - for (let i = 0; i < divsInFifthCell.length; i++) { - let currentDiv = divsInFifthCell[i]; - let imgDiv = currentDiv.getElementsByTagName('div')[0]; - let dataDiv = currentDiv.getElementsByTagName('div')[1]; - - if (dataDiv) { - let skuText = dataDiv.innerHTML.match(/SKU:\s*([A-Za-z0-9-]+)/); - - if (skuText && skuText[1]) { - getProductData(skuText[1], domain).then(data => { - if (!data) { - console.log('Product not found:', skuText[1]); - return; - } - - console.log('Product found:', skuText[1]); - imgDiv.innerHTML = ''; - const imgElement = makeImg(data); - imgDiv.appendChild(imgElement); - - imgElement.addEventListener('mouseover', function() { - showLargeImage(data, imgElement); - }); - - imgElement.addEventListener('mouseout', function() { - hideLargeImage(); - }); - }); - } - } - } - } - } - } - } -} - -function makeImg(src = 'https://www.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png') { - const img = document.createElement('img'); - img.src = src; - img.alt = 'image'; - img.className = 'img-fluid center-block'; - img.style.maxWidth = '48px'; - img.style.maxHeight = '48px'; - - return img; -} - -function showLargeImage(src, imgElement) { - const largeImg = document.createElement('img'); - largeImg.src = src; - largeImg.style.position = 'absolute'; - largeImg.style.maxWidth = '400px'; - largeImg.style.maxHeight = '400px'; - largeImg.style.border = '1px solid #ccc'; - largeImg.style.background = '#fff'; - largeImg.style.zIndex = '1000'; - largeImg.style.top = imgElement.getBoundingClientRect().top + window.scrollY + 'px'; - largeImg.style.left = imgElement.getBoundingClientRect().left + imgElement.offsetWidth + 10 + 'px'; - largeImg.id = 'largeImagePreview'; - - document.body.appendChild(largeImg); -} - -function hideLargeImage() { - const largeImg = document.getElementById('largeImagePreview'); - if (largeImg) { - largeImg.remove(); - } -} - -async function getProductData(sku, domain) { - let url; - if (domain === 'marianek.pl') { - url = `https://marianek.pl/api/v1/product.php?sku=${sku}`; - } else if (domain === 'pomysloweprezenty.pl') { - url = `https://pomysloweprezenty.pl/api/v1/product.php?sku=${sku}`; - } else { - console.error('Unsupported domain:', domain); - return null; - } - - try { - const response = await fetch(url); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - const data = await response.json(); console.log(data); - return data.img; - } catch (error) { - console.error('Error fetching product data: ' + url, error); - return null; - } -} - -const currentUrl2 = window.location.href; -const pattern = /^https:\/\/projectpro\.apilo\.com\/warehouse\/shipment\/new-for-order\/.+/; - -if (pattern.test(currentUrl2)) { - const portletBody = document.querySelector('.kt-portlet__body'); - if (portletBody) { - const buttonContainer = document.createElement('div'); - buttonContainer.className = 'custom-button-container'; - buttonContainer.style.display = 'flex'; - buttonContainer.style.gap = '10px'; - buttonContainer.style.marginBottom = '15px'; - portletBody.parentNode.insertBefore(buttonContainer, portletBody); - - const buttonP2D = createButton('Inpost P2D', '#007bff', 'P2D.inpost', 'RZE14N||RZE14N'); - const buttonD2D = createButton('Inpost D2D', '#ff7800', 'D2D.inpostkurier'); - const buttonD2P = createButton('Inpost D2P', '#28a745', 'D2P.inpost'); - const buttonP2P = createButton('Inpost P2P', '#ffc107', 'P2P.inpost', 'RZE14N||RZE14N'); - - buttonContainer.appendChild(buttonP2D); - buttonContainer.appendChild(buttonD2D); - buttonContainer.appendChild(buttonD2P); - buttonContainer.appendChild(buttonP2P); - } -} - -function createButton(text, backgroundColor, method, dropoffPoint = null) { - const button = document.createElement('button'); - button.textContent = text; - button.style.background = backgroundColor; - button.style.display = 'inline-flex'; - button.style.width = '200px'; - button.style.height = '40px'; - button.style.alignItems = 'center'; - button.style.justifyContent = 'center'; - button.style.color = '#FFF'; - button.style.border = 'none'; - button.style.cursor = 'pointer'; - - button.addEventListener('click', () => handleShipment(method, dropoffPoint)); - - return button; -} - -async function handleShipment(method, dropoffPoint = null) { - try { - await selectPackageType(); - await setShipmentMethod(method); - if (dropoffPoint) { - await setDropoffPoint(dropoffPoint); - } - await setParcelWeight('1'); - await submitShipment(); - } catch (error) { - console.error('Wystąpił błąd: ', error); - } -} - -function retryUntilSuccess(fn, interval = 500, retries = 10) { - return new Promise((resolve, reject) => { - const attempt = async () => { - try { - const result = await fn(); - resolve(result); - } catch (err) { - if (retries === 0) { - reject(err); - } else { - setTimeout(() => { - retries--; - attempt(); - }, interval); - } - } - }; - attempt(); - }); -} - -function selectPackageType() { - return retryUntilSuccess(() => { - return new Promise((resolve, reject) => { - const selectElement = document.getElementById('warehousebundle_shipment_packageType'); - if (selectElement) { - selectElement.value = 'package'; - const event = document.createEvent('HTMLEvents'); - event.initEvent('change', true, false); - selectElement.dispatchEvent(event); - resolve(); - } else { - reject('Nie znaleziono elementu packageType'); - } - }); - }); -} - -function setShipmentMethod(method) { - return retryUntilSuccess(() => { - return new Promise((resolve, reject) => { - const methodElement = document.getElementById('warehousebundle_shipment_method'); - if (methodElement) { - methodElement.value = method; - const methodEvent = document.createEvent('HTMLEvents'); - methodEvent.initEvent('change', true, false); - methodElement.dispatchEvent(methodEvent); - resolve(); - } else { - reject('Nie znaleziono elementu shipment_method'); - } - }); - }); -} - -function setDropoffPoint(dropoffPoint) { - return retryUntilSuccess(() => { - return new Promise((resolve, reject) => { - const dropoffPointElement = document.getElementById('warehousebundle_shipment_preferences_dropoffPoint'); - if (dropoffPointElement) { - dropoffPointElement.value = dropoffPoint; - const dropoffPointEvent = document.createEvent('HTMLEvents'); - dropoffPointEvent.initEvent('change', true, false); - dropoffPointElement.dispatchEvent(dropoffPointEvent); - resolve(); - } else { - reject('Nie znaleziono elementu dropoffPoint'); - } - }); - }); -} - -function setParcelWeight(weight) { - return retryUntilSuccess(() => { - return new Promise((resolve, reject) => { - const weightElement = document.getElementById('warehousebundle_shipment_shipmentParcels_0_weight'); - if (weightElement) { - weightElement.value = weight; - const weightEvent = document.createEvent('HTMLEvents'); - weightEvent.initEvent('change', true, false); - weightElement.dispatchEvent(weightEvent); - resolve(); - } else { - reject('Nie znaleziono elementu shipmentParcels_0_weight'); - } - }); - }); -} - -function submitShipment() { - return retryUntilSuccess(() => { - return new Promise((resolve, reject) => { - const submitButton = document.getElementById('warehousebundle_shipment_buttons_submit'); - if (submitButton) { - submitButton.click(); - resolve(); - } else { - reject('Nie znaleziono przycisku submit'); - } - }); - }); -} \ No newline at end of file diff --git a/autoload/admin/factory/class.ShopProduct.php b/autoload/admin/factory/class.ShopProduct.php index d07164f..e8c2522 100644 --- a/autoload/admin/factory/class.ShopProduct.php +++ b/autoload/admin/factory/class.ShopProduct.php @@ -3,6 +3,38 @@ namespace admin\factory; use shop\Product; class ShopProduct { + private static function seoLinkUsedByOtherProduct( int $product_id, string $lang_id, string $seo_link ): bool + { + global $mdb; + + if ( !$seo_link ) + return false; + + return (bool) $mdb -> count( 'pp_shop_products_langs', [ + 'AND' => [ + 'lang_id' => $lang_id, + 'seo_link' => $seo_link, + 'product_id[!]' => $product_id, + ], + ] ); + } + + private static function removeConflictingRedirectSources( int $product_id, string $lang_id, string $from ): void + { + global $mdb; + + if ( !$from ) + return; + + $mdb -> delete( 'pp_redirects', [ + 'AND' => [ + 'from' => $from, + 'lang_id' => $lang_id, + 'product_id[!]' => $product_id, + ], + ] ); + } + // count_product static public function count_product( $where = null ) { @@ -1010,18 +1042,30 @@ class ShopProduct if ( $new_seo_link !== $current_seo_link and $current_seo_link != '' ) { - if ( !$mdb -> count( 'pp_redirects', [ 'from' => $current_seo_link, 'to' => $new_seo_link, 'lang_id' => $lg['id'], 'product_id' => $product_id ] ) ) + if ( $mdb -> count( 'pp_redirects', [ 'from' => $new_seo_link, 'to' => $current_seo_link, 'lang_id' => $lg['id'], 'product_id' => $product_id ] ) ) + $mdb -> delete( 'pp_redirects', [ 'from' => $new_seo_link, 'to' => $current_seo_link, 'lang_id' => $lg['id'], 'product_id' => $product_id ] ); + + $mdb -> delete( 'pp_redirects', [ + 'AND' => [ + 'product_id' => $product_id, + 'lang_id' => $lg['id'], + 'from' => $current_seo_link, + 'to[!]' => $new_seo_link, + ], + ] ); + + if ( !self::seoLinkUsedByOtherProduct( (int) $product_id, (string) $lg['id'], (string) $current_seo_link ) ) { - if ( $mdb -> count( 'pp_redirects', [ 'from' => $new_seo_link, 'to' => $current_seo_link, 'lang_id' => $lg['id'], 'product_id' => $product_id ] ) ) - $mdb -> delete( 'pp_redirects', [ 'from' => $new_seo_link, 'to' => $current_seo_link, 'lang_id' => $lg['id'], 'product_id' => $product_id ] ); - else + self::removeConflictingRedirectSources( (int) $product_id, (string) $lg['id'], (string) $current_seo_link ); + + if ( !$mdb -> count( 'pp_redirects', [ 'from' => $current_seo_link, 'to' => $new_seo_link, 'lang_id' => $lg['id'], 'product_id' => $product_id ] ) ) { - if ( \S::canAddRedirect( $current_seo_link, $new_seo_link ) ) + if ( \S::canAddRedirect( $current_seo_link, $new_seo_link, $lg['id'] ) ) $mdb -> insert( 'pp_redirects', [ 'from' => $current_seo_link, 'to' => $new_seo_link, 'lang_id' => $lg['id'], 'product_id' => $product_id ] ); - else - $mdb -> delete( 'pp_redirects', [ 'product_id' => $product_id, 'lang_id' => $lg['id'] ] ); } } + else + $mdb -> delete( 'pp_redirects', [ 'AND' => [ 'product_id' => $product_id, 'lang_id' => $lg['id'], 'from' => $current_seo_link ] ] ); } $mdb -> update( 'pp_shop_products_langs', [ diff --git a/autoload/class.S.php b/autoload/class.S.php index c6aac6f..67e0aa3 100644 --- a/autoload/class.S.php +++ b/autoload/class.S.php @@ -49,46 +49,58 @@ class S return $parts; } - static function canAddRedirect( $from, $to ) + static function canAddRedirect( $from, $to, $lang_id = null ) { global $mdb; - $redirects = $mdb -> select( 'pp_redirects', '*' ); + if ( !$from or !$to or $from === $to ) + return false; + + $where = []; + if ( null !== $lang_id ) + $where['lang_id'] = $lang_id; + + $redirects = $mdb -> select( 'pp_redirects', [ 'from', 'to' ], $where ); $redirectMap = []; foreach ( $redirects as $redirect ) { - $redirectMap[$redirect['from']] = $redirect['to']; + if ( !isset( $redirectMap[$redirect['from']] ) ) + $redirectMap[$redirect['from']] = []; + + if ( !in_array( $redirect['to'], $redirectMap[$redirect['from']], true ) ) + $redirectMap[$redirect['from']][] = $redirect['to']; } - // Dodaj nowe przekierowanie do mapy tymczasowo - $redirectMap[$from] = $to; + if ( !isset( $redirectMap[$from] ) ) + $redirectMap[$from] = []; + + if ( !in_array( $to, $redirectMap[$from], true ) ) + $redirectMap[$from][] = $to; - // Funkcja do sprawdzania cyklu za pomocą DFS $visited = []; - $stack = []; - - function hasCycle($current, $target, &$redirectMap, &$visited) + $stack = [ $to ]; + while ( !empty( $stack ) ) { - if ($current === $target) { - return true; - } + $current = array_pop( $stack ); - if (isset($visited[$current])) { - return false; - } + if ( $current === $from ) + return false; + + if ( isset( $visited[$current] ) ) + continue; $visited[$current] = true; - if (isset($redirectMap[$current])) { - return hasCycle($redirectMap[$current], $target, $redirectMap, $visited); + if ( isset( $redirectMap[$current] ) ) + { + foreach ( $redirectMap[$current] as $next ) + if ( !isset( $visited[$next] ) ) + $stack[] = $next; } - - return false; } - // Sprawdź, czy istnieje ścieżka z $newTo do $newFrom - return !hasCycle($to, $from, $redirectMap, $visited); + return true; } static public function clear_redis_cache() @@ -1113,3 +1125,4 @@ class S return false; } } + diff --git a/autoload/front/factory/class.Layouts.php b/autoload/front/factory/class.Layouts.php index c23d930..5454e2f 100644 --- a/autoload/front/factory/class.Layouts.php +++ b/autoload/front/factory/class.Layouts.php @@ -8,6 +8,29 @@ class Layouts return $mdb -> get( 'pp_layouts', 'id', [ 'categories_default' => 1 ] ); } + static public function default_layout() + { + global $mdb; + + $cacheHandler = new \CacheHandler(); + $cacheKey = "\front\factory\Layouts::default_layout"; + + $objectData = $cacheHandler -> get( $cacheKey ); + if ( $objectData ) + { + $cachedLayout = @unserialize( $objectData ); + if ( is_array( $cachedLayout ) and !empty( $cachedLayout ) ) + return $cachedLayout; + + $cacheHandler -> delete( $cacheKey ); + } + + $layout = $mdb -> get( 'pp_layouts', '*', [ 'status' => 1 ] ); + $cacheHandler -> set( $cacheKey, $layout ); + + return $layout; + } + static public function product_layout( $product_id ) { global $mdb; @@ -16,18 +39,47 @@ class Layouts $cacheKey = "\front\factory\Layouts::product_layout:$product_id"; $objectData = $cacheHandler -> get( $cacheKey ); - - if ( !$objectData ) + if ( $objectData ) { - $layout = $mdb -> get( 'pp_layouts', [ '[><]pp_shop_products' => [ 'id' => 'layout_id' ] ], '*', [ 'pp_shop_products.id' => (int)$product_id ] ); + $cachedLayout = @unserialize( $objectData ); + if ( is_array( $cachedLayout ) and !empty( $cachedLayout ) ) + return $cachedLayout; - $cacheHandler -> set( $cacheKey, $layout ); + $cacheHandler -> delete( $cacheKey ); } + + $layoutRows = $mdb -> query( + "SELECT pp_layouts.* + FROM pp_layouts + JOIN pp_shop_products ON pp_layouts.id = pp_shop_products.layout_id + WHERE pp_shop_products.id = " . (int)$product_id . " + ORDER BY pp_layouts.id DESC" + ) -> fetchAll( \PDO::FETCH_ASSOC ); + + if ( is_array( $layoutRows ) and isset( $layoutRows[0] ) ) + $layout = $layoutRows[0]; else { - return unserialize( $objectData ); + $layoutRows = $mdb -> query( + "SELECT pp_layouts.* + FROM pp_layouts + JOIN pp_layouts_categories ON pp_layouts.id = pp_layouts_categories.layout_id + JOIN pp_shop_products_categories ON pp_shop_products_categories.category_id = pp_layouts_categories.category_id + WHERE pp_shop_products_categories.product_id = " . (int)$product_id . " + ORDER BY pp_shop_products_categories.o ASC, pp_layouts.id DESC" + ) -> fetchAll( \PDO::FETCH_ASSOC ); + + if ( is_array( $layoutRows ) and isset( $layoutRows[0] ) ) + $layout = $layoutRows[0]; + else + $layout = $mdb -> get( 'pp_layouts', '*', [ 'categories_default' => 1 ] ); } + if ( !$layout ) + $layout = $mdb -> get( 'pp_layouts', '*', [ 'status' => 1 ] ); + + $cacheHandler -> set( $cacheKey, $layout ); + return $layout; } @@ -62,21 +114,34 @@ class Layouts $cacheKey = "\front\factory\Layouts::category_layout:$category_id"; $objectData = $cacheHandler -> get( $cacheKey ); - - if ( !$objectData ) + if ( $objectData ) { - $layout = $mdb -> query( "SELECT pp_layouts.* FROM pp_layouts JOIN pp_layouts_categories ON pp_layouts.id = pp_layouts_categories.layout_id WHERE pp_layouts_categories.category_id = " . (int)$category_id ) -> fetchAll( \PDO::FETCH_ASSOC ); - if ( !$layout ) - $layout = $mdb -> get( 'pp_layouts', '*', [ 'categories_default' => 1 ] ); + $cachedLayout = @unserialize( $objectData ); + if ( is_array( $cachedLayout ) and !empty( $cachedLayout ) ) + return $cachedLayout; - $cacheHandler -> set( $cacheKey, $layout[0] ); + $cacheHandler -> delete( $cacheKey ); } + + $layoutRows = $mdb -> query( + "SELECT pp_layouts.* + FROM pp_layouts + JOIN pp_layouts_categories ON pp_layouts.id = pp_layouts_categories.layout_id + WHERE pp_layouts_categories.category_id = " . (int)$category_id . " + ORDER BY pp_layouts.id DESC" + ) -> fetchAll( \PDO::FETCH_ASSOC ); + + if ( is_array( $layoutRows ) and isset( $layoutRows[0] ) ) + $layout = $layoutRows[0]; else - { - return unserialize( $objectData ); - } + $layout = $mdb -> get( 'pp_layouts', '*', [ 'categories_default' => 1 ] ); - return $layout[0]; + if ( !$layout ) + $layout = $mdb -> get( 'pp_layouts', '*', [ 'status' => 1 ] ); + + $cacheHandler -> set( $cacheKey, $layout ); + + return $layout; } static public function active_layout( $page_id ) diff --git a/autoload/front/view/class.Site.php b/autoload/front/view/class.Site.php index d48388a..7d9d677 100644 --- a/autoload/front/view/class.Site.php +++ b/autoload/front/view/class.Site.php @@ -34,6 +34,9 @@ class Site if ( \S::get( 'category' ) ) $layout = \front\factory\Layouts::category_layout( \S::get( 'category' ) ); + if ( !$layout and \S::get( 'module' ) ) + $layout = \front\factory\Layouts::default_layout(); + if ( !$layout ) $layout = \front\factory\Layouts::active_layout( $page['id'] ); diff --git a/autoload/shop/class.Basket.php b/autoload/shop/class.Basket.php index 58de358..8f1a675 100644 --- a/autoload/shop/class.Basket.php +++ b/autoload/shop/class.Basket.php @@ -23,7 +23,27 @@ class Basket implements \ArrayAccess foreach ( $basket as $key => $val ) { - $quantity_options = \shop\Product::get_product_permutation_quantity_options( $val['parent_id'] ? $val['parent_id'] : $val['product-id'], $val['attributes'][0] ); + $permutation = null; + + if ( isset( $val['parent_id'] ) and (int)$val['parent_id'] and isset( $val['product-id'] ) ) + $permutation = \shop\Product::get_product_permutation_hash( (int)$val['product-id'] ); + + if ( !$permutation and isset( $val['attributes'] ) and is_array( $val['attributes'] ) and count( $val['attributes'] ) ) + $permutation = implode( '|', $val['attributes'] ); + + $quantity_options = \shop\Product::get_product_permutation_quantity_options( + $val['parent_id'] ? $val['parent_id'] : $val['product-id'], + $permutation + ); + + if ( + (int)$basket[ $key ][ 'quantity' ] < 1 + and ( (int)$quantity_options['quantity'] > 0 or (int)$quantity_options['stock_0_buy'] === 1 ) + ) + { + $basket[ $key ][ 'quantity' ] = 1; + $result = true; + } if ( ( $val[ 'quantity' ] > $quantity_options['quantity'] ) and !$quantity_options['stock_0_buy'] ) { diff --git a/autoload/shop/class.Product.php b/autoload/shop/class.Product.php index 0e46e6d..770e55e 100644 --- a/autoload/shop/class.Product.php +++ b/autoload/shop/class.Product.php @@ -537,7 +537,7 @@ class Product implements \ArrayAccess global $mdb, $settings; $cacheHandler = new \CacheHandler(); - $cacheKey = "\shop\Product::get_product_permutation_quantity_options:$product_id:$permutation"; + $cacheKey = "\shop\Product::get_product_permutation_quantity_options:v2:$product_id:$permutation"; $objectData = $cacheHandler -> get( $cacheKey ); @@ -546,10 +546,15 @@ class Product implements \ArrayAccess if ( $mdb -> count( 'pp_shop_products', [ 'AND' => [ 'parent_id' => $product_id, 'permutation_hash' => $permutation ] ] ) ) { $result['quantity'] = $mdb -> get( 'pp_shop_products', 'quantity', [ 'AND' => [ 'parent_id' => $product_id, 'permutation_hash' => $permutation ] ] ); + $result['stock_0_buy'] = $mdb -> get( 'pp_shop_products', 'stock_0_buy', [ 'AND' => [ 'parent_id' => $product_id, 'permutation_hash' => $permutation ] ] ); + if ( $result['quantity'] == null ) { $result['quantity'] = $mdb -> get( 'pp_shop_products', 'quantity', [ 'id' => $product_id ] ); - $result['stock_0_buy'] = $mdb -> get( 'pp_shop_products', 'stock_0_buy', [ 'id' => $product_id] ); + + if ( $result['stock_0_buy'] == null ) + $result['stock_0_buy'] = $mdb -> get( 'pp_shop_products', 'stock_0_buy', [ 'id' => $product_id] ); + $result['messages'] = $mdb -> get( 'pp_shop_products_langs', [ 'warehouse_message_zero', 'warehouse_message_nonzero' ], [ 'AND' => [ 'product_id' => $product_id, 'lang_id' => 'pl' ] ] ); if ( !$result['messages']['warehouse_message_zero'] ) @@ -560,7 +565,6 @@ class Product implements \ArrayAccess } else { - $result['stock_0_buy'] = $mdb -> get( 'pp_shop_products', 'stock_0_buy', [ 'AND' => [ 'parent_id' => $product_id, 'permutation_hash' => $permutation ] ] ); $result['messages'] = $mdb -> get( 'pp_shop_products_langs', [ 'warehouse_message_zero', 'warehouse_message_nonzero' ], [ 'AND' => [ 'product_id' => $product_id, 'lang_id' => 'pl' ] ] ); if ( !$result['messages']['warehouse_message_zero'] ) @@ -758,6 +762,13 @@ class Product implements \ArrayAccess return $mdb -> get( 'pp_shop_products', 'id', [ 'AND' => [ 'parent_id' => $parent_id, 'permutation_hash' => implode( '|', $attributes ) ] ] ); } + // pobranie permutation_hash dla kombinacji produktu + static public function get_product_permutation_hash( int $product_id ) + { + global $mdb; + return $mdb -> get( 'pp_shop_products', 'permutation_hash', [ 'id' => $product_id ] ); + } + // pobranie listy atrybutów z wybranymi wartościami static public function get_product_attributes( $products ) { diff --git a/geocode-cache.php b/geocode-cache.php deleted file mode 100644 index 0ebb528..0000000 --- a/geocode-cache.php +++ /dev/null @@ -1,211 +0,0 @@ - $raw, - 'norm' => $norm, - 'hash' => hash('sha256', $norm), - 'mode' => $mode, - ]; -} - -// DB -try { - $pdo = new PDO(DB_DSN, DB_USER, DB_PASS, [ - PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, - PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, - ]); -} catch (Throwable $e) { - http_response_code(500); - echo json_encode(['error' => 'DB connection failed']); - exit; -} - -// Wejście -$q = isset($_GET['q']) ? trim((string)$_GET['q']) : ''; -$lat = isset($_GET['lat']) ? (float)$_GET['lat'] : null; -$lng = isset($_GET['lng']) ? (float)$_GET['lng'] : null; - -if ($q === '' && ($lat === null || $lng === null)) { - http_response_code(400); - echo json_encode(['error' => 'Provide q=address or lat & lng']); - exit; -} - -$key = build_cache_key($q, $lat, $lng); -$now = now_mysql(); - -// ===== 1) CACHE LOOKUP (forward i reverse) ===== -$stmt = $pdo->prepare("SELECT * FROM geocode_cache WHERE query_hash = :h AND (expires_at IS NULL OR expires_at > NOW()) LIMIT 1"); -$stmt->execute([':h' => $key['hash']]); -if ($row = $stmt->fetch()) { - $pdo->prepare("UPDATE geocode_cache SET hits = hits + 1, updated_at = NOW() WHERE id = :id")->execute([':id' => $row['id']]); - echo json_encode([ - 'from_cache' => true, - 'source' => 'cache', - 'stale' => false, - 'lat' => (float)$row['lat'], - 'lng' => (float)$row['lng'], - 'formatted_address' => $row['formatted_address'], - 'place_id' => $row['place_id'], - 'provider' => $row['provider'], - ], JSON_UNESCAPED_UNICODE); - exit; -} - -// ===== 2) PYTANIE DO GOOGLE ===== -function google_request(array $params): array { - $base = 'https://maps.googleapis.com/maps/api/geocode/json'; - $params['key'] = GOOGLE_GEOCODE_KEY; - $params += ['language' => GOOGLE_LANGUAGE, 'region' => GOOGLE_REGION]; - $url = $base . '?' . http_build_query($params); - - $ch = curl_init($url); - curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 5, CURLOPT_CONNECTTIMEOUT => 3]); - $res = curl_exec($ch); $err = curl_error($ch); curl_close($ch); - if ($res === false) throw new RuntimeException('cURL error: ' . $err); - $data = json_decode($res, true); - if (!is_array($data)) throw new RuntimeException('Invalid JSON from Google'); - if (($data['status'] ?? '') !== 'OK' || empty($data['results'][0])) { - $status = $data['status'] ?? 'UNKNOWN'; - throw new RuntimeException('Geocoding failed: ' . $status); - } - return $data; -} - -try { - $data = ($key['mode'] === 'forward') - ? google_request(['address' => $q]) - : google_request(['latlng' => $lat . ',' . $lng]); - - $top = $data['results'][0]; - $latV = (float)$top['geometry']['location']['lat']; - $lngV = (float)$top['geometry']['location']['lng']; - $fmt = (string)($top['formatted_address'] ?? ''); - $pid = (string)($top['place_id'] ?? ''); - - // ===== 3) ZAPIS DO CACHE (forward i reverse) ===== - $ttlDays = ($key['mode'] === 'forward') ? CACHE_TTL_DAYS_FORWARD : CACHE_TTL_DAYS_REVERSE; - $expires = add_days($now, $ttlDays); - - $stmt = $pdo->prepare("INSERT INTO geocode_cache - (query_hash, query_raw, query_norm, lat, lng, formatted_address, place_id, provider, hits, created_at, updated_at, expires_at, raw_json) - VALUES (:h, :raw, :norm, :lat, :lng, :fmt, :pid, 'google', 1, :now, :now, :exp, :json) - ON DUPLICATE KEY UPDATE - lat = VALUES(lat), - lng = VALUES(lng), - formatted_address = VALUES(formatted_address), - place_id = VALUES(place_id), - updated_at = VALUES(updated_at), - expires_at = VALUES(expires_at), - raw_json = VALUES(raw_json), - hits = hits + 1"); - - $stmt->execute([ - ':h' => $key['hash'], - ':raw' => $key['raw'], - ':norm'=> $key['norm'], - ':lat' => $latV, - ':lng' => $lngV, - ':fmt' => $fmt, - ':pid' => $pid, - ':now' => $now, - ':exp' => $expires, - ':json'=> json_encode($data, JSON_UNESCAPED_UNICODE), - ]); - - echo json_encode([ - 'from_cache' => false, - 'source' => 'google', - 'stale' => false, - 'lat' => $latV, - 'lng' => $lngV, - 'formatted_address' => $fmt, - 'place_id' => $pid, - 'provider' => 'google', - ], JSON_UNESCAPED_UNICODE); - exit; - -} catch (Throwable $e) { - // ===== 4) Fallback: zwróć najświeższe co mamy (też dla reverse) ===== - try { - $stmt = $pdo->prepare("SELECT * FROM geocode_cache WHERE query_hash = :h LIMIT 1"); - $stmt->execute([':h' => $key['hash']]); - if ($row = $stmt->fetch()) { - http_response_code(200); - echo json_encode([ - 'from_cache' => true, - 'source' => 'cache', - 'stale' => true, - 'lat' => (float)$row['lat'], - 'lng' => (float)$row['lng'], - 'formatted_address' => $row['formatted_address'], - 'place_id' => $row['place_id'], - 'provider' => $row['provider'], - ], JSON_UNESCAPED_UNICODE); - exit; - } - } catch (Throwable $ignored) {} - - http_response_code(502); - echo json_encode(['error' => $e->getMessage()]); - exit; -} \ No newline at end of file diff --git a/templates/shop-basket/basket-details.php b/templates/shop-basket/basket-details.php index cc98a12..4e2649c 100644 --- a/templates/shop-basket/basket-details.php +++ b/templates/shop-basket/basket-details.php @@ -7,6 +7,22 @@ unset( $price ); unset( $price_new ); $product = \shop\Product::getFromCache( (int)$position['product-id'], $this -> lang_id ); + + $permutation = null; + if ( isset( $position['parent_id'] ) and (int)$position['parent_id'] and isset( $position['product-id'] ) ) + $permutation = \shop\Product::get_product_permutation_hash( (int)$position['product-id'] ); + + if ( !$permutation and isset( $position['attributes'] ) and is_array( $position['attributes'] ) and count( $position['attributes'] ) ) + $permutation = implode( '|', $position['attributes'] ); + + $quantity_options = \shop\Product::get_product_permutation_quantity_options( + (int)( $position['parent_id'] ? $position['parent_id'] : $position['product-id'] ), + $permutation + ); + + $max_quantity = (int)$quantity_options['quantity']; + if ( !$max_quantity and (int)$quantity_options['stock_0_buy'] ) + $max_quantity = 999; ?>
\ No newline at end of file + diff --git a/templates/shop-product/product.php b/templates/shop-product/product.php index 8077c7d..35cc314 100644 --- a/templates/shop-product/product.php +++ b/templates/shop-product/product.php @@ -321,17 +321,17 @@ }); }; - if ( $( '#product #tab-0' ).visible() ) + if ( $( '#product #tab-0' ).is( ':visible' ) ) $( '#product #tabs-menu #tab-link-0' ).addClass( 'current' ); else $( '#product #tabs-menu #tab-link-0' ).removeClass( 'current' ); - if ( $( '#product #tab-1' ).visible() ) + if ( $( '#product #tab-1' ).is( ':visible' ) ) $( '#product #tabs-menu #tab-link-1' ).addClass( 'current' ); else $( '#product #tabs-menu #tab-link-1' ).removeClass( 'current' ); - if ( $( '#product #tab-2' ).visible() ) + if ( $( '#product #tab-2' ).is( ':visible' ) ) $( '#product #tabs-menu #tab-link-2' ).addClass( 'current' ); else $( '#product #tabs-menu #tab-link-2' ).removeClass( 'current' ); diff --git a/updates/0.20/ver_0.267.zip b/updates/0.20/ver_0.267.zip new file mode 100644 index 0000000..c90bfc8 Binary files /dev/null and b/updates/0.20/ver_0.267.zip differ diff --git a/updates/0.20/ver_0.267_files.txt b/updates/0.20/ver_0.267_files.txt new file mode 100644 index 0000000..6f2e124 --- /dev/null +++ b/updates/0.20/ver_0.267_files.txt @@ -0,0 +1,2 @@ +F: ../apilo-bck +F: ../geocode-cache.php diff --git a/updates/0.20/ver_0.267_sql.txt b/updates/0.20/ver_0.267_sql.txt new file mode 100644 index 0000000..0dcc48c --- /dev/null +++ b/updates/0.20/ver_0.267_sql.txt @@ -0,0 +1,11 @@ +DELETE r1 FROM pp_redirects r1 +INNER JOIN pp_redirects r2 + ON r1.`from` = r2.`from` + AND (r1.lang_id <=> r2.lang_id) + AND ( + IFNULL(r1.date_add, '1000-01-01 00:00:00') < IFNULL(r2.date_add, '1000-01-01 00:00:00') + OR ( + IFNULL(r1.date_add, '1000-01-01 00:00:00') = IFNULL(r2.date_add, '1000-01-01 00:00:00') + AND r1.id < r2.id + ) + ); diff --git a/updates/changelog.php b/updates/changelog.php index e2098d7..ba595e9 100644 --- a/updates/changelog.php +++ b/updates/changelog.php @@ -1,3 +1,13 @@ +ver. 0.267 - 13.02.2026