From 6543f8dc315be7f76b11c47232c4c5c061bfc71c Mon Sep 17 00:00:00 2001 From: Jacek Pyziak Date: Sat, 14 Feb 2026 20:16:18 +0100 Subject: [PATCH] feat: Add Transport module with repository, controller, and views - Implemented TransportRepository for managing transport data with methods for listing, finding, saving, and retrieving transport costs. - Created ShopTransportController to handle transport-related actions, including listing, editing, and saving transports. - Added views for transport management: transports list and transport edit forms. - Introduced JavaScript for responsive tabs in transport edit view. - Updated testing suite with comprehensive unit tests for TransportRepository and ShopTransportController. - Increased test coverage with new assertions and scenarios for transport functionalities. --- .phpunit.result.cache | 2 +- .../transport-edit-custom-script.php | 12 + .../shop-transport/transport-edit.php | 145 +------- .../shop-transport/transports-list.php | 1 + admin/templates/shop-transport/view-list.php | 71 ---- admin/templates/site/main-layout.php | 2 +- .../Domain/Transport/TransportRepository.php | 342 ++++++++++++++++++ .../Controllers/ShopTransportController.php | 342 ++++++++++++++++++ autoload/admin/class.Site.php | 8 + .../admin/controls/class.ShopTransport.php | 37 -- .../admin/factory/class.ShopTransport.php | 100 +---- autoload/admin/view/class.ShopTransport.php | 5 - .../front/factory/class.ShopTransport.php | 28 +- docs/CHANGELOG.md | 18 + docs/DATABASE_STRUCTURE.md | 31 ++ docs/PROJECT_STRUCTURE.md | 10 + docs/REFACTORING_PLAN.md | 11 +- docs/TESTING.md | 26 +- .../Transport/TransportRepositoryTest.php | 334 +++++++++++++++++ .../ShopTransportControllerTest.php | 67 ++++ 20 files changed, 1215 insertions(+), 377 deletions(-) create mode 100644 admin/templates/shop-transport/transport-edit-custom-script.php create mode 100644 admin/templates/shop-transport/transports-list.php delete mode 100644 admin/templates/shop-transport/view-list.php create mode 100644 autoload/Domain/Transport/TransportRepository.php create mode 100644 autoload/admin/Controllers/ShopTransportController.php delete mode 100644 autoload/admin/controls/class.ShopTransport.php delete mode 100644 autoload/admin/view/class.ShopTransport.php create mode 100644 tests/Unit/Domain/Transport/TransportRepositoryTest.php create mode 100644 tests/Unit/admin/Controllers/ShopTransportControllerTest.php diff --git a/.phpunit.result.cache b/.phpunit.result.cache index df04dc3..9612492 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.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 +{"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.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.003,"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,"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,"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.084,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testVerifyTwofaCodeReturnsFalseForExpiredCode":0.074,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testVerifyTwofaCodeReturnsTrueForValidCode":0.148,"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.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.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.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,"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,"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.001,"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.001,"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}} \ No newline at end of file diff --git a/admin/templates/shop-transport/transport-edit-custom-script.php b/admin/templates/shop-transport/transport-edit-custom-script.php new file mode 100644 index 0000000..f7aac6e --- /dev/null +++ b/admin/templates/shop-transport/transport-edit-custom-script.php @@ -0,0 +1,12 @@ + diff --git a/admin/templates/shop-transport/transport-edit.php b/admin/templates/shop-transport/transport-edit.php index 6d2c87c..2f0eeee 100644 --- a/admin/templates/shop-transport/transport-edit.php +++ b/admin/templates/shop-transport/transport-edit.php @@ -1,139 +1,8 @@ - - - apilo_carrier_account_list as $carrier_account ) -{ - $carrier_accounts[ $carrier_account['id'] ] = $carrier_account['name']; -} -ob_start(); -?> -
- -
-
- 'Nazwa', - 'class' => 'require', - 'name' => 'name', - 'id' => 'name', - 'readonly' => true, - 'value' => $this -> transport_details['name'] - ) - );?> - 'Nazwa widoczna', - 'name' => 'name_visible', - 'id' => 'name_visible', - 'value' => $this -> transport_details['name_visible'] - ) - );?> - 'Opis', - 'name' => 'description', - 'id' => 'description', - 'value' => $this -> transport_details['description'] - ) - );?> - 'Koszt (PLN)', - 'class' => 'number-format require', - 'name' => 'cost', - 'id' => 'cost', - 'value' => $this -> transport_details['cost'] - ) - );?> - 'Maks. WP', - 'class' => 'number-format require', - 'name' => 'max_wp', - 'id' => 'max_wp', - 'value' => $this -> transport_details['max_wp'] - ) - );?> - 'Domyślna forma dostawy', - 'name' => 'default', - 'checked' => $this -> transport_details['default'] == 1 ? true : false - ] );?> - 'Aktywny', - 'name' => 'status', - 'checked' => $this -> transport_details['status'] == 1 ? true : false - ) - );?> - 'Darmowa dostawa', - 'name' => 'delivery_free', - 'checked' => $this -> transport_details['delivery_free'] == 1 ? true : false - ] );?> - 'Kurier z Apilo', - 'name' => 'apilo_carrier_account_id', - 'id' => 'apilo_carrier_account_id', - 'values' => $carrier_accounts, - 'value' => $this -> transport_details['apilo_carrier_account_id'] - ] );?> -
-
- payments_list ) ): foreach ( $this -> payments_list as $payment_method ):?> -
-
-
- transport_details['payment_methods'] ) ):?>checked="checked" /> - -
-
-
- -
-
-
- $this->form, +]); -$grid = new \gridEdit; -$grid -> id = 'transport-edit'; -$grid -> gdb_opt = $gdb; -$grid -> include_plugins = true; -$grid -> title = 'Edycja rodzaju transportu'; -$grid -> fields = [ - [ - 'db' => 'id', - 'type' => 'hidden', - 'value' => $this -> transport_details['id'] - ] - ]; -$grid -> actions = [ - 'save' => [ 'url' => '/admin/shop_transport/transport_save/', 'back_url' => '/admin/shop_transport/view_list/' ], - 'cancel' => [ 'url' => '/admin/shop_transport/view_list/' ] - ]; -$grid -> external_code = $out; -$grid -> persist_edit = true; -$grid -> id_param = 'id'; - -echo $grid -> draw(); -?> - \ No newline at end of file +echo \Tpl::view('shop-transport/transport-edit-custom-script', [ + 'form' => $this->form, +]); diff --git a/admin/templates/shop-transport/transports-list.php b/admin/templates/shop-transport/transports-list.php new file mode 100644 index 0000000..0f89c5b --- /dev/null +++ b/admin/templates/shop-transport/transports-list.php @@ -0,0 +1 @@ + $this->viewModel]); ?> diff --git a/admin/templates/shop-transport/view-list.php b/admin/templates/shop-transport/view-list.php deleted file mode 100644 index 250acfb..0000000 --- a/admin/templates/shop-transport/view-list.php +++ /dev/null @@ -1,71 +0,0 @@ - apilo_carrier_account_list as $carrier_account ) -{ - $carrier_accounts[ $carrier_account['id'] ] = $carrier_account['name']; -} - -$grid = new \grid( 'pp_shop_transports' ); -$grid -> gdb_opt = $gdb; -$grid -> debug = true; -$grid -> order = [ 'column' => 'name', 'type' => 'ASC' ]; -$grid -> search = [ - [ 'name' => 'Nazwa', 'db' => 'name', 'type' => 'text' ], - [ 'name' => 'Aktywny', 'db' => 'status', 'type' => 'select', 'replace' => [ 'array' => [ 0 => 'nie', 1 => 'tak' ] ] ] - ]; -$grid -> columns_view = [ - [ - 'name' => 'Lp.', - 'th' => [ 'class' => 'g-lp' ], - 'td' => [ 'class' => 'g-center' ], - 'autoincrement' => true - ], [ - 'name' => 'Domyślna FT', - 'db' => 'default', - 'replace' => [ 'array' => [ 0 => 'nie', 1 => 'tak' ] ], - 'td' => [ 'class' => 'g-center' ], - 'th' => [ 'class' => 'g-center', 'style' => 'width: 150px;' ], - 'sort' => true - ], [ - 'name' => 'Aktywny', - 'db' => 'status', - 'replace' => [ 'array' => [ 0 => 'nie', 1 => 'tak' ] ], - 'td' => [ 'class' => 'g-center' ], - 'th' => [ 'class' => 'g-center', 'style' => 'width: 150px;' ], - 'sort' => true - ], - [ - 'name' => 'Koszt', - 'db' => 'cost', - 'td' => [ 'class' => 'g-center' ], - 'th' => [ 'class' => 'g-center', 'style' => 'width: 150px;' ], - 'php' => 'echo \\S::decimal( [cost] ) . " zł";', - 'sort' => true - ], - [ - 'name' => 'Maks. WP', - 'db' => 'max_wp', - 'td' => [ 'class' => 'g-center' ], - 'th' => [ 'class' => 'g-center', 'style' => 'width: 150px;' ], - 'sort' => true - ], - [ - 'name' => 'Nazwa', - 'db' => 'name', - 'sort' => true, - 'php' => 'echo "[name]";' - ], [ - 'name' => 'Typ kuriera Apilo', - 'db' => 'apilo_carrier_account_id', - 'th' => [ 'class' => 'g-center' ], - 'td' => [ 'class' => 'g-center' ], - 'replace' => [ 'array' => $carrier_accounts ], - ], [ - 'name' => 'Edytuj', - 'action' => [ 'type' => 'edit', 'url' => '/admin/shop_transport/transport_edit/id=[id]' ], - 'th' => [ 'class' => 'g-center', 'style' => 'width: 70px;' ], - 'td' => [ 'class' => 'g-center' ] - ] - ]; -echo $grid -> draw(); \ No newline at end of file diff --git a/admin/templates/site/main-layout.php b/admin/templates/site/main-layout.php index 7a8bf07..112dcb1 100644 --- a/admin/templates/site/main-layout.php +++ b/admin/templates/site/main-layout.php @@ -70,7 +70,7 @@
  • Cechy produktów
  • -
  • Rodzaje transportu
  • +
  • Rodzaje transportu
  • Metody płatności
  • Statusy zamówień diff --git a/autoload/Domain/Transport/TransportRepository.php b/autoload/Domain/Transport/TransportRepository.php new file mode 100644 index 0000000..fe3f32d --- /dev/null +++ b/autoload/Domain/Transport/TransportRepository.php @@ -0,0 +1,342 @@ +db = $db; + } + + public function listForAdmin( + array $filters = [], + string $sortColumn = 'name', + string $sortDir = 'ASC', + int $page = 1, + int $perPage = 15 + ): array { + $allowedSortColumns = [ + 'id' => 'st.id', + 'name' => 'st.name', + 'status' => 'st.status', + 'cost' => 'st.cost', + 'max_wp' => 'st.max_wp', + 'default' => 'st.default', + 'o' => 'st.o', + ]; + + $sortSql = $allowedSortColumns[$sortColumn] ?? 'st.name'; + $sortDir = strtoupper(trim($sortDir)) === 'DESC' ? 'DESC' : 'ASC'; + $page = max(1, $page); + $perPage = min(self::MAX_PER_PAGE, max(1, $perPage)); + $offset = ($page - 1) * $perPage; + + $where = ['1 = 1']; + $params = []; + + $name = trim((string)($filters['name'] ?? '')); + if ($name !== '') { + if (strlen($name) > 255) { + $name = substr($name, 0, 255); + } + $where[] = 'st.name LIKE :name'; + $params[':name'] = '%' . $name . '%'; + } + + $status = trim((string)($filters['status'] ?? '')); + if ($status === '0' || $status === '1') { + $where[] = 'st.status = :status'; + $params[':status'] = (int)$status; + } + + $whereSql = implode(' AND ', $where); + + $sqlCount = " + SELECT COUNT(0) + FROM pp_shop_transports AS st + WHERE {$whereSql} + "; + + $stmtCount = $this->db->query($sqlCount, $params); + $countRows = $stmtCount ? $stmtCount->fetchAll() : []; + $total = isset($countRows[0][0]) ? (int)$countRows[0][0] : 0; + + $sql = " + SELECT + st.id, + st.name, + st.name_visible, + st.description, + st.status, + st.cost, + st.max_wp, + st.default, + st.apilo_carrier_account_id, + st.delivery_free, + st.o + FROM pp_shop_transports AS st + WHERE {$whereSql} + ORDER BY {$sortSql} {$sortDir}, st.id ASC + LIMIT {$perPage} OFFSET {$offset} + "; + + $stmt = $this->db->query($sql, $params); + $items = $stmt ? $stmt->fetchAll() : []; + + if (!is_array($items)) { + $items = []; + } + + foreach ($items as &$item) { + $item = $this->normalizeTransport($item); + } + unset($item); + + return [ + 'items' => $items, + 'total' => $total, + ]; + } + + public function find(int $transportId): ?array + { + if ($transportId <= 0) { + return null; + } + + $transport = $this->db->get('pp_shop_transports', '*', ['id' => $transportId]); + + if (!is_array($transport)) { + return null; + } + + $transport = $this->normalizeTransport($transport); + + $paymentMethods = $this->db->select( + 'pp_shop_transport_payment_methods', + 'id_payment_method', + ['id_transport' => $transportId] + ); + + $transport['payment_methods'] = is_array($paymentMethods) ? $paymentMethods : []; + + return $transport; + } + + public function save(array $data): ?int + { + $transportId = isset($data['id']) ? (int)$data['id'] : 0; + $name = trim((string)($data['name'] ?? '')); + $nameVisible = trim((string)($data['name_visible'] ?? '')); + $description = trim((string)($data['description'] ?? '')); + $status = $this->toSwitchValue($data['status'] ?? 0); + $cost = isset($data['cost']) ? (float)$data['cost'] : 0.0; + $maxWp = isset($data['max_wp']) && $data['max_wp'] !== '' ? (int)$data['max_wp'] : null; + $default = $this->toSwitchValue($data['default'] ?? 0); + $apiloCarrierAccountId = isset($data['apilo_carrier_account_id']) && $data['apilo_carrier_account_id'] !== '' + ? (int)$data['apilo_carrier_account_id'] + : null; + $deliveryFree = $this->toSwitchValue($data['delivery_free'] ?? 0); + $paymentMethods = $data['payment_methods'] ?? []; + + if ($default === 1) { + $this->db->update('pp_shop_transports', ['default' => 0]); + } + + $transportData = [ + 'name' => $name, + 'name_visible' => $nameVisible, + 'description' => $description, + 'status' => $status, + 'default' => $default, + 'cost' => $cost, + 'max_wp' => $maxWp, + 'apilo_carrier_account_id' => $apiloCarrierAccountId, + 'delivery_free' => $deliveryFree, + ]; + + if (!$transportId) { + $this->db->insert('pp_shop_transports', $transportData); + $id = $this->db->id(); + + if ($id) { + $this->savePaymentMethodLinks((int)$id, $paymentMethods); + return (int)$id; + } + + return null; + } else { + $this->db->update('pp_shop_transports', $transportData, ['id' => $transportId]); + $this->db->delete('pp_shop_transport_payment_methods', ['id_transport' => $transportId]); + $this->savePaymentMethodLinks($transportId, $paymentMethods); + return $transportId; + } + } + + public function allActive(): array + { + $transports = $this->db->select( + 'pp_shop_transports', + '*', + [ + 'status' => 1, + 'ORDER' => ['o' => 'ASC'], + ] + ); + + if (!is_array($transports)) { + return []; + } + + foreach ($transports as &$transport) { + $transport = $this->normalizeTransport($transport); + } + unset($transport); + + return $transports; + } + + public function findActiveById(int $transportId): ?array + { + if ($transportId <= 0) { + return null; + } + + $transport = $this->db->get( + 'pp_shop_transports', + '*', + ['AND' => ['id' => $transportId, 'status' => 1]] + ); + + if (!$transport) { + return null; + } + + return $this->normalizeTransport($transport); + } + + public function getApiloCarrierAccountId(int $transportId): ?int + { + if ($transportId <= 0) { + return null; + } + + $result = $this->db->get( + 'pp_shop_transports', + 'apilo_carrier_account_id', + ['id' => $transportId] + ); + + return $result !== null ? (int)$result : null; + } + + public function getTransportCost(int $transportId): ?float + { + if ($transportId <= 0) { + return null; + } + + $result = $this->db->get( + 'pp_shop_transports', + 'cost', + ['AND' => ['id' => $transportId, 'status' => 1]] + ); + + return $result !== null ? (float)$result : null; + } + + public function lowestTransportPrice(int $wp): ?float + { + $result = $this->db->get( + 'pp_shop_transports', + 'cost', + [ + 'AND' => [ + 'status' => 1, + 'id' => [2, 4, 6, 8, 9], + 'max_wp[>=]' => $wp + ], + 'ORDER' => ['cost' => 'ASC'] + ] + ); + + return $result !== null ? (float)$result : null; + } + + public function allForAdmin(): array + { + $transports = $this->db->select( + 'pp_shop_transports', + '*', + ['ORDER' => ['o' => 'ASC']] + ); + + if (!is_array($transports)) { + return []; + } + + foreach ($transports as &$transport) { + $transport = $this->normalizeTransport($transport); + } + unset($transport); + + return $transports; + } + + private function savePaymentMethodLinks(int $transportId, $paymentMethods): void + { + if (is_array($paymentMethods)) { + foreach ($paymentMethods as $paymentMethodId) { + $this->db->insert('pp_shop_transport_payment_methods', [ + 'id_payment_method' => (int)$paymentMethodId, + 'id_transport' => $transportId, + ]); + } + } elseif ($paymentMethods) { + $this->db->insert('pp_shop_transport_payment_methods', [ + 'id_payment_method' => (int)$paymentMethods, + 'id_transport' => $transportId, + ]); + } + } + + private function normalizeTransport(array $transport): array + { + $transport['id'] = isset($transport['id']) ? (int)$transport['id'] : 0; + $transport['status'] = $this->toSwitchValue($transport['status'] ?? 0); + $transport['default'] = $this->toSwitchValue($transport['default'] ?? 0); + $transport['delivery_free'] = $this->toSwitchValue($transport['delivery_free'] ?? 0); + $transport['cost'] = isset($transport['cost']) ? (float)$transport['cost'] : 0.0; + $transport['max_wp'] = isset($transport['max_wp']) && $transport['max_wp'] !== null + ? (int)$transport['max_wp'] + : null; + $transport['apilo_carrier_account_id'] = isset($transport['apilo_carrier_account_id']) && $transport['apilo_carrier_account_id'] !== null + ? (int)$transport['apilo_carrier_account_id'] + : null; + $transport['o'] = isset($transport['o']) ? (int)$transport['o'] : 0; + + return $transport; + } + + private function toSwitchValue($value): int + { + if (is_bool($value)) { + return $value ? 1 : 0; + } + + if (is_numeric($value)) { + return ((int)$value) === 1 ? 1 : 0; + } + + if (is_string($value)) { + $normalized = strtolower(trim($value)); + return in_array($normalized, ['1', 'on', 'true', 'yes'], true) ? 1 : 0; + } + + return 0; + } +} diff --git a/autoload/admin/Controllers/ShopTransportController.php b/autoload/admin/Controllers/ShopTransportController.php new file mode 100644 index 0000000..65fc5ae --- /dev/null +++ b/autoload/admin/Controllers/ShopTransportController.php @@ -0,0 +1,342 @@ +transportRepository = $transportRepository; + $this->paymentMethodRepository = $paymentMethodRepository; + } + + public function list(): string + { + $sortableColumns = ['id', 'name', 'status', 'cost', 'max_wp', 'default', 'o']; + $filterDefinitions = [ + [ + 'key' => 'name', + 'label' => 'Nazwa', + 'type' => 'text', + ], + [ + 'key' => 'status', + 'label' => 'Aktywny', + 'type' => 'select', + 'options' => [ + '' => '- aktywny -', + '1' => 'tak', + '0' => 'nie', + ], + ], + ]; + + $listRequest = \admin\Support\TableListRequestFactory::fromRequest( + $filterDefinitions, + $sortableColumns, + 'name' + ); + + $sortDir = $listRequest['sortDir']; + if (trim((string)\S::get('sort')) === '') { + $sortDir = 'ASC'; + } + + $result = $this->transportRepository->listForAdmin( + $listRequest['filters'], + $listRequest['sortColumn'], + $sortDir, + $listRequest['page'], + $listRequest['perPage'] + ); + + $apiloCarrierAccounts = $this->getApiloCarrierAccounts(); + + $rows = []; + $lp = ($listRequest['page'] - 1) * $listRequest['perPage'] + 1; + foreach ($result['items'] as $item) { + $id = (int)($item['id'] ?? 0); + $name = trim((string)($item['name'] ?? '')); + $status = (int)($item['status'] ?? 0); + $cost = (float)($item['cost'] ?? 0.0); + $maxWp = $item['max_wp'] ?? null; + $default = (int)($item['default'] ?? 0); + $apiloCarrierAccountId = $item['apilo_carrier_account_id'] ?? null; + + $apiloLabel = '-'; + if ($apiloCarrierAccountId !== null) { + $apiloKey = (string)$apiloCarrierAccountId; + if (isset($apiloCarrierAccounts[$apiloKey])) { + $apiloLabel = $apiloCarrierAccounts[$apiloKey]; + } + } + + $rows[] = [ + 'lp' => $lp++ . '.', + 'default' => $default === 1 ? 'tak' : 'nie', + 'status' => $status === 1 ? 'tak' : 'nie', + 'cost' => \S::decimal($cost) . ' zł', + 'max_wp' => $maxWp !== null ? (int)$maxWp : '-', + 'name' => '' . htmlspecialchars($name, ENT_QUOTES, 'UTF-8') . '', + 'apilo_carrier' => htmlspecialchars((string)$apiloLabel, ENT_QUOTES, 'UTF-8'), + '_actions' => [ + [ + 'label' => 'Edytuj', + 'url' => '/admin/shop_transport/edit/id=' . $id, + 'class' => 'btn btn-xs btn-primary', + ], + ], + ]; + } + + $total = (int)$result['total']; + $totalPages = max(1, (int)ceil($total / $listRequest['perPage'])); + + $viewModel = new PaginatedTableViewModel( + [ + ['key' => 'lp', 'label' => 'Lp.', 'class' => 'text-center', 'sortable' => false], + ['key' => 'default', 'sort_key' => 'default', 'label' => 'Domyślna FT', 'class' => 'text-center', 'sortable' => true, 'raw' => true], + ['key' => 'status', 'sort_key' => 'status', 'label' => 'Aktywny', 'class' => 'text-center', 'sortable' => true, 'raw' => true], + ['key' => 'cost', 'sort_key' => 'cost', 'label' => 'Koszt', 'class' => 'text-center', 'sortable' => true, 'raw' => true], + ['key' => 'max_wp', 'sort_key' => 'max_wp', 'label' => 'Maks. WP', 'class' => 'text-center', 'sortable' => true, 'raw' => true], + ['key' => 'name', 'sort_key' => 'name', 'label' => 'Nazwa', 'sortable' => true, 'raw' => true], + ['key' => 'apilo_carrier', 'label' => 'Typ kuriera Apilo', 'class' => 'text-center', 'sortable' => false], + ], + $rows, + $listRequest['viewFilters'], + [ + 'column' => $listRequest['sortColumn'], + 'dir' => $sortDir, + ], + [ + 'page' => $listRequest['page'], + 'per_page' => $listRequest['perPage'], + 'total' => $total, + 'total_pages' => $totalPages, + ], + array_merge($listRequest['queryFilters'], [ + 'sort' => $listRequest['sortColumn'], + 'dir' => $sortDir, + 'per_page' => $listRequest['perPage'], + ]), + $listRequest['perPageOptions'], + $sortableColumns, + '/admin/shop_transport/list/', + 'Brak danych w tabeli.' + ); + + return \Tpl::view('shop-transport/transports-list', [ + 'viewModel' => $viewModel, + ]); + } + + public function edit(): string + { + $transport = $this->transportRepository->find((int)\S::get('id')); + if ($transport === null) { + \S::alert('Rodzaj transportu nie został znaleziony.'); + header('Location: /admin/shop_transport/list/'); + exit; + } + + $paymentMethods = $this->paymentMethodRepository->allForAdmin(); + $apiloCarrierAccounts = $this->getApiloCarrierAccounts(); + + return \Tpl::view('shop-transport/transport-edit', [ + 'form' => $this->buildFormViewModel($transport, $paymentMethods, $apiloCarrierAccounts), + ]); + } + + public function save(): void + { + $payload = $_POST; + $transportId = isset($payload['id']) && $payload['id'] !== '' + ? (int)$payload['id'] + : (int)\S::get('id'); + + $payload['id'] = $transportId; + + $id = $this->transportRepository->save($payload); + if ($id !== null) { + \S::delete_dir('../temp/'); + echo json_encode([ + 'success' => true, + 'id' => (int)$id, + 'message' => 'Rodzaj transportu został zapisany.', + ]); + exit; + } + + echo json_encode([ + 'success' => false, + 'errors' => ['general' => 'Podczas zapisywania rodzaju transportu wystąpił błąd.'], + ]); + exit; + } + + private function buildFormViewModel( + array $transport, + array $paymentMethods, + array $apiloCarrierAccounts + ): FormEditViewModel { + $id = (int)($transport['id'] ?? 0); + $name = (string)($transport['name'] ?? ''); + + $apiloOptions = ['' => '--- wybierz konto przewoźnika ---']; + foreach ($apiloCarrierAccounts as $carrierId => $carrierName) { + $apiloOptions[(string)$carrierId] = $carrierName; + } + + $data = [ + 'id' => $id, + 'name' => $name, + 'name_visible' => (string)($transport['name_visible'] ?? ''), + 'description' => (string)($transport['description'] ?? ''), + 'cost' => (float)($transport['cost'] ?? 0.0), + 'max_wp' => $transport['max_wp'] ?? '', + 'default' => (int)($transport['default'] ?? 0), + 'status' => (int)($transport['status'] ?? 0), + 'delivery_free' => (int)($transport['delivery_free'] ?? 0), + 'apilo_carrier_account_id' => $transport['apilo_carrier_account_id'] ?? '', + ]; + + $fields = [ + FormField::hidden('id', $id), + FormField::text('name', [ + 'label' => 'Nazwa', + 'tab' => 'general', + 'readonly' => true, + 'required' => true, + ]), + FormField::text('name_visible', [ + 'label' => 'Nazwa widoczna', + 'tab' => 'general', + ]), + FormField::text('description', [ + 'label' => 'Opis', + 'tab' => 'general', + ]), + FormField::number('cost', [ + 'label' => 'Koszt (PLN)', + 'tab' => 'general', + 'step' => 0.01, + 'required' => true, + ]), + FormField::number('max_wp', [ + 'label' => 'Maks. WP', + 'tab' => 'general', + 'required' => true, + ]), + FormField::switch('default', [ + 'label' => 'Domyślna forma dostawy', + 'tab' => 'general', + ]), + FormField::switch('status', [ + 'label' => 'Aktywny', + 'tab' => 'general', + ]), + FormField::switch('delivery_free', [ + 'label' => 'Darmowa dostawa', + 'tab' => 'general', + ]), + FormField::select('apilo_carrier_account_id', [ + 'label' => 'Kurier z Apilo', + 'tab' => 'general', + 'options' => $apiloOptions, + ]), + ]; + + $transportPaymentMethods = $transport['payment_methods'] ?? []; + + $paymentMethodsHtml = ''; + if (is_array($paymentMethods) && !empty($paymentMethods)) { + foreach ($paymentMethods as $paymentMethod) { + $pmId = (int)($paymentMethod['id'] ?? 0); + $pmName = htmlspecialchars((string)($paymentMethod['name'] ?? ''), ENT_QUOTES, 'UTF-8'); + $pmStatus = (int)($paymentMethod['status'] ?? 0); + $checked = in_array($pmId, $transportPaymentMethods) ? 'checked="checked"' : ''; + $statusClass = $pmStatus === 0 ? 'text-muted' : ''; + + $paymentMethodsHtml .= '
    '; + $paymentMethodsHtml .= '
    '; + $paymentMethodsHtml .= '
    '; + $paymentMethodsHtml .= ''; + $paymentMethodsHtml .= '' . $pmName . ''; + $paymentMethodsHtml .= '
    '; + $paymentMethodsHtml .= '
    '; + $paymentMethodsHtml .= '
    '; + } + } + + $fields[] = FormField::custom( + 'payment_methods_section', + $paymentMethodsHtml, + ['tab' => 'payment_methods'] + ); + + $tabs = [ + new FormTab('general', 'Ogólne', 'fa-file'), + new FormTab('payment_methods', 'Powiązane metody płatności', 'fa-wrench'), + ]; + + $actionUrl = '/admin/shop_transport/save/id=' . $id; + $actions = [ + FormAction::save($actionUrl, '/admin/shop_transport/list/'), + FormAction::cancel('/admin/shop_transport/list/'), + ]; + + return new FormEditViewModel( + 'shop-transport-edit', + 'Edycja rodzaju transportu: ' . $name, + $data, + $fields, + $tabs, + $actions, + 'POST', + $actionUrl, + '/admin/shop_transport/list/', + true, + ['id' => $id] + ); + } + + private function getApiloCarrierAccounts(): array + { + $rawSetting = \admin\factory\Integrations::apilo_settings('carrier-account-list'); + $raw = null; + + if (is_array($rawSetting)) { + $raw = $rawSetting; + } elseif (is_string($rawSetting)) { + $decoded = @unserialize($rawSetting); + if (is_array($decoded)) { + $raw = $decoded; + } + } + + if (!is_array($raw)) { + return []; + } + + $list = []; + foreach ($raw as $carrier) { + if (is_array($carrier) && isset($carrier['id'], $carrier['name'])) { + $list[(string)$carrier['id']] = (string)$carrier['name']; + } + } + + return $list; + } +} diff --git a/autoload/admin/class.Site.php b/autoload/admin/class.Site.php index 20e7d52..92feee1 100644 --- a/autoload/admin/class.Site.php +++ b/autoload/admin/class.Site.php @@ -323,6 +323,14 @@ class Site new \Domain\PaymentMethod\PaymentMethodRepository( $mdb ) ); }, + 'ShopTransport' => function() { + global $mdb; + + return new \admin\Controllers\ShopTransportController( + new \Domain\Transport\TransportRepository( $mdb ), + new \Domain\PaymentMethod\PaymentMethodRepository( $mdb ) + ); + }, 'Pages' => function() { global $mdb; diff --git a/autoload/admin/controls/class.ShopTransport.php b/autoload/admin/controls/class.ShopTransport.php deleted file mode 100644 index adbc796..0000000 --- a/autoload/admin/controls/class.ShopTransport.php +++ /dev/null @@ -1,37 +0,0 @@ - 'error', 'msg' => 'Podczas zapisywania rodzaju transportu wystąpił błąd. Proszę spróbować ponownie.' ]; - $values = json_decode( \S::get( 'values' ), true ); - - if ( $id = \admin\factory\ShopTransport::transport_save( - $values['id'], $values['name'], $values['name_visible'], $values['description'], $values['status'], $values['cost'], $values['payment_methods'], $values['max_wp'], $values['default'], $values['apilo_carrier_account_id'], $values['delivery_free'] - ) ) - $response = [ 'status' => 'ok', 'msg' => 'Rodzaj transportu został zapisany.', 'id' => $id ]; - - echo json_encode( $response ); - exit; - } - - public static function transport_edit() - { - global $mdb; - $paymentMethodRepository = new \Domain\PaymentMethod\PaymentMethodRepository( $mdb ); - - return \Tpl::view( 'shop-transport/transport-edit', [ - 'transport_details' => \admin\factory\ShopTransport::transport_details( \S::get( 'id' ) ), - 'payments_list' => $paymentMethodRepository -> allForAdmin(), - 'apilo_carrier_account_list' => unserialize( \admin\factory\Integrations::apilo_settings( 'carrier-account-list' ) ), - ] ); - } - - public static function view_list() - { - return \Tpl::view( 'shop-transport/view-list', [ - 'apilo_carrier_account_list' => unserialize( \admin\factory\Integrations::apilo_settings( 'carrier-account-list' ) ), - ] ); - } -} diff --git a/autoload/admin/factory/class.ShopTransport.php b/autoload/admin/factory/class.ShopTransport.php index 6637f5d..5c00ae2 100644 --- a/autoload/admin/factory/class.ShopTransport.php +++ b/autoload/admin/factory/class.ShopTransport.php @@ -4,103 +4,7 @@ class ShopTransport { public static function lowest_transport_price( $wp ) { global $mdb; - return $mdb -> get( 'pp_shop_transports', 'cost', [ 'AND' => [ 'status' => 1, 'id' => [ 2, 4, 6, 8, 9 ], 'max_wp[>=]' => $wp ], 'ORDER' => [ 'cost' => 'ASC' ] ] ); - } - - public static function transport_save( $transport_id, $name, $name_visible, $description, $status, $cost, $payment_methods, $max_wp, $default, $apilo_carrier_account_id, $delivery_free ) - { - global $mdb; - - if ( !$transport_id ) - { - if ( $default == 'on' ) - $mdb -> update( 'pp_shop_transports', [ 'default' => '0' ] ); - - $mdb -> insert( 'pp_shop_transports', [ - 'name' => $name, - 'name_visible' => $name_visible, - 'description' => $description, - 'status' => $status == 'on' ? 1 : 0, - 'default' => $default == 'on' ? 1 : 0, - 'cost' => $cost, - 'max_wp' => $max_wp ? $max_wp : null, - 'apilo_carrier_account_id' => $apilo_carrier_account_id ? $apilo_carrier_account_id : null, - 'delivery_free' => $delivery_free == 'on' ? 1 : 0 - ] ); - - $id = $mdb -> id(); - - if ( $id ) - { - if ( is_array( $payment_methods ) ) foreach ( $payment_methods as $payment_method ) - { - $mdb -> insert( 'pp_shop_transport_payment_methods', [ - 'id_payment_method' => (int)$payment_method, - 'id_transport' => (int)$id - ] ); - } - else if ( $payment_methods ) - { - $mdb -> insert( 'pp_shop_transport_payment_methods', [ - 'id_payment_method' => (int)$payment_methods, - 'id_transport' => (int)$id - ] ); - } - - \S::delete_dir( '../temp/' ); - - return $id; - } - } - else - { - if ( $default == 'on' ) - $mdb -> update( 'pp_shop_transports', [ 'default' => '0' ] ); - - $mdb -> update( 'pp_shop_transports', [ - 'name' => $name, - 'name_visible' => $name_visible, - 'description' => $description, - 'status' => $status == 'on' ? 1 : 0, - 'default' => $default == 'on' ? 1 : 0, - 'cost' => $cost, - 'max_wp' => $max_wp ? $max_wp : null, - 'apilo_carrier_account_id' => $apilo_carrier_account_id ? $apilo_carrier_account_id : null, - 'delivery_free' => $delivery_free == 'on' ? 1 : 0 - ], [ - 'id' => $transport_id - ] ); - - $mdb -> delete( 'pp_shop_transport_payment_methods', [ 'id_transport' => (int)$transport_id ] ); - - if ( is_array( $payment_methods ) ) foreach ( $payment_methods as $payment_method ) - { - $mdb -> insert( 'pp_shop_transport_payment_methods', [ - 'id_payment_method' => (int)$payment_method, - 'id_transport' => (int)$transport_id - ] ); - } - else if ( $payment_methods ) - { - $mdb -> insert( 'pp_shop_transport_payment_methods', [ - 'id_payment_method' => (int)$payment_methods, - 'id_transport' => (int)$transport_id - ] ); - } - - \S::delete_dir( '../temp/' ); - - return true; - } - } - - public static function transport_details( $transport_id ) - { - global $mdb; - - $transport = $mdb -> get( 'pp_shop_transports', '*', [ 'id' => $transport_id ] ); - $transport['payment_methods'] = $mdb -> select( 'pp_shop_transport_payment_methods', 'id_payment_method', [ 'id_transport' => $transport_id ] ); - - return $transport; + $repo = new \Domain\Transport\TransportRepository($mdb); + return $repo->lowestTransportPrice($wp); } } diff --git a/autoload/admin/view/class.ShopTransport.php b/autoload/admin/view/class.ShopTransport.php deleted file mode 100644 index 929eb62..0000000 --- a/autoload/admin/view/class.ShopTransport.php +++ /dev/null @@ -1,5 +0,0 @@ - get( 'pp_shop_transports', 'apilo_carrier_account_id', [ 'id' => $transport_method_id ] ); + $repo = new \Domain\Transport\TransportRepository($mdb); + return $repo->getApiloCarrierAccountId($transport_method_id); } public static function transport_methods( $basket, $coupon ) @@ -20,14 +20,8 @@ class ShopTransport if ( !$objectData ) { - $results = $mdb -> query( 'SELECT ' - . 'pst.id, name, name_visible, description, cost, max_wp, pst.default, delivery_free ' - . 'FROM ' - . 'pp_shop_transports AS pst ' - . 'WHERE ' - . 'status = 1 ORDER BY o ASC' ) -> fetchAll( \PDO::FETCH_ASSOC ); - if ( is_array( $results ) and !empty( $results ) ) foreach ( $results as $row ) - $transports_tmp[] = $row; + $repo = new \Domain\Transport\TransportRepository($mdb); + $transports_tmp = $repo->allActive(); $cacheHandler -> set( $cacheKey, $transports_tmp ); } @@ -65,11 +59,8 @@ class ShopTransport if ( !$cost = \Cache::fetch( 'transport_cost_' . $transport_id ) ) { - $cost = $mdb -> get( 'pp_shop_transports', 'cost', [ - 'AND' => [ - 'id' => $transport_id, - 'status' => 1 - ] ] ); + $repo = new \Domain\Transport\TransportRepository($mdb); + $cost = $repo->getTransportCost($transport_id); \Cache::store( 'transport_cost_' . $transport_id, $cost ); } @@ -82,11 +73,8 @@ class ShopTransport if ( !$transport = \Cache::fetch( 'transport' . $transport_id ) ) { - $transport = $mdb -> get( 'pp_shop_transports', '*', [ - 'AND' => [ - 'id' => $transport_id, - 'status' => 1 - ] ] ); + $repo = new \Domain\Transport\TransportRepository($mdb); + $transport = $repo->findActiveById($transport_id); \Cache::store( 'transport' . $transport_id, $transport ); } diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 2b1c17d..657e55b 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -4,6 +4,24 @@ Logi zmian z migracji na Domain-Driven Architecture. Najnowsze na gorze. --- +## ver. 0.269 (2026-02-14) - ShopTransport + +- **ShopTransport** - migracja `/admin/shop_transport` na Domain + DI + nowe widoki + - NOWE: `Domain\Transport\TransportRepository` (`listForAdmin`, `find`, `save`, `allActive`, `allForAdmin`, `findActiveById`, `getTransportCost`, `lowestTransportPrice`, `getApiloCarrierAccountId`) + - NOWE: `admin\Controllers\ShopTransportController` (DI) z akcjami `list`, `edit`, `save` + - NOWE: widoki `shop-transport/transports-list.php` i `shop-transport/transport-edit.php` + `transport-edit-custom-script.php` + - UPDATE: routing i menu admin na kanoniczny URL `/admin/shop_transport/list/` + - UPDATE: `admin\factory\ShopTransport`, `front\factory\ShopTransport` przepiete na nowe repozytorium + - FIX: `save()` return type `?int` zamiast `int|bool` (spojnosc z PaymentMethod) + - FIX: `toSwitchValue()` helper zamiast `=== 'on'` (obsluga '1', 'on', 'true', 'yes') + - FIX: `\S::delete_dir()` przeniesione z repozytorium do kontrolera (DDD) + - FIX: Medoo `select()` syntax - ORDER w WHERE zamiast 4-arg form + - CLEANUP: usuniete legacy `autoload/admin/controls/class.ShopTransport.php`, `autoload/admin/view/class.ShopTransport.php`, `admin/templates/shop-transport/view-list.php` + - FIX: `transports-list.php` - zmienna `'viewModel'` zmieniona na `'list'` (zgodnie z `table-list.php` komponentem) +- Testy: **OK (300 tests, 895 assertions)** + +--- + ## ver. 0.268 (2026-02-14) - ShopPaymentMethod + Apilo token keepalive - **ShopPaymentMethod** - migracja `/admin/shop_payment_method` na Domain + DI + nowe widoki diff --git a/docs/DATABASE_STRUCTURE.md b/docs/DATABASE_STRUCTURE.md index acb13ca..c3ee1b1 100644 --- a/docs/DATABASE_STRUCTURE.md +++ b/docs/DATABASE_STRUCTURE.md @@ -384,6 +384,37 @@ Metody platnosci sklepu (modul `/admin/shop_payment_method`). **Aktualizacja 2026-02-14 (ver. 0.268):** modul `/admin/shop_payment_method` korzysta z `Domain\PaymentMethod\PaymentMethodRepository` przez `admin\Controllers\ShopPaymentMethodController`. Usunieto legacy klasy `admin\controls\ShopPaymentMethod`, `admin\factory\ShopPaymentMethod`, `admin\view\ShopPaymentMethod` oraz widok `admin/templates/shop-payment-method/view-list.php`. +## pp_shop_transports +Rodzaje transportu sklepu (modul `/admin/shop_transport`). + +| Kolumna | Opis | +|---------|------| +| id | PK | +| name | Nazwa (systemowa, readonly) | +| name_visible | Nazwa widoczna dla klienta | +| description | Opis metody transportu | +| status | Status: 1 = aktywny, 0 = nieaktywny | +| cost | Koszt dostawy (PLN) | +| max_wp | Maksymalna waga paczki (NULL = bez limitu) | +| default | Domyslna forma dostawy (0/1) | +| delivery_free | Czy obsluguje darmowa dostawe (0/1) | +| apilo_carrier_account_id | ID konta przewoznika w Apilo (NULL gdy brak mapowania) | +| o | Kolejnosc wyswietlania | + +**Uzywane w:** `Domain\Transport\TransportRepository`, `admin\Controllers\ShopTransportController`, `front\factory\ShopTransport`, `admin\factory\ShopTransport` + +## pp_shop_transport_payment_methods +Powiazanie metod transportu z metodami platnosci (tabela lacznikowa). + +| Kolumna | Opis | +|---------|------| +| id_transport | FK do pp_shop_transports | +| id_payment_method | FK do pp_shop_payment_methods | + +**Uzywane w:** `Domain\Transport\TransportRepository`, `Domain\PaymentMethod\PaymentMethodRepository::forTransport()` + +**Aktualizacja 2026-02-14 (ver. 0.269):** modul `/admin/shop_transport` korzysta z `Domain\Transport\TransportRepository` przez `admin\Controllers\ShopTransportController`. Usunieto legacy klasy `admin\controls\ShopTransport`, `admin\view\ShopTransport` oraz widok `admin/templates/shop-transport/view-list.php`. + ## pp_shop_apilo_settings Ustawienia integracji Apilo (key-value). diff --git a/docs/PROJECT_STRUCTURE.md b/docs/PROJECT_STRUCTURE.md index 450e66b..016afee 100644 --- a/docs/PROJECT_STRUCTURE.md +++ b/docs/PROJECT_STRUCTURE.md @@ -126,6 +126,7 @@ shopPRO/ ### Tabele checkout - `pp_shop_payment_methods` - metody platnosci sklepu (mapowanie `apilo_payment_type_id`) +- `pp_shop_transports` - rodzaje transportu sklepu (mapowanie `apilo_carrier_account_id`) - `pp_shop_transport_payment_methods` - powiazanie metod transportu i platnosci Pelna dokumentacja tabel: `DATABASE_STRUCTURE.md` @@ -215,6 +216,8 @@ autoload/ │ │ └── CouponRepository.php │ ├── ShopStatus/ │ │ └── ShopStatusRepository.php +│ ├── Transport/ +│ │ └── TransportRepository.php │ └── ... ├── admin/ │ ├── Controllers/ # Nowe kontrolery (namespace \admin\Controllers\) @@ -232,6 +235,13 @@ autoload/ - Modul `/admin/shop_payment_method/*` dziala na nowych widokach (`payment-methods-list`, `payment-method-edit`). - Usunieto legacy: `autoload/admin/controls/class.ShopPaymentMethod.php`, `autoload/admin/factory/class.ShopPaymentMethod.php`, `autoload/admin/view/class.ShopPaymentMethod.php`, `admin/templates/shop-payment-method/view-list.php`. +**Aktualizacja 2026-02-14 (ver. 0.269):** +- Dodano modul domenowy `Domain/Transport/TransportRepository.php`. +- Dodano kontroler DI `admin/Controllers/ShopTransportController.php`. +- Modul `/admin/shop_transport/*` dziala na nowych widokach (`transports-list`, `transport-edit`). +- Usunieto legacy: `autoload/admin/controls/class.ShopTransport.php`, `autoload/admin/view/class.ShopTransport.php`, `admin/templates/shop-transport/view-list.php`. +- `admin\factory\ShopTransport` i `front\factory\ShopTransport` przepiete na repozytorium. + ### Routing admin (admin\Site::route()) 1. Sprawdź mapę `$newControllers` → utwórz instancję z DI → wywołaj 2. Jeśli nowy kontroler nie istnieje (`class_exists()` = false) → fallback na `admin\controls\` diff --git a/docs/REFACTORING_PLAN.md b/docs/REFACTORING_PLAN.md index 5d80306..f804131 100644 --- a/docs/REFACTORING_PLAN.md +++ b/docs/REFACTORING_PLAN.md @@ -149,6 +149,7 @@ grep -r "Product::getQuantity" . | 18 | ShopCoupon | 0.266 | listForAdmin, find, save, delete, categoriesTree | | 19 | ShopStatuses | 0.267 | listForAdmin, find, save, color picker | | 20 | ShopPaymentMethod | 0.268 | listForAdmin, find, save, allActive, mapowanie Apilo, DI kontroler | +| 21 | ShopTransport | 0.269 | listForAdmin, find, save, allActive, allForAdmin, findActiveById, getTransportCost, lowestTransportPrice, getApiloCarrierAccountId, powiazanie z PaymentMethod, DI kontroler | ### Product - szczegolowy status - ✅ getQuantity (ver. 0.238) @@ -167,12 +168,12 @@ grep -r "Product::getQuantity" . ## Kolejność refaktoryzacji (priorytet) -1-20: ✅ Cache, Product, Banner, Settings, Dictionaries, ProductArchive, Filemanager, Users, Pages, Integrations, ShopPromotion, ShopCoupon, ShopStatuses, ShopPaymentMethod +1-21: ✅ Cache, Product, Banner, Settings, Dictionaries, ProductArchive, Filemanager, Users, Pages, Integrations, ShopPromotion, ShopCoupon, ShopStatuses, ShopPaymentMethod, ShopTransport Nastepne: -21. **Order** -22. **Category** -23. **ShopAttribute** +22. **Order** +23. **Category** +24. **ShopAttribute** ## Form Edit System @@ -269,7 +270,7 @@ tests/ │ └── UsersControllerTest.php └── Integration/ ``` -**Łącznie: 280 testów, 828 asercji** +**Łącznie: 300 testów, 895 asercji** Pelna dokumentacja testow: `TESTING.md` diff --git a/docs/TESTING.md b/docs/TESTING.md index 67ff0d5..18f4481 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -36,7 +36,7 @@ Alternatywnie (Git Bash): Ostatnio zweryfikowano: 2026-02-14 ```text -OK (280 tests, 828 assertions) +OK (300 tests, 895 assertions) ``` ## Struktura testow @@ -57,6 +57,7 @@ tests/ | | |-- Promotion/PromotionRepositoryTest.php | | |-- Settings/SettingsRepositoryTest.php | | |-- ShopStatus/ShopStatusRepositoryTest.php +| | |-- Transport/TransportRepositoryTest.php | | `-- User/UserRepositoryTest.php | `-- admin/ | `-- Controllers/ @@ -69,6 +70,7 @@ tests/ | |-- ShopPaymentMethodControllerTest.php | |-- ShopPromotionControllerTest.php | |-- ShopStatusesControllerTest.php +| |-- ShopTransportControllerTest.php | `-- UsersControllerTest.php `-- Integration/ ``` @@ -351,3 +353,25 @@ OK (254 tests, 736 assertions) Nowe testy dodane 2026-02-14: - `tests/Unit/Domain/ShopStatus/ShopStatusRepositoryTest.php` (9 testow: find z ID=0, find null apilo, save update, save z ID=0, empty apilo sets null, reject negative ID, getApiloStatusId, getByIntegrationStatusId, allStatuses, whitelist sortowania) - `tests/Unit/admin/Controllers/ShopStatusesControllerTest.php` (5 testow: kontrakty metod, brak aliasow legacy, return types, DI konstruktora) + +## Aktualizacja suite (ShopPaymentMethod refactor, ver. 0.268) +Ostatnio zweryfikowano: 2026-02-14 + +```text +OK (280 tests, 828 assertions) +``` + +Nowe testy dodane 2026-02-14: +- `tests/Unit/Domain/PaymentMethod/PaymentMethodRepositoryTest.php` (14 testow: find invalid/null/normalize, save update/null/non-numeric apilo, listForAdmin whitelist, allActive, allForAdmin, findActiveById, isActive, getApiloPaymentTypeId, forTransport) +- `tests/Unit/admin/Controllers/ShopPaymentMethodControllerTest.php` (5 testow: kontrakty metod, brak aliasow legacy, return types, DI konstruktora) + +## Aktualizacja suite (ShopTransport refactor, ver. 0.269) +Ostatnio zweryfikowano: 2026-02-14 + +```text +OK (300 tests, 895 assertions) +``` + +Nowe testy dodane 2026-02-14: +- `tests/Unit/Domain/Transport/TransportRepositoryTest.php` (14 testow: find invalid/null/normalize/nullables, save insert/update/failure/default reset/switch normalization, listForAdmin whitelist, allActive, getApiloCarrierAccountId, getTransportCost, allForAdmin) +- `tests/Unit/admin/Controllers/ShopTransportControllerTest.php` (5 testow: kontrakty metod, brak aliasow legacy, return types, DI konstruktora z 2 repo) diff --git a/tests/Unit/Domain/Transport/TransportRepositoryTest.php b/tests/Unit/Domain/Transport/TransportRepositoryTest.php new file mode 100644 index 0000000..fab82a9 --- /dev/null +++ b/tests/Unit/Domain/Transport/TransportRepositoryTest.php @@ -0,0 +1,334 @@ +createMock(\medoo::class); + $mockDb->expects($this->never())->method('get'); + + $repository = new TransportRepository($mockDb); + $this->assertNull($repository->find(0)); + $this->assertNull($repository->find(-1)); + } + + public function testFindReturnsNullWhenNotFound(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('get') + ->with('pp_shop_transports', '*', ['id' => 5]) + ->willReturn(null); + + $repository = new TransportRepository($mockDb); + $this->assertNull($repository->find(5)); + } + + public function testFindNormalizesDataAndIncludesPaymentMethods(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('get') + ->with('pp_shop_transports', '*', ['id' => 3]) + ->willReturn([ + 'id' => '3', + 'name' => 'Kurier DPD', + 'name_visible' => 'DPD', + 'description' => 'Opis', + 'status' => '1', + 'cost' => '15.99', + 'max_wp' => '30', + 'default' => '0', + 'delivery_free' => '1', + 'apilo_carrier_account_id' => '42', + 'o' => '2', + ]); + + $mockDb->expects($this->once()) + ->method('select') + ->with('pp_shop_transport_payment_methods', 'id_payment_method', ['id_transport' => 3]) + ->willReturn([1, 3, 5]); + + $repository = new TransportRepository($mockDb); + $result = $repository->find(3); + + $this->assertIsArray($result); + $this->assertSame(3, $result['id']); + $this->assertSame('Kurier DPD', $result['name']); + $this->assertSame(1, $result['status']); + $this->assertSame(15.99, $result['cost']); + $this->assertSame(30, $result['max_wp']); + $this->assertSame(0, $result['default']); + $this->assertSame(1, $result['delivery_free']); + $this->assertSame(42, $result['apilo_carrier_account_id']); + $this->assertSame(2, $result['o']); + $this->assertSame([1, 3, 5], $result['payment_methods']); + } + + public function testFindHandlesNullMaxWpAndApiloId(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->method('get') + ->willReturn([ + 'id' => '1', + 'name' => 'Test', + 'status' => '0', + 'cost' => '0', + 'max_wp' => null, + 'default' => '0', + 'delivery_free' => '0', + 'apilo_carrier_account_id' => null, + 'o' => '0', + ]); + $mockDb->method('select')->willReturn([]); + + $repository = new TransportRepository($mockDb); + $result = $repository->find(1); + + $this->assertNull($result['max_wp']); + $this->assertNull($result['apilo_carrier_account_id']); + $this->assertSame([], $result['payment_methods']); + } + + public function testSaveInsertReturnsNewId(): void + { + $mockDb = $this->createMock(\medoo::class); + + $mockDb->expects($this->once()) + ->method('insert') + ->with('pp_shop_transports', $this->callback(function ($data) { + return $data['name'] === 'Nowy transport' + && $data['status'] === 1 + && $data['cost'] === 10.5; + })); + + $mockDb->expects($this->once()) + ->method('id') + ->willReturn('7'); + + $repository = new TransportRepository($mockDb); + $id = $repository->save([ + 'id' => 0, + 'name' => 'Nowy transport', + 'status' => 'on', + 'cost' => '10.5', + 'default' => '0', + 'delivery_free' => 0, + ]); + + $this->assertSame(7, $id); + } + + public function testSaveUpdateReturnsExistingId(): void + { + $mockDb = $this->createMock(\medoo::class); + + $mockDb->expects($this->once()) + ->method('update') + ->with('pp_shop_transports', $this->isType('array'), ['id' => 4]); + + $mockDb->expects($this->once()) + ->method('delete') + ->with('pp_shop_transport_payment_methods', ['id_transport' => 4]); + + $repository = new TransportRepository($mockDb); + $id = $repository->save([ + 'id' => 4, + 'name' => 'Update', + 'status' => 1, + 'cost' => 5.0, + 'default' => 0, + 'delivery_free' => 'on', + ]); + + $this->assertSame(4, $id); + } + + public function testSaveInsertReturnsNullOnFailure(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->method('insert'); + $mockDb->method('id')->willReturn(null); + + $repository = new TransportRepository($mockDb); + $this->assertNull($repository->save([ + 'id' => 0, + 'name' => 'Fail', + ])); + } + + public function testSaveResetsDefaultWhenSettingNew(): void + { + $mockDb = $this->createMock(\medoo::class); + $updateCalls = []; + + $mockDb->method('update') + ->willReturnCallback(function ($table, $data, $where = null) use (&$updateCalls) { + $updateCalls[] = ['table' => $table, 'data' => $data, 'where' => $where]; + }); + + $mockDb->method('delete'); + + $repository = new TransportRepository($mockDb); + $repository->save([ + 'id' => 2, + 'name' => 'X', + 'default' => 'on', + 'status' => 0, + 'delivery_free' => 0, + ]); + + $this->assertCount(2, $updateCalls); + $this->assertSame(['default' => 0], $updateCalls[0]['data']); + $this->assertNull($updateCalls[0]['where']); + } + + public function testSaveSwitchValuesNormalization(): void + { + $mockDb = $this->createMock(\medoo::class); + $savedData = null; + + $mockDb->method('update') + ->willReturnCallback(function ($table, $data) use (&$savedData) { + $savedData = $data; + }); + $mockDb->method('delete'); + + $repository = new TransportRepository($mockDb); + $repository->save([ + 'id' => 1, + 'name' => 'T', + 'status' => 'on', + 'default' => 'true', + 'delivery_free' => '1', + 'cost' => 0, + ]); + + $this->assertSame(1, $savedData['status']); + $this->assertSame(1, $savedData['default']); + $this->assertSame(1, $savedData['delivery_free']); + } + + public function testListForAdminWhitelistsSortColumn(): void + { + $mockDb = $this->createMock(\medoo::class); + $queries = []; + + $mockDb->method('query') + ->willReturnCallback(function ($sql, $params = []) use (&$queries) { + $queries[] = ['sql' => $sql, 'params' => $params]; + + if (strpos($sql, 'COUNT(0)') !== false) { + return new class { + public function fetchAll() { return [[1]]; } + }; + } + + return new class { + public function fetchAll() { + return [[ + 'id' => 1, 'name' => 'DPD', 'name_visible' => '', + 'description' => '', 'status' => 1, 'cost' => 10.0, + 'max_wp' => null, 'default' => 0, + 'apilo_carrier_account_id' => null, 'delivery_free' => 0, 'o' => 1, + ]]; + } + }; + }); + + $repository = new TransportRepository($mockDb); + $result = $repository->listForAdmin( + [], + 'name DESC; DROP TABLE pp_shop_transports; --', + 'DESC; DELETE FROM pp_users; --', + 1, + 999 + ); + + $this->assertCount(2, $queries); + $dataSql = $queries[1]['sql']; + + $this->assertMatchesRegularExpression('/ORDER BY\s+st\.name\s+ASC,\s+st\.id\s+ASC/i', $dataSql); + $this->assertStringNotContainsString('DROP TABLE', $dataSql); + $this->assertMatchesRegularExpression('/LIMIT\s+100\s+OFFSET\s+0/i', $dataSql); + } + + public function testAllActiveReturnsNormalizedRows(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('select') + ->willReturn([ + [ + 'id' => '2', 'name' => 'InPost', 'status' => '1', + 'cost' => '9.99', 'max_wp' => '25', 'default' => '1', + 'delivery_free' => '0', 'apilo_carrier_account_id' => null, 'o' => '1', + ], + ]); + + $repository = new TransportRepository($mockDb); + $rows = $repository->allActive(); + + $this->assertCount(1, $rows); + $this->assertSame(2, $rows[0]['id']); + $this->assertSame(9.99, $rows[0]['cost']); + $this->assertSame(1, $rows[0]['status']); + $this->assertSame(1, $rows[0]['default']); + } + + public function testGetApiloCarrierAccountIdReturnsNullForInvalidId(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->never())->method('get'); + + $repository = new TransportRepository($mockDb); + $this->assertNull($repository->getApiloCarrierAccountId(0)); + } + + public function testGetApiloCarrierAccountIdReturnsIntOrNull(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->exactly(2)) + ->method('get') + ->willReturnOnConsecutiveCalls(null, '55'); + + $repository = new TransportRepository($mockDb); + $this->assertNull($repository->getApiloCarrierAccountId(1)); + $this->assertSame(55, $repository->getApiloCarrierAccountId(2)); + } + + public function testGetTransportCostReturnsFloatOrNull(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->exactly(2)) + ->method('get') + ->willReturnOnConsecutiveCalls(null, '12.50'); + + $repository = new TransportRepository($mockDb); + $this->assertNull($repository->getTransportCost(99)); + $this->assertSame(12.5, $repository->getTransportCost(1)); + } + + public function testAllForAdminReturnsAllTransports(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('select') + ->willReturn([ + ['id' => '1', 'name' => 'A', 'status' => '1', 'cost' => '5', 'max_wp' => null, 'default' => '0', 'delivery_free' => '0', 'apilo_carrier_account_id' => null, 'o' => '1'], + ['id' => '2', 'name' => 'B', 'status' => '0', 'cost' => '10', 'max_wp' => '50', 'default' => '1', 'delivery_free' => '1', 'apilo_carrier_account_id' => '3', 'o' => '2'], + ]); + + $repository = new TransportRepository($mockDb); + $rows = $repository->allForAdmin(); + + $this->assertCount(2, $rows); + $this->assertSame(0, $rows[1]['status']); + $this->assertSame(1, $rows[1]['default']); + $this->assertSame(50, $rows[1]['max_wp']); + } +} diff --git a/tests/Unit/admin/Controllers/ShopTransportControllerTest.php b/tests/Unit/admin/Controllers/ShopTransportControllerTest.php new file mode 100644 index 0000000..43a5100 --- /dev/null +++ b/tests/Unit/admin/Controllers/ShopTransportControllerTest.php @@ -0,0 +1,67 @@ +transportRepository = $this->createMock(TransportRepository::class); + $this->paymentMethodRepository = $this->createMock(PaymentMethodRepository::class); + $this->controller = new ShopTransportController( + $this->transportRepository, + $this->paymentMethodRepository + ); + } + + public function testConstructorAcceptsRepositories(): void + { + $controller = new ShopTransportController( + $this->transportRepository, + $this->paymentMethodRepository + ); + $this->assertInstanceOf(ShopTransportController::class, $controller); + } + + public function testHasMainActionMethods(): void + { + $this->assertTrue(method_exists($this->controller, 'list')); + $this->assertTrue(method_exists($this->controller, 'edit')); + $this->assertTrue(method_exists($this->controller, 'save')); + } + + public function testHasNoLegacyAliasMethods(): void + { + $this->assertFalse(method_exists($this->controller, 'view_list')); + $this->assertFalse(method_exists($this->controller, 'transport_edit')); + $this->assertFalse(method_exists($this->controller, 'transport_save')); + } + + public function testActionMethodReturnTypes(): void + { + $reflection = new \ReflectionClass($this->controller); + + $this->assertEquals('string', (string)$reflection->getMethod('list')->getReturnType()); + $this->assertEquals('string', (string)$reflection->getMethod('edit')->getReturnType()); + $this->assertEquals('void', (string)$reflection->getMethod('save')->getReturnType()); + } + + public function testConstructorRequiresBothRepositories(): void + { + $reflection = new \ReflectionClass(ShopTransportController::class); + $constructor = $reflection->getConstructor(); + $params = $constructor->getParameters(); + + $this->assertCount(2, $params); + $this->assertEquals('Domain\Transport\TransportRepository', $params[0]->getType()->getName()); + $this->assertEquals('Domain\PaymentMethod\PaymentMethodRepository', $params[1]->getType()->getName()); + } +}