From 40e777afe61439180b00a3a79b9313b97171f67e Mon Sep 17 00:00:00 2001 From: Jacek Pyziak Date: Sat, 14 Feb 2026 00:05:23 +0100 Subject: [PATCH] refactor(shop-coupon): migrate admin module to DI and release 0.266 --- .phpunit.result.cache | 2 +- AGENTS.md | 2 +- DATABASE_STRUCTURE.md | 22 + PROJECT_STRUCTURE.md | 11 + REFACTORING_PLAN.md | 95 ++++- TESTING.md | 20 +- admin/templates/layouts/layout-edit.php | 128 +++++- .../templates/layouts/subcategories-list.php | 5 +- admin/templates/layouts/subpages-list.php | 4 +- .../coupon-categories-selector.php | 19 + .../shop-coupon/coupon-categories-tree.php | 68 +++ .../shop-coupon/coupon-edit-custom-script.php | 109 +++++ .../templates/shop-coupon/coupon-edit-new.php | 3 + admin/templates/shop-coupon/coupon-edit.php | 151 ------- admin/templates/shop-coupon/coupons-list.php | 2 + admin/templates/shop-coupon/view-list.php | 88 ---- admin/templates/site/main-layout.php | 92 +++-- autoload/Domain/Article/ArticleRepository.php | 2 + autoload/Domain/Banner/BannerRepository.php | 2 + autoload/Domain/Coupon/CouponRepository.php | 391 ++++++++++++++++++ .../Dictionaries/DictionariesRepository.php | 1 + .../Integrations/IntegrationsRepository.php | 1 + autoload/Domain/Layouts/LayoutsRepository.php | 25 +- .../Newsletter/NewsletterRepository.php | 2 +- autoload/Domain/Pages/PagesRepository.php | 5 + .../Domain/Promotion/PromotionRepository.php | 2 + .../Domain/Settings/SettingsRepository.php | 1 + autoload/Domain/User/UserRepository.php | 2 + .../admin/Controllers/BannerController.php | 1 - .../Controllers/ShopCouponController.php | 353 ++++++++++++++++ autoload/admin/class.Site.php | 7 + autoload/admin/controls/class.ShopCoupon.php | 58 --- autoload/admin/factory/class.ShopCoupon.php | 54 --- index.php | 13 +- .../Domain/Coupon/CouponRepositoryTest.php | 268 ++++++++++++ .../Controllers/ShopCouponControllerTest.php | 65 +++ updates/0.20/ver_0.266.zip | Bin 0 -> 97410 bytes updates/0.20/ver_0.266_files.txt | 4 + updates/changelog.php | 14 +- updates/versions.php | 2 +- 40 files changed, 1668 insertions(+), 426 deletions(-) create mode 100644 admin/templates/shop-coupon/coupon-categories-selector.php create mode 100644 admin/templates/shop-coupon/coupon-categories-tree.php create mode 100644 admin/templates/shop-coupon/coupon-edit-custom-script.php create mode 100644 admin/templates/shop-coupon/coupon-edit-new.php delete mode 100644 admin/templates/shop-coupon/coupon-edit.php create mode 100644 admin/templates/shop-coupon/coupons-list.php delete mode 100644 admin/templates/shop-coupon/view-list.php create mode 100644 autoload/Domain/Coupon/CouponRepository.php create mode 100644 autoload/admin/Controllers/ShopCouponController.php delete mode 100644 autoload/admin/controls/class.ShopCoupon.php delete mode 100644 autoload/admin/factory/class.ShopCoupon.php create mode 100644 tests/Unit/Domain/Coupon/CouponRepositoryTest.php create mode 100644 tests/Unit/admin/Controllers/ShopCouponControllerTest.php create mode 100644 updates/0.20/ver_0.266.zip create mode 100644 updates/0.20/ver_0.266_files.txt diff --git a/.phpunit.result.cache b/.phpunit.result.cache index 1d17c8a..228c629 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.002,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testHasListMethod":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testListMethodReturnType":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testHasEditMethod":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testEditMethodReturnType":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testConstructorAcceptsRepository":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testConstructorRequiresArticleRepository":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testFindReturnsArticleWithRelations":0.004,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testFindReturnsNullWhenArticleDoesNotExist":0,"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,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveTranslationsUpsertsForExistingArticle":0.001,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSavePagesForNewArticle":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveDeletesMarkedImagesOnUpdate":0.001,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testArchiveSetsStatusToMinusOne":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testArchiveReturnsFalseWhenUpdateFails":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveGalleryOrderUpdatesImageOrder":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveGalleryOrderSkipsEmptyValues":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testHasBrowseListMethod":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testHasGalleryOrderSaveMethod":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testBrowseListMethodReturnType":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testGalleryOrderSaveMethodReturnType":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testListForAdminWhitelistsSortAndDirection":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testListForAdminUsesBoundParamsForTitleFilter":0,"Tests\\Unit\\Domain\\Banner\\BannerRepositoryTest::testSaveWithLegacyFormat":0,"Tests\\Unit\\Domain\\Banner\\BannerRepositoryTest::testSaveUpdatesExistingTranslationsByBannerAndLang":0,"Tests\\Unit\\Domain\\Banner\\BannerRepositoryTest::testListForAdminIncludesThumbnailSrc":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testRestoreSetsStatusToZero":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testDeletePermanentlyRemovesArticleAndRelations":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testListArchivedForAdminWhitelistsSortAndDirection":0,"Tests\\Unit\\Domain\\Dictionaries\\DictionariesRepositoryTest::testFindReturnsUnitWithTranslations":0.001,"Tests\\Unit\\Domain\\Dictionaries\\DictionariesRepositoryTest::testFindReturnsNullWhenUnitNotFound":0,"Tests\\Unit\\Domain\\Dictionaries\\DictionariesRepositoryTest::testSaveInsertsNewUnitAndTranslationsForStringLanguageId":0,"Tests\\Unit\\Domain\\Dictionaries\\DictionariesRepositoryTest::testDeleteRemovesUnitAndTranslations":0,"Tests\\Unit\\Domain\\Dictionaries\\DictionariesRepositoryTest::testGetUnitNameByIdReturnsTextFromDatabase":0,"Tests\\Unit\\Domain\\Dictionaries\\DictionariesRepositoryTest::testGetUnitNameByIdSupportsStringLanguageId":0,"Tests\\Unit\\Domain\\Dictionaries\\DictionariesRepositoryTest::testAllUnitsReturnsArrayIndexedById":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testLanguageDetailsReturnsArrayOrNull":0.001,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testLanguagesListReturnsArray":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testSaveLanguageRejectsInvalidLanguageId":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testSaveTranslationInsertsNewTranslationAndReturnsId":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testDeleteTranslationReturnsBoolean":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testListForAdminReturnsItemsAndTotal":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testDefaultLanguageIdReturnsLanguageWithStartFlag":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testDefaultLanguageIdFallsBackToFirstLanguageOrPl":0,"Tests\\Unit\\Domain\\Layouts\\LayoutsRepositoryTest::testFindReturnsLayoutWithRelations":0.001,"Tests\\Unit\\Domain\\Layouts\\LayoutsRepositoryTest::testDeleteReturnsFalseWhenOnlyOneLayoutExists":0,"Tests\\Unit\\Domain\\Layouts\\LayoutsRepositoryTest::testFindReturnsDefaultLayoutWhenRecordDoesNotExist":0,"Tests\\Unit\\Domain\\Layouts\\LayoutsRepositoryTest::testSaveInsertsNewLayoutAndReturnsId":0,"Tests\\Unit\\Domain\\Layouts\\LayoutsRepositoryTest::testListAllReturnsArray":0,"Tests\\Unit\\Domain\\Newsletter\\NewsletterRepositoryTest::testTemplateDetailsReturnsNullForInvalidId":0.002,"Tests\\Unit\\Domain\\Newsletter\\NewsletterRepositoryTest::testTemplateDetailsReturnsArray":0,"Tests\\Unit\\Domain\\Newsletter\\NewsletterRepositoryTest::testSaveSettingsUpdatesHeaderAndFooter":0,"Tests\\Unit\\Domain\\Newsletter\\NewsletterRepositoryTest::testDeleteTemplateReturnsFalseForAdminTemplate":0,"Tests\\Unit\\Domain\\Newsletter\\NewsletterRepositoryTest::testTemplateByNameReturnsText":0,"Tests\\Unit\\Domain\\Scontainers\\ScontainersRepositoryTest::testFindReturnsDefaultContainerForInvalidId":0.001,"Tests\\Unit\\Domain\\Scontainers\\ScontainersRepositoryTest::testDeleteReturnsFalseForInvalidId":0,"Tests\\Unit\\Domain\\Scontainers\\ScontainersRepositoryTest::testFindReturnsContainerWithTranslations":0,"Tests\\Unit\\Domain\\Scontainers\\ScontainersRepositoryTest::testDetailsForLanguageReturnsNullForInvalidData":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testFindReturnsUserWhenExists":0.001,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testFindReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testCheckLoginReturnsErrorWhenLoginIsTaken":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testCheckLoginReturnsOkWhenAvailable":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testSaveReturnsErrorForTooShortPasswordOnCreate":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testSaveReturnsErrorForMismatchedPasswordsOnCreate":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testSaveCreatesUserWithNormalizedSwitches":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testSaveUpdatesExistingUserWithPassword":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testSaveUpdatesExistingUserWithoutPassword":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testSaveReturnsErrorForTooShortPasswordOnUpdate":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testSaveReturnsErrorForMismatchedPasswordsOnUpdate":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testDeleteReturnsTrue":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testDeleteReturnsFalseOnFailure":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testDetailsReturnsUserByLogin":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testDetailsReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testLogonReturnsSuccessForValidCredentials":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testLogonReturnsZeroForNonexistentUser":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testLogonReturnsNegativeOneForBlockedUser":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testVerifyTwofaCodeReturnsFalseForNonexistentUser":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testVerifyTwofaCodeReturnsFalseAfterMaxAttempts":0.078,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testVerifyTwofaCodeReturnsFalseForExpiredCode":0.079,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testVerifyTwofaCodeReturnsTrueForValidCode":0.158,"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}} \ 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.002,"Tests\\Unit\\Domain\\Banner\\BannerRepositoryTest::testFindReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\Banner\\BannerRepositoryTest::testDeleteReturnsTrue":0.001,"Tests\\Unit\\Domain\\Banner\\BannerRepositoryTest::testSaveInsertsNewBanner":0,"Tests\\Unit\\Domain\\Cache\\CacheRepositoryTest::testClearCacheWithRedis":0.001,"Tests\\Unit\\Domain\\Cache\\CacheRepositoryTest::testClearCacheRedisUnavailable":0,"Tests\\Unit\\Domain\\Cache\\CacheRepositoryTest::testClearCacheWithoutRedis":0,"Tests\\Unit\\Domain\\Cache\\CacheRepositoryTest::testClearCacheReturnStructure":0,"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testCanBeInstantiated":0,"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testHasSaveSettingsMethod":0,"Tests\\Unit\\Domain\\Settings\\SettingsRepositoryTest::testHasGetSettingsMethod":0,"Tests\\Unit\\admin\\Controllers\\SettingsControllerTest::testConstructorAcceptsRepository":0.001,"Tests\\Unit\\admin\\Controllers\\SettingsControllerTest::testHasClearCacheMethod":0,"Tests\\Unit\\admin\\Controllers\\SettingsControllerTest::testHasClearCacheAjaxMethod":0,"Tests\\Unit\\admin\\Controllers\\SettingsControllerTest::testHasSaveMethod":0,"Tests\\Unit\\admin\\Controllers\\SettingsControllerTest::testHasViewMethod":0,"Tests\\Unit\\admin\\Controllers\\SettingsControllerTest::testIsNotAbstract":0,"Tests\\Unit\\admin\\Controllers\\SettingsControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testCanCreateController":0.004,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testHasListMethod":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testListMethodReturnType":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testHasEditMethod":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testEditMethodReturnType":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testConstructorAcceptsRepository":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testConstructorRequiresArticleRepository":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testFindReturnsArticleWithRelations":0.004,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testFindReturnsNullWhenArticleDoesNotExist":0.001,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testDeleteNonassignedFilesDeletesDbRows":0.001,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testDeleteNonassignedImagesDeletesDbRows":0.001,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveCreatesNewArticle":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveReturnsZeroWhenInsertFails":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveUpdatesExistingArticle":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveTranslationsInsertsForNewArticle":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveTranslationsUpsertsForExistingArticle":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSavePagesForNewArticle":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveDeletesMarkedImagesOnUpdate":0.001,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testArchiveSetsStatusToMinusOne":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testArchiveReturnsFalseWhenUpdateFails":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveGalleryOrderUpdatesImageOrder":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveGalleryOrderSkipsEmptyValues":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testHasBrowseListMethod":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testHasGalleryOrderSaveMethod":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testBrowseListMethodReturnType":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testGalleryOrderSaveMethodReturnType":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testListForAdminWhitelistsSortAndDirection":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testListForAdminUsesBoundParamsForTitleFilter":0,"Tests\\Unit\\Domain\\Banner\\BannerRepositoryTest::testSaveWithLegacyFormat":0,"Tests\\Unit\\Domain\\Banner\\BannerRepositoryTest::testSaveUpdatesExistingTranslationsByBannerAndLang":0,"Tests\\Unit\\Domain\\Banner\\BannerRepositoryTest::testListForAdminIncludesThumbnailSrc":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testRestoreSetsStatusToZero":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testDeletePermanentlyRemovesArticleAndRelations":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testListArchivedForAdminWhitelistsSortAndDirection":0,"Tests\\Unit\\Domain\\Dictionaries\\DictionariesRepositoryTest::testFindReturnsUnitWithTranslations":0.001,"Tests\\Unit\\Domain\\Dictionaries\\DictionariesRepositoryTest::testFindReturnsNullWhenUnitNotFound":0,"Tests\\Unit\\Domain\\Dictionaries\\DictionariesRepositoryTest::testSaveInsertsNewUnitAndTranslationsForStringLanguageId":0,"Tests\\Unit\\Domain\\Dictionaries\\DictionariesRepositoryTest::testDeleteRemovesUnitAndTranslations":0,"Tests\\Unit\\Domain\\Dictionaries\\DictionariesRepositoryTest::testGetUnitNameByIdReturnsTextFromDatabase":0,"Tests\\Unit\\Domain\\Dictionaries\\DictionariesRepositoryTest::testGetUnitNameByIdSupportsStringLanguageId":0,"Tests\\Unit\\Domain\\Dictionaries\\DictionariesRepositoryTest::testAllUnitsReturnsArrayIndexedById":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testLanguageDetailsReturnsArrayOrNull":0.001,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testLanguagesListReturnsArray":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testSaveLanguageRejectsInvalidLanguageId":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testSaveTranslationInsertsNewTranslationAndReturnsId":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testDeleteTranslationReturnsBoolean":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testListForAdminReturnsItemsAndTotal":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testDefaultLanguageIdReturnsLanguageWithStartFlag":0,"Tests\\Unit\\Domain\\Languages\\LanguagesRepositoryTest::testDefaultLanguageIdFallsBackToFirstLanguageOrPl":0,"Tests\\Unit\\Domain\\Layouts\\LayoutsRepositoryTest::testFindReturnsLayoutWithRelations":0.001,"Tests\\Unit\\Domain\\Layouts\\LayoutsRepositoryTest::testDeleteReturnsFalseWhenOnlyOneLayoutExists":0,"Tests\\Unit\\Domain\\Layouts\\LayoutsRepositoryTest::testFindReturnsDefaultLayoutWhenRecordDoesNotExist":0,"Tests\\Unit\\Domain\\Layouts\\LayoutsRepositoryTest::testSaveInsertsNewLayoutAndReturnsId":0,"Tests\\Unit\\Domain\\Layouts\\LayoutsRepositoryTest::testListAllReturnsArray":0,"Tests\\Unit\\Domain\\Newsletter\\NewsletterRepositoryTest::testTemplateDetailsReturnsNullForInvalidId":0.002,"Tests\\Unit\\Domain\\Newsletter\\NewsletterRepositoryTest::testTemplateDetailsReturnsArray":0,"Tests\\Unit\\Domain\\Newsletter\\NewsletterRepositoryTest::testSaveSettingsUpdatesHeaderAndFooter":0,"Tests\\Unit\\Domain\\Newsletter\\NewsletterRepositoryTest::testDeleteTemplateReturnsFalseForAdminTemplate":0,"Tests\\Unit\\Domain\\Newsletter\\NewsletterRepositoryTest::testTemplateByNameReturnsText":0,"Tests\\Unit\\Domain\\Scontainers\\ScontainersRepositoryTest::testFindReturnsDefaultContainerForInvalidId":0.001,"Tests\\Unit\\Domain\\Scontainers\\ScontainersRepositoryTest::testDeleteReturnsFalseForInvalidId":0,"Tests\\Unit\\Domain\\Scontainers\\ScontainersRepositoryTest::testFindReturnsContainerWithTranslations":0,"Tests\\Unit\\Domain\\Scontainers\\ScontainersRepositoryTest::testDetailsForLanguageReturnsNullForInvalidData":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testFindReturnsUserWhenExists":0.001,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testFindReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testCheckLoginReturnsErrorWhenLoginIsTaken":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testCheckLoginReturnsOkWhenAvailable":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testSaveReturnsErrorForTooShortPasswordOnCreate":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testSaveReturnsErrorForMismatchedPasswordsOnCreate":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testSaveCreatesUserWithNormalizedSwitches":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testSaveUpdatesExistingUserWithPassword":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testSaveUpdatesExistingUserWithoutPassword":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testSaveReturnsErrorForTooShortPasswordOnUpdate":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testSaveReturnsErrorForMismatchedPasswordsOnUpdate":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testDeleteReturnsTrue":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testDeleteReturnsFalseOnFailure":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testDetailsReturnsUserByLogin":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testDetailsReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testLogonReturnsSuccessForValidCredentials":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testLogonReturnsZeroForNonexistentUser":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testLogonReturnsNegativeOneForBlockedUser":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testVerifyTwofaCodeReturnsFalseForNonexistentUser":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testVerifyTwofaCodeReturnsFalseAfterMaxAttempts":0.08,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testVerifyTwofaCodeReturnsFalseForExpiredCode":0.081,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testVerifyTwofaCodeReturnsTrueForValidCode":0.155,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testSendTwofaCodeReturnsFalseWhen2FADisabled":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testSendTwofaCodeReturnsFalseForInvalidEmail":0.001,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testUpdateByIdCallsDbUpdate":0,"Tests\\Unit\\Domain\\User\\UserRepositoryTest::testListForAdminReturnsItemsAndTotal":0.001,"Tests\\Unit\\admin\\Controllers\\ArticlesArchiveControllerTest::testConstructorAcceptsRepository":0.002,"Tests\\Unit\\admin\\Controllers\\ArticlesArchiveControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\admin\\Controllers\\ArticlesArchiveControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\ArticlesArchiveControllerTest::testConstructorRequiresArticleRepository":0,"Tests\\Unit\\admin\\Controllers\\DictionariesControllerTest::testConstructorAcceptsRepository":0.002,"Tests\\Unit\\admin\\Controllers\\DictionariesControllerTest::testHasListMethod":0,"Tests\\Unit\\admin\\Controllers\\DictionariesControllerTest::testHasEditMethod":0,"Tests\\Unit\\admin\\Controllers\\DictionariesControllerTest::testHasSaveMethod":0,"Tests\\Unit\\admin\\Controllers\\DictionariesControllerTest::testHasDeleteMethod":0,"Tests\\Unit\\admin\\Controllers\\DictionariesControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\DictionariesControllerTest::testConstructorRequiresDictionariesRepository":0,"Tests\\Unit\\admin\\Controllers\\LanguagesControllerTest::testConstructorAcceptsRepository":0.001,"Tests\\Unit\\admin\\Controllers\\LanguagesControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\admin\\Controllers\\LanguagesControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\LanguagesControllerTest::testConstructorRequiresLanguagesRepository":0,"Tests\\Unit\\admin\\Controllers\\LayoutsControllerTest::testConstructorAcceptsRepository":0.001,"Tests\\Unit\\admin\\Controllers\\LayoutsControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\admin\\Controllers\\LayoutsControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\LayoutsControllerTest::testConstructorRequiresLayoutsRepository":0,"Tests\\Unit\\admin\\Controllers\\NewsletterControllerTest::testConstructorAcceptsDependencies":0.002,"Tests\\Unit\\admin\\Controllers\\NewsletterControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\admin\\Controllers\\NewsletterControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\NewsletterControllerTest::testConstructorRequiresRepositoryAndRenderer":0,"Tests\\Unit\\admin\\Controllers\\ScontainersControllerTest::testConstructorAcceptsDependencies":0.001,"Tests\\Unit\\admin\\Controllers\\ScontainersControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\admin\\Controllers\\ScontainersControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\ScontainersControllerTest::testConstructorRequiresRepositoryAndLanguagesRepository":0,"Tests\\Unit\\admin\\Controllers\\UsersControllerTest::testConstructorAcceptsRepository":0.002,"Tests\\Unit\\admin\\Controllers\\UsersControllerTest::testHasViewListMethod":0,"Tests\\Unit\\admin\\Controllers\\UsersControllerTest::testHasUserEditMethod":0,"Tests\\Unit\\admin\\Controllers\\UsersControllerTest::testHasUserSaveMethod":0,"Tests\\Unit\\admin\\Controllers\\UsersControllerTest::testHasUserDeleteMethod":0,"Tests\\Unit\\admin\\Controllers\\UsersControllerTest::testHasTwofaMethod":0,"Tests\\Unit\\admin\\Controllers\\UsersControllerTest::testHasLoginFormMethod":0,"Tests\\Unit\\admin\\Controllers\\UsersControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\UsersControllerTest::testConstructorRequiresUserRepository":0,"Tests\\Unit\\admin\\Controllers\\UsersControllerTest::testNormalizeUserReturnsDefaultsForNull":0,"Tests\\Unit\\admin\\Controllers\\UsersControllerTest::testNormalizeUserCastsTypes":0,"Tests\\Unit\\admin\\Controllers\\UsersControllerTest::testNormalizeUserHandlesPartialData":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveFilesOrderUpdatesFilesOrder":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testSaveFilesOrderSkipsEmptyValues":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testPagesSummaryForArticlesBuildsLabels":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testUpdateImageAltDelegatesToDatabase":0,"Tests\\Unit\\Domain\\Article\\ArticleRepositoryTest::testMarkFileToDeleteDelegatesToDatabase":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testFindReturnsDefaultCouponForInvalidId":0.001,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testFindNormalizesCouponData":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testSaveInsertsCouponAndReturnsId":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testSaveUpdatesCouponAndReturnsId":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testDeleteReturnsFalseForInvalidId":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testDeleteReturnsTrueWhenDatabaseDeleteSucceeds":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testListForAdminWhitelistsSortAndDirection":0,"Tests\\Unit\\Domain\\Coupon\\CouponRepositoryTest::testCategoriesTreeReturnsHierarchy":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testGetSettingsReturnsArray":0.001,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testGetSettingReturnsValue":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testGetSettingReturnsNullWhenNotFound":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testSaveSettingUpdatesExistingValue":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testSaveSettingInsertsNewValue":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testInvalidProviderThrowsException":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testLinkProductUpdatesDatabase":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testUnlinkProductClearsFields":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testGetProductSkuReturnsValue":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testGetProductSkuReturnsNullForMissing":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testApiloGetAccessTokenReturnsNullWithoutSettings":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testApiloFetchListThrowsForInvalidType":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testAllPublicMethodsExist":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testSettingsTableMapping":0,"Tests\\Unit\\Domain\\Integrations\\IntegrationsRepositoryTest::testShopproProviderWorks":0,"Tests\\Unit\\Domain\\Pages\\PagesRepositoryTest::testMenusListReturnsArray":0.001,"Tests\\Unit\\Domain\\Pages\\PagesRepositoryTest::testMenuDeleteReturnsFalseWhenMenuHasPages":0,"Tests\\Unit\\Domain\\Pages\\PagesRepositoryTest::testGenerateSeoLinkAddsSuffixWhenBaseSlugExists":0,"Tests\\Unit\\Domain\\Pages\\PagesRepositoryTest::testPageUrlPreviewBuildsLanguagePrefixedUrlForNonDefaultLanguage":0,"Tests\\Unit\\Domain\\Promotion\\PromotionRepositoryTest::testFindReturnsDefaultPromotionForInvalidId":0.001,"Tests\\Unit\\Domain\\Promotion\\PromotionRepositoryTest::testSaveInsertsPromotionAndReturnsId":0,"Tests\\Unit\\Domain\\Promotion\\PromotionRepositoryTest::testDeleteReturnsFalseForInvalidId":0,"Tests\\Unit\\Domain\\Promotion\\PromotionRepositoryTest::testDeleteReturnsTrueWhenDatabaseDeleteSucceeds":0,"Tests\\Unit\\Domain\\Promotion\\PromotionRepositoryTest::testListForAdminWhitelistsSortAndDirection":0,"Tests\\Unit\\Domain\\Promotion\\PromotionRepositoryTest::testCategoriesTreeReturnsHierarchy":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testHasImageAltChangeMethod":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testHasFileNameChangeMethod":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testHasImageDeleteMethod":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testHasFileDeleteMethod":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testImageAltChangeMethodReturnType":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testFileNameChangeMethodReturnType":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testImageDeleteMethodReturnType":0,"Tests\\Unit\\admin\\Controllers\\ArticlesControllerTest::testFileDeleteMethodReturnType":0,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testConstructorAcceptsDependencies":0.002,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testConstructorRequiresRepository":0,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testHasAllApiloSettingsMethods":0,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testHasAllApiloDataFetchMethods":0,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testHasAllApiloProductMethods":0,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testHasAllShopproMethods":0,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testApiloSettingsReturnsString":0,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testShopproSettingsReturnsString":0,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testVoidReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testDoesNotHaveSellasistMethods":0,"Tests\\Unit\\admin\\Controllers\\IntegrationsControllerTest::testDoesNotHaveBaselinkerMethods":0,"Tests\\Unit\\admin\\Controllers\\PagesControllerTest::testConstructorAcceptsRepositories":0.001,"Tests\\Unit\\admin\\Controllers\\PagesControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\admin\\Controllers\\PagesControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\PagesControllerTest::testConstructorRequiresPagesLanguagesAndLayoutsRepositories":0,"Tests\\Unit\\admin\\Controllers\\ShopCouponControllerTest::testConstructorAcceptsRepository":0.001,"Tests\\Unit\\admin\\Controllers\\ShopCouponControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopCouponControllerTest::testHasLegacyAliasMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopCouponControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\ShopCouponControllerTest::testConstructorRequiresCouponRepository":0,"Tests\\Unit\\admin\\Controllers\\ShopPromotionControllerTest::testConstructorAcceptsRepository":0.001,"Tests\\Unit\\admin\\Controllers\\ShopPromotionControllerTest::testHasMainActionMethods":0,"Tests\\Unit\\admin\\Controllers\\ShopPromotionControllerTest::testActionMethodReturnTypes":0,"Tests\\Unit\\admin\\Controllers\\ShopPromotionControllerTest::testConstructorRequiresPromotionRepository":0}} \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index 66c39d3..3c92d26 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -28,4 +28,4 @@ To ma pomóc zachować spójność zmian i dokumentacji. ## INNE -Przejdźmy teraz do refaktoringu wszystkiego co związane z https://shoppro.project-dc.pl/admin/articles_archive/, nowe widoki, klasy (usuwanie starych), poprawa routingu, przeszukanie innych klas pod względem zależności. Zapisz plan i przedstaw mi go a po akceptacji realizuj krok po kroku w trybie Human In The Loop \ No newline at end of file +Przejdźmy teraz do refaktoringu wszystkiego co związane z https://shoppro.project-dc.pl/admin/shop_coupon/, nowe widoki, klasy (usuwanie starych), poprawa routingu, przeszukanie innych klas pod względem zależności. Zapisz plan i przedstaw mi go a po akceptacji realizuj krok po kroku w trybie Human In The Loop \ No newline at end of file diff --git a/DATABASE_STRUCTURE.md b/DATABASE_STRUCTURE.md index 69ed2b1..385f34c 100644 --- a/DATABASE_STRUCTURE.md +++ b/DATABASE_STRUCTURE.md @@ -320,6 +320,28 @@ Tlumaczenia kontenerow statycznych (per jezyk). **Aktualizacja 2026-02-12 (ver. 0.260):** modul `/admin/articles_archive` korzysta z `Domain\Article\ArticleRepository` (`listArchivedForAdmin`, `restore`, `deletePermanently`) przez `admin\Controllers\ArticlesArchiveController`. +## pp_shop_coupon +Kody rabatowe sklepu (modul `/admin/shop_coupon`). + +| Kolumna | Opis | +|---------|------| +| id | PK | +| name | Kod kuponu (UNIQUE) | +| status | Status: 1 = aktywny, 0 = nieaktywny | +| send | Czy kupon zostal wyslany (0/1) | +| used | Czy kupon zostal wykorzystany (0/1) | +| date_used | Data wykorzystania kuponu (NULL gdy brak) | +| used_count | Licznik uzyc kuponu | +| type | Typ kuponu (obecnie: 1 = rabat procentowy na koszyk) | +| amount | Wartosc kuponu (np. procent) | +| one_time | Czy kupon jednorazowy (0/1) | +| include_discounted_product | Czy obejmuje rowniez produkty przecenione (0/1) | +| categories | JSON z ID kategorii objetych kuponem (NULL = bez ograniczenia) | + +**Uzywane w:** `Domain\Coupon\CouponRepository`, `admin\Controllers\ShopCouponController`, `shop\Coupon`, `front\factory\ShopCoupon`, `front\factory\ShopOrder` + +**Aktualizacja 2026-02-13 (ver. 0.266):** modul `/admin/shop_coupon` korzysta z `Domain\Coupon\CouponRepository` przez `admin\Controllers\ShopCouponController`. Usunieto legacy klasy `admin\controls\ShopCoupon` i `admin\factory\ShopCoupon`. + ## pp_shop_promotion Promocje sklepu (modul `/admin/shop_promotion`). diff --git a/PROJECT_STRUCTURE.md b/PROJECT_STRUCTURE.md index 819c691..7df694b 100644 --- a/PROJECT_STRUCTURE.md +++ b/PROJECT_STRUCTURE.md @@ -492,3 +492,14 @@ Aktualnie w suite są też testy modułów `Dictionaries`, `Articles` i `Users` - UPDATE: `shop\Promotion::get_active_promotions()` uwzglednia `date_from` (`NULL` lub `<= dzisiaj`) obok `date_to`. - FIX: edycja promocji zapisuje update zamiast insert (stabilne przekazanie `id` przez hidden field + fallback `id` z URL w `save()`). - Testy: **OK (222 tests, 614 assertions)**. + +## Aktualizacja 2026-02-13 (ShopCoupon refactor, ver. 0.266) +- NOWE: `Domain\Coupon\CouponRepository` (`listForAdmin`, `find`, `save`, `delete`, `categoriesTree`). +- NOWE: `admin\Controllers\ShopCouponController` (DI) dla akcji `list`, `edit`, `save`, `delete`. +- UPDATE: zachowana kompatybilnosc aliasow legacy akcji (`view_list`, `coupon_edit`, `coupon_save`, `coupon_delete`) w nowym kontrolerze. +- UPDATE: modul `/admin/shop_coupon/*` przepiety z legacy `grid/gridEdit` na `components/table-list` i `components/form-edit`. +- NOWE: widoki/partiale `shop-coupon/coupons-list`, `shop-coupon/coupon-edit-new`, `shop-coupon/coupon-categories-selector`, `shop-coupon/coupon-categories-tree`, `shop-coupon/coupon-edit-custom-script`. +- CLEANUP: usuniete legacy klasy/pliki `autoload/admin/controls/class.ShopCoupon.php`, `autoload/admin/factory/class.ShopCoupon.php`, `admin/templates/shop-coupon/view-list.php`, `admin/templates/shop-coupon/coupon-edit.php`. +- UPDATE: menu admin wskazuje kanoniczny URL `/admin/shop_coupon/list/`. +- FIX: ujednolicone zachowanie drzewek i styl checkboxow miedzy widokami `/admin/shop_coupon/edit/*` i `/admin/layouts/edit/*` (strzalki, focus, iCheck). +- Testy: **OK (235 tests, 682 assertions)**. diff --git a/REFACTORING_PLAN.md b/REFACTORING_PLAN.md index c0118bf..7aa43de 100644 --- a/REFACTORING_PLAN.md +++ b/REFACTORING_PLAN.md @@ -511,15 +511,15 @@ class BannerController { $formHandler = new FormRequestHandler(); $viewModel = $this->buildFormViewModel(); // jak w edit() - + $result = $formHandler->handleSubmit($viewModel, $_POST); - + if (!$result['success']) { // Błędy walidacji - zapisane automatycznie do sesji echo json_encode(['success' => false, 'errors' => $result['errors']]); exit; } - + // Sukces - persist wyczyszczony automatycznie $this->repository->save($result['data']); echo json_encode(['success' => true]); @@ -782,3 +782,92 @@ Gdy `persist = true`: - FIX: zapis edycji promocji nie tworzy nowego rekordu (hidden `id` + fallback `id` z URL) - TEST: rozszerzono `PromotionRepositoryTest` o asercje `date_from` - Testy po zmianie: **OK (222 tests, 614 assertions)**. + +## Plan 2026-02-13 - Refaktoryzacja `/admin/shop_coupon/` (HITL) +- [x] Etap 1 (analiza i kontrakt URL/routingu): + - potwierdzic docelowy kontrakt URL: `/admin/shop_coupon/list/`, `/admin/shop_coupon/edit/`, `/admin/shop_coupon/save/`, `/admin/shop_coupon/delete/` + - decyzja: utrzymujemy aliasy legacy (`view_list`, `coupon_edit`, `coupon_save`, `coupon_delete`) w nowym kontrolerze jako kompatybilnosc wsteczna, przy jednoczesnym przejsciu menu i nowych widokow na URL kanoniczne + - sprawdzic mapowanie modulu `ShopCoupon` w `admin\Site` (DI factory + fallback) +- [x] Etap 2 (Domain): + - dodac `Domain\Coupon\CouponRepository`: + - `listForAdmin(filters, sort, dir, page, perPage)` (whitelist sortowania + paginacja) + - `find(int $id)` (domyslne dane dla nowego formularza) + - `save(array $data): ?int` (insert/update, normalizacja switchy, JSON dla `categories`) + - `delete(int $id): bool` + - `categoriesTree(?int $parentId): array` (drzewo kategorii bez zaleznosci od `admin\factory\ShopCategory`) +- [x] Etap 3 (Admin Controller + routing DI): + - dodac `admin\Controllers\ShopCouponController` z akcjami `list`, `edit`, `save`, `delete` + - przepiac routing DI w `admin\Site::$newControllers` dla modulu `ShopCoupon` + - zachowac obsluge legacy payload `values` JSON i nowego payload `$_POST` z `components/form-edit` +- [x] Etap 4 (widoki): + - przepiac liste z `grid` na `components/table-list` (filtry: nazwa, aktywny, uzyty, wyslany) + - przepiac edycje z `gridEdit` na `components/form-edit` + - dodac partiale drzewa kategorii w module `shop-coupon` (usuniecie zaleznosci od `shop-product/subcategories-list`) + - dodac `shop-coupon/coupon-edit-custom-script.php` (obsluga drzewa kategorii i zachowania formularza) +- [x] Etap 5 (cleanup i zaleznosci): + - usunac legacy po pelnym przepieciu: + - `autoload/admin/controls/class.ShopCoupon.php` + - `autoload/admin/factory/class.ShopCoupon.php` + - `admin/templates/shop-coupon/view-list.php` (wersja grid) + - `admin/templates/shop-coupon/coupon-edit.php` (wersja gridEdit) + - przepiac menu admin na kanoniczny URL `/admin/shop_coupon/list/` + - przeszukac repo i usunac pozostale odwolania do `shop_coupon/view_list` i legacy klas `admin\controls\ShopCoupon`, `admin\factory\ShopCoupon` +- [x] Etap 6 (testy): + - dodac `tests/Unit/Domain/Coupon/CouponRepositoryTest.php` + - dodac `tests/Unit/admin/Controllers/ShopCouponControllerTest.php` + - uruchomic testy modulu + pelny `composer test` +- [x] Etap 7 (dokumentacja i release note): + - zaktualizowac `DATABASE_STRUCTURE.md` (dodac `pp_shop_coupon`) + - zaktualizowac `PROJECT_STRUCTURE.md` + - zaktualizowac `REFACTORING_PLAN.md` (sekcja "Aktualizacja ...") + - zaktualizowac `TESTING.md` (nowy wynik suite + nowe testy) + - dopisac wpis w `updates/changelog.php` + +### Tryb HITL dla realizacji +- Po kazdym etapie (1-7) zatrzymanie i krotkie podsumowanie diffu do akceptacji przed kolejnym krokiem. + +### Postep 2026-02-13 (ShopCoupon) +- Etap 2 zakonczony: + - NOWE: `autoload/Domain/Coupon/CouponRepository.php` + - Zakres: `listForAdmin`, `find`, `save`, `delete`, `categoriesTree` + - Walidacja: `php -l` OK +- Etap 3 zakonczony: + - NOWE: `autoload/admin/Controllers/ShopCouponController.php` + - UPDATE: `autoload/admin/class.Site.php` - rejestracja DI factory dla modulu `ShopCoupon` + - Kompatybilnosc: dodane aliasy akcji `view_list`, `coupon_edit`, `coupon_save`, `coupon_delete` + - Walidacja: `php -l` OK +- Etap 4 zakonczony: + - NOWE widoki: `admin/templates/shop-coupon/coupons-list.php`, `admin/templates/shop-coupon/coupon-edit-new.php` + - NOWE partiale: `admin/templates/shop-coupon/coupon-categories-selector.php`, `admin/templates/shop-coupon/coupon-categories-tree.php` + - NOWY skrypt: `admin/templates/shop-coupon/coupon-edit-custom-script.php` + - UPDATE: `ShopCouponController::edit()` buduje `FormEditViewModel` (zakladki ustawienia/kategorie) + - Walidacja: `php -l` OK +- Etap 5 zakonczony: + - CLEANUP: usuniete pliki legacy: + - `autoload/admin/controls/class.ShopCoupon.php` + - `autoload/admin/factory/class.ShopCoupon.php` + - `admin/templates/shop-coupon/view-list.php` + - `admin/templates/shop-coupon/coupon-edit.php` + - UPDATE: menu admin (`admin/templates/site/main-layout.php`) wskazuje kanoniczny URL `/admin/shop_coupon/list/` + - WERYFIKACJA: brak odwolan do `shop_coupon/view_list`, `admin\controls\ShopCoupon`, `admin\factory\ShopCoupon` w kodzie +- Etap 6 zakonczony: + - NOWE testy: + - `tests/Unit/Domain/Coupon/CouponRepositoryTest.php` (8 testow) + - `tests/Unit/admin/Controllers/ShopCouponControllerTest.php` (5 testow) + - Test modulu: `OK (8 tests, 49 assertions)` + - Pelny suite: `OK (235 tests, 682 assertions)` +- Etap 7 zakonczony: + - UPDATE: dokumentacja techniczna zaktualizowana (`DATABASE_STRUCTURE.md`, `PROJECT_STRUCTURE.md`, `TESTING.md`) + - UPDATE: dopisany release note w `updates/changelog.php` (ver. 0.266) + +## Aktualizacja 2026-02-13 (ver. 0.266) +- **ShopCoupon** - migracja `/admin/shop_coupon` na Domain + DI + nowe widoki + - NOWE: `Domain\Coupon\CouponRepository` (`listForAdmin`, `find`, `save`, `delete`, `categoriesTree`) + - NOWE: `admin\Controllers\ShopCouponController` (DI) z akcjami `list`, `edit`, `save`, `delete` + - UPDATE: kompatybilnosc aliasow legacy (`view_list`, `coupon_edit`, `coupon_save`, `coupon_delete`) obslugiwana przez nowy kontroler + - UPDATE: modul `/admin/shop_coupon/*` dziala na `components/table-list` i `components/form-edit` + - NOWE: widoki/partiale `shop-coupon/coupons-list`, `shop-coupon/coupon-edit-new`, `shop-coupon/coupon-categories-selector`, `shop-coupon/coupon-categories-tree`, `shop-coupon/coupon-edit-custom-script` + - CLEANUP: usuniete legacy `autoload/admin/controls/class.ShopCoupon.php`, `autoload/admin/factory/class.ShopCoupon.php`, `admin/templates/shop-coupon/view-list.php`, `admin/templates/shop-coupon/coupon-edit.php` + - UPDATE: menu admin przepiete na kanoniczny URL `/admin/shop_coupon/list/` + - FIX: po akceptacji HITL ujednolicone UI drzewek i checkboxow miedzy kuponami i layoutami (spojne strzalki, brak nieestetycznego focusu, iCheck dla checkboxow) +- Testy po zmianie: **OK (235 tests, 682 assertions)**. diff --git a/TESTING.md b/TESTING.md index 11d99f9..d7675b0 100644 --- a/TESTING.md +++ b/TESTING.md @@ -33,10 +33,10 @@ Alternatywnie (Git Bash): ## Aktualny stan suite -Ostatnio zweryfikowano: 2026-02-10 +Ostatnio zweryfikowano: 2026-02-13 ```text -OK (82 tests, 181 assertions) +OK (235 tests, 682 assertions) ``` ## Struktura testow @@ -49,6 +49,7 @@ tests/ | | |-- Article/ArticleRepositoryTest.php | | |-- Banner/BannerRepositoryTest.php | | |-- Cache/CacheRepositoryTest.php +| | |-- Coupon/CouponRepositoryTest.php | | |-- Dictionaries/DictionariesRepositoryTest.php | | |-- Product/ProductRepositoryTest.php | | |-- Settings/SettingsRepositoryTest.php @@ -61,6 +62,7 @@ tests/ | |-- IntegrationsControllerTest.php | |-- ProductArchiveControllerTest.php | |-- SettingsControllerTest.php +| |-- ShopCouponControllerTest.php | `-- UsersControllerTest.php `-- Integration/ ``` @@ -318,3 +320,17 @@ OK (222 tests, 614 assertions) Zmiany testowe 2026-02-13: - rozszerzenie `tests/Unit/Domain/Promotion/PromotionRepositoryTest.php` o asercje `date_from` + +## Aktualizacja suite (ShopCoupon refactor, ver. 0.266) +Ostatnio zweryfikowano: 2026-02-13 + +```text +OK (235 tests, 682 assertions) +``` + +Nowe testy dodane 2026-02-13: +- `tests/Unit/Domain/Coupon/CouponRepositoryTest.php` (8 testow: find default/normalize, save insert/update, delete, whitelist sortowania, drzewo kategorii) +- `tests/Unit/admin/Controllers/ShopCouponControllerTest.php` (5 testow: kontrakty metod, aliasy legacy, DI konstruktora) + +Ponowna weryfikacja po poprawkach UI (drzewko + checkboxy): 2026-02-13 +- `OK (235 tests, 682 assertions)` diff --git a/admin/templates/layouts/layout-edit.php b/admin/templates/layouts/layout-edit.php index 2de458c..1885bc3 100644 --- a/admin/templates/layouts/layout-edit.php +++ b/admin/templates/layouts/layout-edit.php @@ -7,6 +7,48 @@ + ">
  • - + ';?> layout['categories'] ) and in_array( $category['id'], $this -> layout['categories'] ) ):?>checked="checked" /> dlang]['title'];?> diff --git a/admin/templates/layouts/subcategories-list.php b/admin/templates/layouts/subcategories-list.php index e1e5bf5..72880f2 100644 --- a/admin/templates/layouts/subcategories-list.php +++ b/admin/templates/layouts/subcategories-list.php @@ -3,7 +3,9 @@ categories as $category ):?>
  • - + ';?> product_categories ) and in_array( $category['id'], $this -> product_categories ) ):?>checked="checked" /> dlang]['title'];?> @@ -17,4 +19,3 @@ - diff --git a/admin/templates/layouts/subpages-list.php b/admin/templates/layouts/subpages-list.php index 3f68d21..61fd7eb 100644 --- a/admin/templates/layouts/subpages-list.php +++ b/admin/templates/layouts/subpages-list.php @@ -3,7 +3,9 @@ pages as $page ):?>
  • > - + layout_pages ) and in_array( $page['id'], $this -> layout_pages ) ):?>checked="checked" />
    label ?? 'Kategorie')); +$inputName = trim((string)($this->inputName ?? 'categories[]')); +$categories = is_array($this->categories ?? null) ? $this->categories : []; +$selectedIds = is_array($this->selectedIds ?? null) ? $this->selectedIds : []; +?> +
    + +
    +
    + $categories, + 'selectedIds' => $selectedIds, + 'inputName' => $inputName, + ]); ?> +
    +
    +
    + diff --git a/admin/templates/shop-coupon/coupon-categories-tree.php b/admin/templates/shop-coupon/coupon-categories-tree.php new file mode 100644 index 0000000..5a22b24 --- /dev/null +++ b/admin/templates/shop-coupon/coupon-categories-tree.php @@ -0,0 +1,68 @@ +categories ?? null) ? $this->categories : []; +$inputName = trim((string)($this->inputName ?? 'categories[]')); +$selectedRaw = is_array($this->selectedIds ?? null) ? $this->selectedIds : []; +$selected = []; +foreach ($selectedRaw as $value) { + $id = (int)$value; + if ($id > 0) { + $selected[$id] = true; + } +} +?> + +
      + + +
    1. +
      + + + + + + + + + + + +
      + + + $children, + 'selectedIds' => array_keys($selected), + 'inputName' => $inputName, + ]); ?> + +
    2. + +
    + + diff --git a/admin/templates/shop-coupon/coupon-edit-custom-script.php b/admin/templates/shop-coupon/coupon-edit-custom-script.php new file mode 100644 index 0000000..5cb6115 --- /dev/null +++ b/admin/templates/shop-coupon/coupon-edit-custom-script.php @@ -0,0 +1,109 @@ + + + diff --git a/admin/templates/shop-coupon/coupon-edit-new.php b/admin/templates/shop-coupon/coupon-edit-new.php new file mode 100644 index 0000000..06f6ba8 --- /dev/null +++ b/admin/templates/shop-coupon/coupon-edit-new.php @@ -0,0 +1,3 @@ + $this->form]); ?> + + diff --git a/admin/templates/shop-coupon/coupon-edit.php b/admin/templates/shop-coupon/coupon-edit.php deleted file mode 100644 index 724ff80..0000000 --- a/admin/templates/shop-coupon/coupon-edit.php +++ /dev/null @@ -1,151 +0,0 @@ - - - - -
    -
      -
    • Ustawienia
    • -
    -
    -
    - 'Nazwa', - 'name' => 'name', - 'id' => 'name', - 'value' => $this -> coupon['name'] - ]); ?> - 'Wysłany', - 'name' => 'send', - 'checked' => $this -> coupon['send'] == 1 ? true : false - ]); ?> - 'Aktywny', - 'name' => 'status', - 'checked' => $this -> coupon['status'] == 1 ? true : false - ]); ?> - 'Użyty', - 'name' => 'used', - 'checked' => $this -> coupon['used'] == 1 ? true : false, - ]); ?> - 'Typ kuponu', - 'name' => 'type', - 'values' => [ 1 => 'Rabat procentowy na koszyk'], - 'value' => $this -> coupo['type'], - ]); ?> - 'Wartość', - 'class' => 'number-format', - 'name' => 'amount', - 'id' => 'amount', - 'value' => $this -> coupon['amount'] - ]); ?> - 'Kupon jednorazowy', - 'name' => 'one_time', - 'checked' => $this -> coupon['one_time'] == 1 ? true : false - ]); ?> - 'Dotyczy również produktów przecenionych', - 'name' => 'include_discounted_product', - 'checked' => $this -> coupon['include_discounted_product'] == 1 ? true : false, - ]); ?> -
    - -
    - -
    -
    -
    -
    -
    -
    -id = 'coupon-edit'; -$grid->gdb_opt = $gdb; -$grid->include_plugins = true; -$grid->title = $this -> coupon['id'] ? 'Edycja kuponu: ' . $this -> coupon['name'] . '' : 'Nowy kupon'; -$grid->fields = [ - [ - 'db' => 'id', - 'type' => 'hidden', - 'value' => $this -> coupon['id'] - ] -]; -$grid->actions = [ - 'save' => ['url' => '/admin/shop_coupon/coupon_save/', 'back_url' => '/admin/shop_coupon/view_list/'], - 'cancel' => ['url' => '/admin/shop_coupon/view_list/'] -]; -$grid->external_code = $out; -$grid->persist_edit = true; -$grid->id_param = 'id'; - -echo $grid->draw(); -?> - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/admin/templates/shop-coupon/coupons-list.php b/admin/templates/shop-coupon/coupons-list.php new file mode 100644 index 0000000..b336124 --- /dev/null +++ b/admin/templates/shop-coupon/coupons-list.php @@ -0,0 +1,2 @@ + $this->viewModel]); ?> + diff --git a/admin/templates/shop-coupon/view-list.php b/admin/templates/shop-coupon/view-list.php deleted file mode 100644 index 08b5493..0000000 --- a/admin/templates/shop-coupon/view-list.php +++ /dev/null @@ -1,88 +0,0 @@ - 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' ] ] ], - [ 'name' => 'Użyty', 'db' => 'used', 'type' => 'select', 'replace' => [ 'array' => [ 0 => 'nie', 1 => 'tak' ] ] ], - [ 'name' => 'Wysłany', 'db' => 'send', 'type' => 'select', 'replace' => [ 'array' => [ 0 => 'nie', 1 => 'tak' ] ] ] - ]; -$grid -> columns_view = [ - [ - 'name' => 'Lp.', - 'th' => [ 'class' => 'g-lp' ], - 'td' => [ 'class' => 'g-center' ], - 'autoincrement' => 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' => 'Użyto X razy', - 'db' => 'used_count', - 'td' => [ 'class' => 'g-center' ], - 'th' => [ 'class' => 'g-center', 'style' => 'width: 150px;' ], - ], [ - 'name' => 'Nazwa', - 'db' => 'name', - 'sort' => true, - 'php' => 'echo "[name]";' - ], [ - 'name' => 'Typ kuponu', - 'db' => 'type', - 'replace' => [ 'array' => [ 1 => 'Rabat procentowy na koszyk' ] ] - ], [ - 'name' => 'Wartość', - 'db' => 'amount', - 'php' => 'if ( [type] == 1 ) echo "[amount]%";' - ], [ - 'name' => 'Kupon jednorazowy', - 'db' => 'one_time', - 'replace' => [ 'array' => [ 0 => 'nie', 1 => 'tak' ] ], - 'td' => [ 'class' => 'g-center' ], - 'th' => [ 'class' => 'g-center', 'style' => 'width: 150px;' ] - ], [ - 'name' => 'Wysłany', - 'db' => 'send', - 'replace' => [ 'array' => [ 0 => 'nie', 1 => 'tak' ] ], - 'td' => [ 'class' => 'g-center' ], - 'th' => [ 'class' => 'g-center', 'style' => 'width: 150px;' ] - ], [ - 'name' => 'Użyty', - 'db' => 'used', - 'replace' => [ 'array' => [ 0 => 'nie', 1 => 'tak' ] ], - 'td' => [ 'class' => 'g-center' ], - 'th' => [ 'class' => 'g-center', 'style' => 'width: 150px;' ] - ], [ - 'name' => 'Data użycia', - 'db' => 'date_used', - 'td' => [ 'class' => 'g-center' ], - 'th' => [ 'class' => 'g-center', 'style' => 'width: 150px;' ] - ], [ - 'name' => 'Edytuj', - 'action' => [ 'type' => 'edit', 'url' => '/admin/shop_coupon/coupon_edit/id=[id]' ], - 'th' => [ 'class' => 'g-center', 'style' => 'width: 70px;' ], - 'td' => [ 'class' => 'g-center' ] - ], [ - 'name' => 'Usuń', - 'action' => [ 'type' => 'delete', 'url' => '/admin/shop_coupon/coupon_delete/id=[id]' ], - 'th' => [ 'class' => 'g-center', 'style' => 'width: 70px;' ], - 'td' => [ 'class' => 'g-center' ] - ] - ]; -$grid -> buttons = [ - [ - 'label' => 'Dodaj kupon', - 'url' => '/admin/shop_coupon/coupon_edit/', - 'icon' => 'fa-plus-circle', - 'class' => 'btn-success' - ] - ]; -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 aeeb88d..847f84d 100644 --- a/admin/templates/site/main-layout.php +++ b/admin/templates/site/main-layout.php @@ -75,7 +75,7 @@
  • Statusy zamówień
  • -
  • Kody rabatowe
  • +
  • Kody rabatowe
  • Promocje
  • Zawartość
    @@ -197,6 +197,55 @@ diff --git a/autoload/Domain/Article/ArticleRepository.php b/autoload/Domain/Article/ArticleRepository.php index 41f7360..968a04f 100644 --- a/autoload/Domain/Article/ArticleRepository.php +++ b/autoload/Domain/Article/ArticleRepository.php @@ -602,6 +602,7 @@ class ArticleRepository ]); } + \S::delete_dir('../temp/'); return true; } @@ -636,6 +637,7 @@ class ArticleRepository } } + \S::delete_dir('../temp/'); return true; } diff --git a/autoload/Domain/Banner/BannerRepository.php b/autoload/Domain/Banner/BannerRepository.php index 083a13a..b2f8ee1 100644 --- a/autoload/Domain/Banner/BannerRepository.php +++ b/autoload/Domain/Banner/BannerRepository.php @@ -102,6 +102,8 @@ class BannerRepository $this->saveTranslations($bannerId, $data['src'], $data['url'], $data['html'], $data['text']); } + \S::delete_dir('../temp/'); + return (int)$bannerId; } diff --git a/autoload/Domain/Coupon/CouponRepository.php b/autoload/Domain/Coupon/CouponRepository.php new file mode 100644 index 0000000..2287289 --- /dev/null +++ b/autoload/Domain/Coupon/CouponRepository.php @@ -0,0 +1,391 @@ +db = $db; + } + + /** + * @return array{items: array>, total: int} + */ + public function listForAdmin( + array $filters, + string $sortColumn = 'name', + string $sortDir = 'ASC', + int $page = 1, + int $perPage = 15 + ): array { + $allowedSortColumns = [ + 'id' => 'sc.id', + 'status' => 'sc.status', + 'used_count' => 'sc.used_count', + 'name' => 'sc.name', + 'type' => 'sc.type', + 'amount' => 'sc.amount', + 'one_time' => 'sc.one_time', + 'send' => 'sc.send', + 'used' => 'sc.used', + 'date_used' => 'sc.date_used', + ]; + + $sortSql = $allowedSortColumns[$sortColumn] ?? 'sc.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[] = 'sc.name LIKE :name'; + $params[':name'] = '%' . $name . '%'; + } + + $status = trim((string)($filters['status'] ?? '')); + if ($status === '0' || $status === '1') { + $where[] = 'sc.status = :status'; + $params[':status'] = (int)$status; + } + + $used = trim((string)($filters['used'] ?? '')); + if ($used === '0' || $used === '1') { + $where[] = 'sc.used = :used'; + $params[':used'] = (int)$used; + } + + $send = trim((string)($filters['send'] ?? '')); + if ($send === '0' || $send === '1') { + $where[] = 'sc.send = :send'; + $params[':send'] = (int)$send; + } + + $whereSql = implode(' AND ', $where); + + $sqlCount = " + SELECT COUNT(0) + FROM pp_shop_coupon AS sc + WHERE {$whereSql} + "; + + $stmtCount = $this->db->query($sqlCount, $params); + $countRows = $stmtCount ? $stmtCount->fetchAll() : []; + $total = isset($countRows[0][0]) ? (int)$countRows[0][0] : 0; + + $sql = " + SELECT + sc.id, + sc.name, + sc.status, + sc.used, + sc.type, + sc.amount, + sc.one_time, + sc.send, + sc.include_discounted_product, + sc.categories, + sc.date_used, + sc.used_count + FROM pp_shop_coupon AS sc + WHERE {$whereSql} + ORDER BY {$sortSql} {$sortDir}, sc.id DESC + LIMIT {$perPage} OFFSET {$offset} + "; + + $stmt = $this->db->query($sql, $params); + $items = $stmt ? $stmt->fetchAll() : []; + + if (!is_array($items)) { + $items = []; + } + + foreach ($items as &$item) { + $item['id'] = (int)($item['id'] ?? 0); + $item['status'] = $this->toSwitchValue($item['status'] ?? 0); + $item['used'] = $this->toSwitchValue($item['used'] ?? 0); + $item['one_time'] = $this->toSwitchValue($item['one_time'] ?? 0); + $item['send'] = $this->toSwitchValue($item['send'] ?? 0); + $item['used_count'] = (int)($item['used_count'] ?? 0); + $item['type'] = (int)($item['type'] ?? 1); + $item['categories'] = $this->decodeIdList($item['categories'] ?? null); + } + unset($item); + + return [ + 'items' => $items, + 'total' => $total, + ]; + } + + public function find(int $couponId): array + { + if ($couponId <= 0) { + return $this->defaultCoupon(); + } + + $coupon = $this->db->get('pp_shop_coupon', '*', ['id' => $couponId]); + if (!is_array($coupon)) { + return $this->defaultCoupon(); + } + + $coupon['id'] = (int)($coupon['id'] ?? 0); + $coupon['status'] = $this->toSwitchValue($coupon['status'] ?? 0); + $coupon['send'] = $this->toSwitchValue($coupon['send'] ?? 0); + $coupon['used'] = $this->toSwitchValue($coupon['used'] ?? 0); + $coupon['one_time'] = $this->toSwitchValue($coupon['one_time'] ?? 0); + $coupon['include_discounted_product'] = $this->toSwitchValue($coupon['include_discounted_product'] ?? 0); + $coupon['type'] = (int)($coupon['type'] ?? 1); + $coupon['used_count'] = (int)($coupon['used_count'] ?? 0); + $coupon['categories'] = $this->decodeIdList($coupon['categories'] ?? null); + + return $coupon; + } + + public function save(array $data): ?int + { + $couponId = (int)($data['id'] ?? 0); + + $row = [ + 'name' => trim((string)($data['name'] ?? '')), + 'status' => $this->toSwitchValue($data['status'] ?? 0), + 'send' => $this->toSwitchValue($data['send'] ?? 0), + 'used' => $this->toSwitchValue($data['used'] ?? 0), + 'type' => (int)($data['type'] ?? 1), + 'amount' => $this->toNullableNumeric($data['amount'] ?? null), + 'one_time' => $this->toSwitchValue($data['one_time'] ?? 0), + 'include_discounted_product' => $this->toSwitchValue($data['include_discounted_product'] ?? 0), + 'categories' => $this->encodeIdList($data['categories'] ?? null), + ]; + + if ($couponId <= 0) { + $this->db->insert('pp_shop_coupon', $row); + $id = (int)$this->db->id(); + return $id > 0 ? $id : null; + } + + $this->db->update('pp_shop_coupon', $row, ['id' => $couponId]); + return $couponId; + } + + public function delete(int $couponId): bool + { + if ($couponId <= 0) { + return false; + } + + return (bool)$this->db->delete('pp_shop_coupon', ['id' => $couponId]); + } + + /** + * @return array> + */ + public function categoriesTree($parentId = null): array + { + $rows = $this->db->select('pp_shop_categories', ['id'], [ + 'parent_id' => $parentId, + 'ORDER' => ['o' => 'ASC'], + ]); + + if (!is_array($rows)) { + return []; + } + + $categories = []; + foreach ($rows as $row) { + $categoryId = (int)($row['id'] ?? 0); + if ($categoryId <= 0) { + continue; + } + + $category = $this->db->get('pp_shop_categories', '*', ['id' => $categoryId]); + if (!is_array($category)) { + continue; + } + + $translations = $this->db->select('pp_shop_categories_langs', '*', ['category_id' => $categoryId]); + $category['languages'] = []; + if (is_array($translations)) { + foreach ($translations as $translation) { + $langId = (string)($translation['lang_id'] ?? ''); + if ($langId !== '') { + $category['languages'][$langId] = $translation; + } + } + } + + $category['title'] = $this->categoryTitle($category['languages']); + $category['subcategories'] = $this->categoriesTree($categoryId); + $categories[] = $category; + } + + return $categories; + } + + private function defaultCoupon(): array + { + return [ + 'id' => 0, + 'name' => '', + 'status' => 1, + 'send' => 0, + 'used' => 0, + 'type' => 1, + 'amount' => null, + 'one_time' => 1, + 'include_discounted_product' => 0, + 'categories' => [], + 'used_count' => 0, + 'date_used' => null, + ]; + } + + 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; + } + + private function toNullableNumeric($value): ?string + { + if ($value === null) { + return null; + } + + $stringValue = trim((string)$value); + if ($stringValue === '') { + return null; + } + + return str_replace(',', '.', $stringValue); + } + + private function encodeIdList($values): ?string + { + $ids = $this->normalizeIdList($values); + if (empty($ids)) { + return null; + } + + return json_encode($ids); + } + + /** + * @return int[] + */ + private function decodeIdList($raw): array + { + if (is_array($raw)) { + return $this->normalizeIdList($raw); + } + + $text = trim((string)$raw); + if ($text === '') { + return []; + } + + $decoded = json_decode($text, true); + if (!is_array($decoded)) { + return []; + } + + return $this->normalizeIdList($decoded); + } + + /** + * @return int[] + */ + private function normalizeIdList($values): array + { + if ($values === null) { + return []; + } + + if (!is_array($values)) { + $text = trim((string)$values); + if ($text === '') { + return []; + } + + if (strpos($text, ',') !== false) { + $values = explode(',', $text); + } else { + $values = [$text]; + } + } + + $ids = []; + foreach ($values as $value) { + $id = (int)$value; + if ($id > 0) { + $ids[$id] = $id; + } + } + + return array_values($ids); + } + + private function categoryTitle(array $languages): string + { + $defaultLang = $this->defaultLanguageId(); + if ($defaultLang !== '' && isset($languages[$defaultLang]['title'])) { + $title = trim((string)$languages[$defaultLang]['title']); + if ($title !== '') { + return $title; + } + } + + foreach ($languages as $language) { + $title = trim((string)($language['title'] ?? '')); + if ($title !== '') { + return $title; + } + } + + return ''; + } + + private function defaultLanguageId(): string + { + if ($this->defaultLangId !== null) { + return $this->defaultLangId; + } + + $rows = $this->db->select('pp_langs', ['id', 'start', 'o'], [ + 'status' => 1, + 'ORDER' => ['start' => 'DESC', 'o' => 'ASC'], + ]); + + if (is_array($rows) && !empty($rows)) { + $this->defaultLangId = (string)($rows[0]['id'] ?? ''); + } else { + $this->defaultLangId = ''; + } + + return $this->defaultLangId; + } +} + diff --git a/autoload/Domain/Dictionaries/DictionariesRepository.php b/autoload/Domain/Dictionaries/DictionariesRepository.php index 8fe6aeb..30169ca 100644 --- a/autoload/Domain/Dictionaries/DictionariesRepository.php +++ b/autoload/Domain/Dictionaries/DictionariesRepository.php @@ -249,6 +249,7 @@ class DictionariesRepository private function clearCache(): void { if (class_exists('\S') && method_exists('\S', 'delete_dir')) { + \S::delete_dir('../temp/'); \S::delete_dir('../temp/dictionaries'); } } diff --git a/autoload/Domain/Integrations/IntegrationsRepository.php b/autoload/Domain/Integrations/IntegrationsRepository.php index 28f62a0..9a70801 100644 --- a/autoload/Domain/Integrations/IntegrationsRepository.php +++ b/autoload/Domain/Integrations/IntegrationsRepository.php @@ -51,6 +51,7 @@ class IntegrationsRepository } else { $this->db->insert( $table, [ 'name' => $name, 'value' => $value ] ); } + \S::delete_dir('../temp/'); return true; } diff --git a/autoload/Domain/Layouts/LayoutsRepository.php b/autoload/Domain/Layouts/LayoutsRepository.php index 7cf6be6..093282a 100644 --- a/autoload/Domain/Layouts/LayoutsRepository.php +++ b/autoload/Domain/Layouts/LayoutsRepository.php @@ -18,7 +18,13 @@ class LayoutsRepository return false; } - return (bool)$this->db->delete('pp_layouts', ['id' => $layoutId]); + $deleted = (bool)$this->db->delete('pp_layouts', ['id' => $layoutId]); + if ($deleted) { + \S::delete_dir('../temp/'); + $this->clearFrontLayoutsCache(); + } + + return $deleted; } public function find(int $layoutId): array @@ -77,6 +83,7 @@ class LayoutsRepository $this->syncCategories($layoutId, $data['categories'] ?? []); \S::delete_dir('../temp/'); + $this->clearFrontLayoutsCache(); return $layoutId; } @@ -288,6 +295,22 @@ class LayoutsRepository ]; } + private function clearFrontLayoutsCache(): void + { + if (!class_exists('\CacheHandler')) { + return; + } + + try { + $cacheHandler = new \CacheHandler(); + if (method_exists($cacheHandler, 'deletePattern')) { + $cacheHandler->deletePattern('*Layouts::*'); + } + } catch (\Throwable $e) { + // Inwalidacja cache nie moze blokowac zapisu/usuwania. + } + } + private function menuPages(int $menuId, $parentId = null): array { if ($menuId <= 0) { diff --git a/autoload/Domain/Newsletter/NewsletterRepository.php b/autoload/Domain/Newsletter/NewsletterRepository.php index 7f7e63e..cdf7da9 100644 --- a/autoload/Domain/Newsletter/NewsletterRepository.php +++ b/autoload/Domain/Newsletter/NewsletterRepository.php @@ -25,6 +25,7 @@ class NewsletterRepository { $this->settingsRepository->updateSetting('newsletter_footer', (string)($values['newsletter_footer'] ?? '')); $this->settingsRepository->updateSetting('newsletter_header', (string)($values['newsletter_header'] ?? '')); + \S::delete_dir('../temp/'); return true; } @@ -289,4 +290,3 @@ class NewsletterRepository ]; } } - diff --git a/autoload/Domain/Pages/PagesRepository.php b/autoload/Domain/Pages/PagesRepository.php index 7c9a941..b897666 100644 --- a/autoload/Domain/Pages/PagesRepository.php +++ b/autoload/Domain/Pages/PagesRepository.php @@ -172,6 +172,9 @@ class PagesRepository 'status' => $statusValue, ]); + if ($result) { + \S::delete_dir('../temp/'); + } return (bool)$result; } @@ -182,6 +185,7 @@ class PagesRepository 'id' => $menuId, ]); + \S::delete_dir('../temp/'); return true; } @@ -278,6 +282,7 @@ class PagesRepository ]); } + \S::delete_dir('../temp/'); return true; } diff --git a/autoload/Domain/Promotion/PromotionRepository.php b/autoload/Domain/Promotion/PromotionRepository.php index 9222431..41d7239 100644 --- a/autoload/Domain/Promotion/PromotionRepository.php +++ b/autoload/Domain/Promotion/PromotionRepository.php @@ -148,11 +148,13 @@ class PromotionRepository } $this->invalidateActivePromotionsCache(); + \S::delete_dir('../temp/'); return $id; } $this->db->update('pp_shop_promotion', $row, ['id' => $promotionId]); $this->invalidateActivePromotionsCache(); + \S::delete_dir('../temp/'); return $promotionId; } diff --git a/autoload/Domain/Settings/SettingsRepository.php b/autoload/Domain/Settings/SettingsRepository.php index 2a0969a..64e7acb 100644 --- a/autoload/Domain/Settings/SettingsRepository.php +++ b/autoload/Domain/Settings/SettingsRepository.php @@ -90,6 +90,7 @@ class SettingsRepository // Zachowanie zgodne z dotychczasowym flow: pelna podmiana zestawu ustawien. $this->db->query('TRUNCATE pp_settings'); $this->updateSettings($settingsToSave); + \S::delete_dir('../temp/'); \S::set_message('Ustawienia zostaly zapisane'); diff --git a/autoload/Domain/User/UserRepository.php b/autoload/Domain/User/UserRepository.php index 4518670..6025886 100644 --- a/autoload/Domain/User/UserRepository.php +++ b/autoload/Domain/User/UserRepository.php @@ -154,6 +154,7 @@ class UserRepository ]); if ($inserted) { + \S::delete_dir('../temp/'); return ['status' => 'ok', 'msg' => 'Uzytkownik zostal zapisany.']; } @@ -186,6 +187,7 @@ class UserRepository 'id' => $userId, ]); + \S::delete_dir('../temp/'); return ['status' => 'ok', 'msg' => 'Uzytkownik zostal zapisany.']; } diff --git a/autoload/admin/Controllers/BannerController.php b/autoload/admin/Controllers/BannerController.php index 8828e58..84016a3 100644 --- a/autoload/admin/Controllers/BannerController.php +++ b/autoload/admin/Controllers/BannerController.php @@ -210,7 +210,6 @@ class BannerController $savedId = $this->repository->save($data); if ($savedId) { - \S::delete_dir('../temp/'); $response = [ 'success' => true, 'id' => $savedId, diff --git a/autoload/admin/Controllers/ShopCouponController.php b/autoload/admin/Controllers/ShopCouponController.php new file mode 100644 index 0000000..9025bf2 --- /dev/null +++ b/autoload/admin/Controllers/ShopCouponController.php @@ -0,0 +1,353 @@ +repository = $repository; + } + + public function list(): string + { + $sortableColumns = ['id', 'status', 'used_count', 'name', 'type', 'amount', 'one_time', 'send', 'used', 'date_used']; + $filterDefinitions = [ + [ + 'key' => 'name', + 'label' => 'Nazwa', + 'type' => 'text', + ], + [ + 'key' => 'status', + 'label' => 'Aktywny', + 'type' => 'select', + 'options' => [ + '' => '- aktywny -', + '1' => 'tak', + '0' => 'nie', + ], + ], + [ + 'key' => 'used', + 'label' => 'Uzyty', + 'type' => 'select', + 'options' => [ + '' => '- uzyty -', + '1' => 'tak', + '0' => 'nie', + ], + ], + [ + 'key' => 'send', + 'label' => 'Wyslany', + 'type' => 'select', + 'options' => [ + '' => '- wyslany -', + '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->repository->listForAdmin( + $listRequest['filters'], + $listRequest['sortColumn'], + $sortDir, + $listRequest['page'], + $listRequest['perPage'] + ); + + $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); + $used = (int)($item['used'] ?? 0); + $send = (int)($item['send'] ?? 0); + $oneTime = (int)($item['one_time'] ?? 0); + $type = (int)($item['type'] ?? 0); + $amount = (string)($item['amount'] ?? ''); + $dateUsed = trim((string)($item['date_used'] ?? '')); + + $rows[] = [ + 'lp' => $lp++ . '.', + 'status' => $status === 1 ? 'tak' : 'nie', + 'used_count' => (int)($item['used_count'] ?? 0), + 'name' => '' . htmlspecialchars($name, ENT_QUOTES, 'UTF-8') . '', + 'type' => htmlspecialchars((string)($type === 1 ? 'Rabat procentowy na koszyk' : '-'), ENT_QUOTES, 'UTF-8'), + 'amount' => $type === 1 && $amount !== '' ? htmlspecialchars($amount, ENT_QUOTES, 'UTF-8') . '%' : '-', + 'one_time' => $oneTime === 1 ? 'tak' : 'nie', + 'send' => $send === 1 ? 'tak' : 'nie', + 'used' => $used === 1 ? 'tak' : 'nie', + 'date_used' => $dateUsed !== '' ? htmlspecialchars($dateUsed, ENT_QUOTES, 'UTF-8') : '-', + '_actions' => [ + [ + 'label' => 'Edytuj', + 'url' => '/admin/shop_coupon/edit/id=' . $id, + 'class' => 'btn btn-xs btn-primary', + ], + [ + 'label' => 'Usun', + 'url' => '/admin/shop_coupon/delete/id=' . $id, + 'class' => 'btn btn-xs btn-danger', + 'confirm' => 'Na pewno chcesz usunac wybrany kupon?', + 'confirm_ok' => 'Usun', + 'confirm_cancel' => 'Anuluj', + ], + ], + ]; + } + + $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' => 'status', 'sort_key' => 'status', 'label' => 'Aktywny', 'class' => 'text-center', 'sortable' => true, 'raw' => true], + ['key' => 'used_count', 'sort_key' => 'used_count', 'label' => 'Uzyto X razy', 'class' => 'text-center', 'sortable' => true], + ['key' => 'name', 'sort_key' => 'name', 'label' => 'Nazwa', 'sortable' => true, 'raw' => true], + ['key' => 'type', 'sort_key' => 'type', 'label' => 'Typ kuponu', 'sortable' => true], + ['key' => 'amount', 'sort_key' => 'amount', 'label' => 'Wartosc', 'class' => 'text-center', 'sortable' => true, 'raw' => true], + ['key' => 'one_time', 'sort_key' => 'one_time', 'label' => 'Kupon jednorazowy', 'class' => 'text-center', 'sortable' => true], + ['key' => 'send', 'sort_key' => 'send', 'label' => 'Wyslany', 'class' => 'text-center', 'sortable' => true], + ['key' => 'used', 'sort_key' => 'used', 'label' => 'Uzyty', 'class' => 'text-center', 'sortable' => true], + ['key' => 'date_used', 'sort_key' => 'date_used', 'label' => 'Data uzycia', 'class' => 'text-center', 'sortable' => true, 'raw' => true], + ], + $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_coupon/list/', + 'Brak danych w tabeli.', + '/admin/shop_coupon/edit/', + 'Dodaj kupon' + ); + + return \Tpl::view('shop-coupon/coupons-list', [ + 'viewModel' => $viewModel, + ]); + } + + public function view_list(): string + { + return $this->list(); + } + + public function edit(): string + { + $coupon = $this->repository->find((int)\S::get('id')); + $categories = $this->repository->categoriesTree(null); + + return \Tpl::view('shop-coupon/coupon-edit-new', [ + 'form' => $this->buildFormViewModel($coupon, $categories), + ]); + } + + public function coupon_edit(): string + { + return $this->edit(); + } + + public function save(): void + { + $legacyValues = \S::get('values'); + + if ($legacyValues) { + $values = json_decode((string)$legacyValues, true); + $response = [ + 'status' => 'error', + 'msg' => 'Podczas zapisywania kuponu wystapil blad. Prosze sprobowac ponownie.', + ]; + + if (is_array($values)) { + $id = $this->repository->save($values); + if (!empty($id)) { + $response = [ + 'status' => 'ok', + 'msg' => 'Kupon zostal zapisany.', + 'id' => (int)$id, + ]; + } + } + + echo json_encode($response); + exit; + } + + $payload = $_POST; + if (empty($payload['id'])) { + $routeId = (int)\S::get('id'); + if ($routeId > 0) { + $payload['id'] = $routeId; + } + } + + $id = $this->repository->save($payload); + if (!empty($id)) { + echo json_encode([ + 'success' => true, + 'id' => (int)$id, + 'message' => 'Kupon zostal zapisany.', + ]); + exit; + } + + echo json_encode([ + 'success' => false, + 'errors' => ['general' => 'Podczas zapisywania kuponu wystapil blad.'], + ]); + exit; + } + + public function coupon_save(): void + { + $this->save(); + } + + public function delete(): void + { + if ($this->repository->delete((int)\S::get('id'))) { + \S::alert('Kupon zostal usuniety.'); + } + + header('Location: /admin/shop_coupon/list/'); + exit; + } + + public function coupon_delete(): void + { + $this->delete(); + } + + private function buildFormViewModel(array $coupon, array $categories): FormEditViewModel + { + $id = (int)($coupon['id'] ?? 0); + $isNew = $id <= 0; + + $data = [ + 'id' => $id, + 'name' => (string)($coupon['name'] ?? ''), + 'send' => (int)($coupon['send'] ?? 0), + 'status' => (int)($coupon['status'] ?? 1), + 'used' => (int)($coupon['used'] ?? 0), + 'type' => (int)($coupon['type'] ?? 1), + 'amount' => (string)($coupon['amount'] ?? ''), + 'one_time' => (int)($coupon['one_time'] ?? 1), + 'include_discounted_product' => (int)($coupon['include_discounted_product'] ?? 0), + ]; + + $fields = [ + FormField::hidden('id', $id), + FormField::text('name', [ + 'label' => 'Nazwa', + 'tab' => 'settings', + 'required' => true, + ]), + FormField::switch('send', [ + 'label' => 'Wyslany', + 'tab' => 'settings', + ]), + FormField::switch('status', [ + 'label' => 'Aktywny', + 'tab' => 'settings', + 'value' => true, + ]), + FormField::switch('used', [ + 'label' => 'Uzyty', + 'tab' => 'settings', + ]), + FormField::select('type', [ + 'label' => 'Typ kuponu', + 'tab' => 'settings', + 'options' => [ + 1 => 'Rabat procentowy na koszyk', + ], + 'required' => true, + ]), + FormField::text('amount', [ + 'label' => 'Wartosc', + 'tab' => 'settings', + 'attributes' => ['class' => 'number-format'], + ]), + FormField::switch('one_time', [ + 'label' => 'Kupon jednorazowy', + 'tab' => 'settings', + 'value' => true, + ]), + FormField::switch('include_discounted_product', [ + 'label' => 'Dotyczy rowniez produktow przecenionych', + 'tab' => 'settings', + ]), + FormField::custom('coupon_categories', \Tpl::view('shop-coupon/coupon-categories-selector', [ + 'label' => 'Ogranicz promocje do wybranych kategorii', + 'inputName' => 'categories[]', + 'categories' => $categories, + 'selectedIds' => is_array($coupon['categories'] ?? null) ? $coupon['categories'] : [], + ]), [ + 'tab' => 'categories', + ]), + ]; + + $tabs = [ + new FormTab('settings', 'Ustawienia', 'fa-wrench'), + new FormTab('categories', 'Kategorie', 'fa-folder-open'), + ]; + + $actionUrl = '/admin/shop_coupon/save/' . ($isNew ? '' : ('id=' . $id)); + $actions = [ + FormAction::save($actionUrl, '/admin/shop_coupon/list/'), + FormAction::cancel('/admin/shop_coupon/list/'), + ]; + + return new FormEditViewModel( + 'shop-coupon-edit', + $isNew ? 'Nowy kupon' : ('Edycja kuponu: ' . (string)($coupon['name'] ?? '')), + $data, + $fields, + $tabs, + $actions, + 'POST', + $actionUrl, + '/admin/shop_coupon/list/', + true, + ['id' => $id] + ); + } +} diff --git a/autoload/admin/class.Site.php b/autoload/admin/class.Site.php index 2cb0584..23b4371 100644 --- a/autoload/admin/class.Site.php +++ b/autoload/admin/class.Site.php @@ -309,6 +309,13 @@ class Site new \Domain\Promotion\PromotionRepository( $mdb ) ); }, + 'ShopCoupon' => function() { + global $mdb; + + return new \admin\Controllers\ShopCouponController( + new \Domain\Coupon\CouponRepository( $mdb ) + ); + }, 'Pages' => function() { global $mdb; diff --git a/autoload/admin/controls/class.ShopCoupon.php b/autoload/admin/controls/class.ShopCoupon.php deleted file mode 100644 index 2ce1ea2..0000000 --- a/autoload/admin/controls/class.ShopCoupon.php +++ /dev/null @@ -1,58 +0,0 @@ - delete() ) - \S::alert( 'Kupon został usunięty.' ); - header( 'Location: /admin/shop_coupon/view_list/' ); - exit; - } - - public static function coupon_save() - { - $response = ['status' => 'error', 'msg' => 'Podczas zapisywania kuponu wystąpił błąd. Proszę spróbować ponownie.']; - $values = json_decode( \S::get( 'values' ), true ); - - if ( $values['categories'] != null ) - $categories = is_array( $values['categories'] ) ? json_encode( $values['categories'] ) : json_encode( [ $values['categories'] ] ); - else - $categories = null; - - if ( \admin\factory\ShopCoupon::save( - $values['id'], - $values['name'], - $values['status'] == 'on' ? 1 : 0, - $values['send'] == 'on' ? 1 : 0, - $values['used'] == 'on' ? 1 : 0, - $values['type'], - $values['amount'], - $values['one_time'] == 'on' ? 1 : 0, - $values['include_discounted_product'] == 'on' ? 1 : 0, - $categories - ) - ) - $response = [ 'status' => 'ok', 'msg' => 'Kupon został zapisany.', 'id' => $values['id'] ]; - - echo json_encode( $response ); - exit; - } - - public static function coupon_edit() - { - return \Tpl::view( 'shop-coupon/coupon-edit', [ - 'coupon' => \admin\factory\ShopCoupon::details( (int)\S::get( 'id' ) ), - 'categories' => \admin\factory\ShopCategory::subcategories( null ), - 'dlang' => \front\factory\Languages::default_language() - ] ); - } - - public static function view_list() - { - return \Tpl::view( 'shop-coupon/view-list' ); - } - -} \ No newline at end of file diff --git a/autoload/admin/factory/class.ShopCoupon.php b/autoload/admin/factory/class.ShopCoupon.php deleted file mode 100644 index 6ba8266..0000000 --- a/autoload/admin/factory/class.ShopCoupon.php +++ /dev/null @@ -1,54 +0,0 @@ - get( 'pp_shop_coupon', '*', [ 'id' => $coupon_id ] ); - } - - public static function coupon_delete( $coupon_id ) - { - global $mdb; - return $mdb -> delete( 'pp_shop_coupon', [ 'id' => $coupon_id ] ); - } - - public static function save( $coupon_id, $name, $status, $send, $used, $type, $amount, $one_time, $include_discounted_product, $categories ) - { - global $mdb; - - if ( !$coupon_id ) - { - if ( $mdb -> insert( 'pp_shop_coupon', [ - 'name' => $name, - 'status' => $status, - 'used' => $used, - 'type' => $type, - 'amount' => $amount, - 'one_time' => $one_time, - 'send' => $send, - 'include_discounted_product' => $include_discounted_product, - 'categories' => $categories - ] ) ) - return $mdb -> id(); - } - else - { - $mdb -> update( 'pp_shop_coupon', [ - 'name' => $name, - 'status' => $status, - 'used' => $used, - 'type' => $type, - 'amount' => $amount, - 'one_time' => $one_time, - 'send' => $send, - 'include_discounted_product' => $include_discounted_product, - 'categories' => $categories - ], [ - 'id' => $coupon_id - ] ); - return true; - } - } -} diff --git a/index.php b/index.php index 6e4dd8f..22ef03d 100644 --- a/index.php +++ b/index.php @@ -136,6 +136,17 @@ if ( \S::get( 'a' ) == 'page' and \S::get( 'id' ) ) \S::set_session( 'page', $page ); } +if ( !is_array( $page ) or !(int)$page['id'] ) +{ + $page = \S::get_session( 'page' ); +} + +if ( !is_array( $page ) or !(int)$page['id'] ) +{ + $page = \front\factory\Pages::page_details(); + \S::set_session( 'page', $page ); +} + if ( \S::get( 'devel' ) ) $settings[ 'devel' ] = true; @@ -350,4 +361,4 @@ if ( file_exists( 'plugins/special-actions-end.php' ) ) include 'plugins/special-actions-end.php'; echo $html; -?> \ No newline at end of file +?> diff --git a/tests/Unit/Domain/Coupon/CouponRepositoryTest.php b/tests/Unit/Domain/Coupon/CouponRepositoryTest.php new file mode 100644 index 0000000..b0fe639 --- /dev/null +++ b/tests/Unit/Domain/Coupon/CouponRepositoryTest.php @@ -0,0 +1,268 @@ +createMock(\medoo::class); + $repository = new CouponRepository($mockDb); + + $result = $repository->find(0); + + $this->assertIsArray($result); + $this->assertSame(0, (int)$result['id']); + $this->assertSame(1, (int)$result['status']); + $this->assertSame(1, (int)$result['one_time']); + $this->assertSame([], $result['categories']); + } + + public function testFindNormalizesCouponData(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('get') + ->with('pp_shop_coupon', '*', ['id' => 15]) + ->willReturn([ + 'id' => '15', + 'name' => 'KOD15', + 'status' => '1', + 'send' => 0, + 'used' => '1', + 'type' => '1', + 'amount' => '15.00', + 'one_time' => '0', + 'include_discounted_product' => '1', + 'categories' => '[4,6,6]', + 'used_count' => '3', + ]); + + $repository = new CouponRepository($mockDb); + $result = $repository->find(15); + + $this->assertSame(15, (int)$result['id']); + $this->assertSame(1, (int)$result['status']); + $this->assertSame(0, (int)$result['send']); + $this->assertSame(1, (int)$result['used']); + $this->assertSame(0, (int)$result['one_time']); + $this->assertSame(1, (int)$result['include_discounted_product']); + $this->assertSame([4, 6], $result['categories']); + $this->assertSame(3, (int)$result['used_count']); + } + + public function testSaveInsertsCouponAndReturnsId(): void + { + $mockDb = $this->createMock(\medoo::class); + $insertRow = null; + + $mockDb->expects($this->once()) + ->method('insert') + ->willReturnCallback(function ($table, $row) use (&$insertRow) { + $this->assertSame('pp_shop_coupon', $table); + $insertRow = $row; + }); + + $mockDb->expects($this->once()) + ->method('id') + ->willReturn(321); + + $repository = new CouponRepository($mockDb); + $id = $repository->save([ + 'name' => ' KOD25 ', + 'status' => 'on', + 'send' => '1', + 'used' => 0, + 'type' => 1, + 'amount' => '25,50', + 'one_time' => 'on', + 'include_discounted_product' => 'on', + 'categories' => [1, '2', 'abc', 2], + ]); + + $this->assertSame(321, $id); + $this->assertIsArray($insertRow); + $this->assertSame('KOD25', $insertRow['name'] ?? ''); + $this->assertSame(1, (int)($insertRow['status'] ?? 0)); + $this->assertSame(1, (int)($insertRow['send'] ?? 0)); + $this->assertSame(0, (int)($insertRow['used'] ?? 1)); + $this->assertSame('25.50', $insertRow['amount'] ?? null); + $this->assertSame('[1,2]', $insertRow['categories'] ?? null); + } + + public function testSaveUpdatesCouponAndReturnsId(): void + { + $mockDb = $this->createMock(\medoo::class); + $updateRow = null; + $updateWhere = null; + + $mockDb->expects($this->once()) + ->method('update') + ->willReturnCallback(function ($table, $row, $where) use (&$updateRow, &$updateWhere) { + $this->assertSame('pp_shop_coupon', $table); + $updateRow = $row; + $updateWhere = $where; + return true; + }); + + $mockDb->expects($this->never())->method('insert'); + $mockDb->expects($this->never())->method('id'); + + $repository = new CouponRepository($mockDb); + $id = $repository->save([ + 'id' => 77, + 'name' => 'KOD77', + 'status' => '0', + 'send' => 'off', + 'used' => '0', + 'type' => 1, + 'amount' => '', + 'one_time' => '0', + 'include_discounted_product' => 0, + 'categories' => '', + ]); + + $this->assertSame(77, $id); + $this->assertIsArray($updateRow); + $this->assertArrayHasKey('amount', $updateRow); + $this->assertArrayHasKey('categories', $updateRow); + $this->assertNull($updateRow['amount']); + $this->assertNull($updateRow['categories']); + $this->assertSame(0, (int)($updateRow['status'] ?? 1)); + $this->assertSame(['id' => 77], $updateWhere); + } + + public function testDeleteReturnsFalseForInvalidId(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->never())->method('delete'); + + $repository = new CouponRepository($mockDb); + $this->assertFalse($repository->delete(0)); + } + + public function testDeleteReturnsTrueWhenDatabaseDeleteSucceeds(): void + { + $mockDb = $this->createMock(\medoo::class); + $mockDb->expects($this->once()) + ->method('delete') + ->with('pp_shop_coupon', ['id' => 55]) + ->willReturn(true); + + $repository = new CouponRepository($mockDb); + $this->assertTrue($repository->delete(55)); + } + + public function testListForAdminWhitelistsSortAndDirection(): 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' => 'KOD', + 'status' => 1, + 'used' => 0, + 'type' => 1, + 'amount' => '10', + 'one_time' => 1, + 'send' => 0, + 'include_discounted_product' => 0, + 'categories' => '[3,5]', + 'date_used' => null, + 'used_count' => 7, + ]]; + } + }; + }); + + $repository = new CouponRepository($mockDb); + $result = $repository->listForAdmin( + [], + 'date_used DESC; DROP TABLE pp_shop_coupon; --', + 'DESC; DELETE FROM pp_users; --', + 1, + 999 + ); + + $this->assertCount(2, $queries); + $dataSql = $queries[1]['sql']; + + $this->assertMatchesRegularExpression('/ORDER BY\s+sc\.name\s+ASC,\s+sc\.id\s+DESC/i', $dataSql); + $this->assertStringNotContainsString('DROP TABLE', $dataSql); + $this->assertStringNotContainsString('DELETE FROM pp_users', $dataSql); + $this->assertMatchesRegularExpression('/LIMIT\s+100\s+OFFSET\s+0/i', $dataSql); + + $this->assertSame([3, 5], $result['items'][0]['categories']); + } + + public function testCategoriesTreeReturnsHierarchy(): void + { + $mockDb = $this->createMock(\medoo::class); + + $mockDb->method('select') + ->willReturnCallback(function ($table, $columns, $where) { + if ($table === 'pp_shop_categories' && array_key_exists('parent_id', $where)) { + if ($where['parent_id'] === null) { + return [['id' => 10]]; + } + if ((int)$where['parent_id'] === 10) { + return [['id' => 11]]; + } + return []; + } + + if ($table === 'pp_shop_categories_langs') { + if ((int)$where['category_id'] === 10) { + return [['lang_id' => 'pl', 'title' => 'Kategoria A']]; + } + if ((int)$where['category_id'] === 11) { + return [['lang_id' => 'pl', 'title' => 'Podkategoria A1']]; + } + return []; + } + + if ($table === 'pp_langs') { + return [['id' => 'pl', 'start' => 1, 'o' => 1]]; + } + + return []; + }); + + $mockDb->method('get') + ->willReturnCallback(function ($table, $columns, $where) { + if ($table === 'pp_shop_categories') { + $id = (int)$where['id']; + return ['id' => $id, 'status' => 1]; + } + return null; + }); + + $repository = new CouponRepository($mockDb); + $tree = $repository->categoriesTree(null); + + $this->assertCount(1, $tree); + $this->assertSame(10, (int)$tree[0]['id']); + $this->assertSame('Kategoria A', $tree[0]['title']); + $this->assertCount(1, $tree[0]['subcategories']); + $this->assertSame(11, (int)$tree[0]['subcategories'][0]['id']); + } +} diff --git a/tests/Unit/admin/Controllers/ShopCouponControllerTest.php b/tests/Unit/admin/Controllers/ShopCouponControllerTest.php new file mode 100644 index 0000000..edf1adf --- /dev/null +++ b/tests/Unit/admin/Controllers/ShopCouponControllerTest.php @@ -0,0 +1,65 @@ +repository = $this->createMock(CouponRepository::class); + $this->controller = new ShopCouponController($this->repository); + } + + public function testConstructorAcceptsRepository(): void + { + $controller = new ShopCouponController($this->repository); + $this->assertInstanceOf(ShopCouponController::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')); + $this->assertTrue(method_exists($this->controller, 'delete')); + } + + public function testHasLegacyAliasMethods(): void + { + $this->assertTrue(method_exists($this->controller, 'view_list')); + $this->assertTrue(method_exists($this->controller, 'coupon_edit')); + $this->assertTrue(method_exists($this->controller, 'coupon_save')); + $this->assertTrue(method_exists($this->controller, 'coupon_delete')); + } + + public function testActionMethodReturnTypes(): void + { + $reflection = new \ReflectionClass($this->controller); + + $this->assertEquals('string', (string)$reflection->getMethod('list')->getReturnType()); + $this->assertEquals('string', (string)$reflection->getMethod('view_list')->getReturnType()); + $this->assertEquals('string', (string)$reflection->getMethod('edit')->getReturnType()); + $this->assertEquals('string', (string)$reflection->getMethod('coupon_edit')->getReturnType()); + $this->assertEquals('void', (string)$reflection->getMethod('save')->getReturnType()); + $this->assertEquals('void', (string)$reflection->getMethod('coupon_save')->getReturnType()); + $this->assertEquals('void', (string)$reflection->getMethod('delete')->getReturnType()); + $this->assertEquals('void', (string)$reflection->getMethod('coupon_delete')->getReturnType()); + } + + public function testConstructorRequiresCouponRepository(): void + { + $reflection = new \ReflectionClass(ShopCouponController::class); + $constructor = $reflection->getConstructor(); + $params = $constructor->getParameters(); + + $this->assertCount(1, $params); + $this->assertEquals('Domain\Coupon\CouponRepository', $params[0]->getType()->getName()); + } +} + diff --git a/updates/0.20/ver_0.266.zip b/updates/0.20/ver_0.266.zip new file mode 100644 index 0000000000000000000000000000000000000000..ea974f82e478b9eaf8986f7d43ed5d9c17b63e6e GIT binary patch literal 97410 zcmZUaV{9$}vu@wowrv}`wr$(CZQHip?%KAyYuk2vzjJdgk~5Rczxg%EBr92uf;0#y zDgg37ZQ3KN1NiU4WCZ}w0E7S$K@~wEL1j^WWfdh=VHH&+QF>bwRTVe@#7{-MTC>He>&dOg{>t6ZL=QXGAt}#=uuE8zvY-rD6VKJXQ{*!LVM`E0c zmPpMkW!6$BA(2eoPX691!=Trg?uE)z4(Dodffb<{iu$#YPb z%$jV7RN>=(I>$lq^O=v#zOjZSvi884$6{CU6kni4EBodxh%%xIW@0Rgg1p2Wt7zIzNh0;*^)=dU+Rto7MJgXRM>nQus2`iJua%}mU!vT#Ftu)3osxu0 z5*W{3$dq@Kvr6_X_ix+t>FnX;6dKdC1ee!Kq^z8|jK|a4FwMbVzggu(XQC$SCXoux zn6RS=sMq6kbF~7&>1wZGQCY`xL4IxL}+!?;KbGQKM|nUUy^&J+buQ zmFF;Q?BUF+^gT|@ZXp4x?4vVlfz%KS_gGtls=94KVy?lg9and-dMsQ&i-SnQ9JrnZ z^@mC|#iU8jQ~?LZ(ozGNJ1R@;;e=pyiglzlW}Ou5vgr&I_LMrg0ycWVX8%RUc5BxP z>K{LzNIMxxeb=S4o#iA?i?)lZ9fvKY!(*>;ebbGnSiQ&aj7K6AW}gq7q71kUE7OtI z$e_@JyS-j7BLmuJ;d2;WD0t^_pa2EetO^<$2SFQi-Dud0YeTc54cErCtMI-SKUX4D zMie69!QZj86=M{#O`;g+m;SWT?`xqwGdu4jH z$Tv0C-L$B=Fl=2!51?S6{i;=#oLpt8Ubh#A>31`?B@NsU@1hTCj|a6#Brrci5OxT zAQ6o;ghkE0vk|p&j8HY2+g2fojKz<;A(=8n$!mx+>v!snKRLa|8+-buL{nZ?bjWdN z=Pc;t%<4$|(?kK}6}>M?@}28q4eIPK1A~?1|$~- zOL$?TZB_=`=(HTl$&y#HKOK_a4VO$vMy<3p?dhuHSe{PLpPVmoTviqsaSG8@Mh{K*k-`SMH;x^s*oaw>ce&} z@lxsM&M!~7-V2`rzfI!05Ws;69rb~q?g zRfJ}=g?e#(dTNhWNK>QP=k+vd0TQCD?nXAyLu!x7TPlP%87E(>K&!@G!dwc!UuY&! z$`fQQko8nv#GdB3W5_o)uPU{T?};m?*~3W@PBn6jvMYUNDb{^1XxQSpW@#NWr5}KL z;^*T$FPo4^eoa2~o2i5&~YziEV0x2XJY&g12cf~8*zCU}x)e&T(E?B7fCB#91dks^E zi5t*r(I$>Kqtqy_abVr8F@5R%le&W zpJxe7TQ-NNMaE{vU4D3mr>`^vcE|=?5LpD)yUh8^vA@(tBl2rQ8~at03&ND0XbRLB{?)Cl}n`; zan0R5PI&x;I=ypEg(j-7M60Sc3N$_LB6f?Wy~!c+)U) zRM}%OGEAm3M+kUXd;~bTyj6 zL~nT_eu&Mci#aGbF5XYLY&9N>6{LI3eYoW>svYjMeE~znsiCMDR%})V^H)j{UThla z#---l+m(nW*jPC8a0aGGa2q_Bxka^JcZyj!mj`+@QN3?Ng*T>QZ!4t4* zb-eaY*PQ#xFrHHWL-yvsG@A>w`g$@9%|H+#u1I6WUxDuqv)kkTU1nGD>N@o;w_8q; zA#UB{J>H<}lqS8!Y>#yU9=%nh;I)|>Gg}mn&48|Y0`GR~8=R|fUOja6m+rvU%3EUO zQYdO*i>Rc*P*4#tMEI1HU?&y@P^Xh8~d4er8zcY?Vc?f$4uvDptod_@w0caw83b=>J2W^@v#i1tC=ii2&D*O^@BGXcDQ~kp&?3ln z(xZ+Of?2~pmB!@25m6ml%n3%n;Qe)P^dL) zgjJh{4cG@<=D>kdUSJK}+kscL!nsIx$+xq40Dy!&<@+sQ)HeMtK!at1zs$a>><`WU zp*!B&ovOU<+yl{A6-u3(|1A^3cn9 z#;Flq9Q(qyu;kqi{_l}{A*8MllGT+8Igz*-IbI*Fr5dT?0zGfT-`J9t6Q})IMr_|5 zeIqNuVQwXN&Fl4%j}foOG8I+v+KHkZJ}QTR@m^iFTYOA|Rzz-R!NsCnCugW6nZ z0M`7b@&bQg6Z0XC*C!9nVpI-Yq|cJ2Id2T}j*%l&eHuXD8ia)Pgg!-;>2)vr^Q-Rt z1tX1e(u*E|#szmH#MZEH1{MCuQ1}peUz7*wd}+MaZ-vgUPEP}g$gRt2Mkd{y_Kwcg zZpIoaup6rVh0j$81Igi8dwaQ+UTD3l9hwkp+bW;EXCizRIB7N=A8~z$t)c@ILdW{n z;P9=vN>X$y@Cr2YqT;7xGlswp1FnB=fH;{zzW8#FMd~#hNW?U~XQRLvOtQD;s?Hn@ zBiOuwy6FwNBzEDa<5)xcGzklRZ#@s-|ug*XYeO6^D+lBGz@i_2~kO+LP!Vf z_&nzdzfTsjZ*+v@O4relz;^(HTIX2uec+4KZ1{_d4gj|Q+Nzw<5J^;LU9%p60sB?> z;@==)gVN5urp9DFh~I7lP0e1pg3V@fMf7T(f45KRcD7vBnRK~K>02D~7X*q8cF~CN zu>Yz{$d7mBOH3A~P2oeCf3W=ia-8pgczKxL6k>R?8~)Z2H-lx16~@G50>**l!ByVV zPGI}^Ktuj*zY{1(LqM{W0pHQX0RaDnT?+pf$W#Dw{};&IJplmJ{{WeSlDw3tu*(1B zvBdwd7@#iR^5=i@SdZ>{67HC*uQ=!rk*JW^YeD)tBVkt15fM5RY1%%l!8|Ie?z%1) z3!AR)N)^}$brKT0P|FUUkKw$3A%`|RKjb6H7tn9eZBM3VX6qAh0D5rXdEF%Mty0!TFn z_L0G=`S6&FEx2F-859QCI=%De+FFv`?AW%pjG-mcC*d3T@_7xO3N(D$S&X=vdzbwY zjl*G>S)arZpeIx``(9@lzx$IeDjB+NA2dN;p=L3%uAM28B&B` z&iw%umri&d%@7p%ekreq^O0OiOo}y+8Qec>X2kW=42O!ydOvR`a}9h}=@kw&(69em)wkrqzGRW} zw+#vKuI%lnGMzAGy@+bq4S3v04rmzUjFv3BBRo!sc~QBt-_KhN6aa3gLTe6SS0#*W zi<~o@urEUGo(ii~sC);K#6< zY+qZiFSn=GxnP!eOQIsrECBq%Lwj3JmZg$zDpGyJn>BnBNKQHMu zzNs$1gd7KsZuCJ+?%R&YaTGjxkz3;Gk5LlXJ;C^@F09h5lT(dkUwaT~HOX7xe0lwV*sQAc4j^XG zcP1n(HFXESf>bLPC`4l55zt5a+_Os@1CxU9hzcIq1EKBu0|-j1`w zuFCKAhqIT(3B$g4mWSrMHx#0M+;R{zUP$dzPbFK2)#m<1KAI*qcbAt7%g=-5)mZ9g zc9Mm!B}qpQp%;cO;JPzSUR%N7{8m9{X|1+R;nA$se{k)|%X_8DAGA6dvg$*4Bb_wg zxzl-zmNMPmriwT+8A&+rf!umBzfe~vas6vldYH+8UfILn%Ms~u#8x4+`Z1jSrk4?s zS&JB|o8KPY`dcnTn0DkWu*tO0r>fj^n3NSCn8uY^6AE+kFPq<3l~=abbFo79I)w0V)0QGyQ$q&Ub2AVG_6tfLIqZp2*1>ZW)h-jaq(5>oa6 zl|d5lHJ+ri_VW(}k+K4N#=BE6m5T9MGr~J!#q4AA!;9+d=lyLNrI&^R{lkDb=q)zb z%C zB2s{(#ye%DrN~JG0(O!Qt7%(H@(Y4nS1qjqv}CZ-J?#J$j(s7l?Hiei(3i|%fO`aY z6dZcEgPh>^hdVQD?RPK1*Lj#E!weZnhYO-+Irus5P9IRNiP_OvEldX2I2p{EF%dUv zOh-2M=pz%MhB!P2i-?CeZ&U~dDX|Gou73zT2E&K8OU?TAto!u{itlf2s6nUL8d=lB zc}0dfYkR^ZBjbp;v1?%{y$;Ig97oO~aE_#?PdfeBk4UDW}vKy?!^K1c=xsdH}TApr5QY>oT+vo zjn-kf)(!BF`Uf1_xWbyw4bWyon@X++<5zGn4Z0%8hK=l1chsw2HaZA5fuxiggk0h7 zj{8fCe3X*nP`caR$ek@L9gZDc2d0XM39B&ggSptiFua85n|wXgpD9Kz+zY1l;SZto z9IiQMO-V1tGtlw8DAk#Y{eZ^W+nP>tzv8=HPijRg&8$N!(9&$fAUP@ROVADyL!_SE z6CE^Rjqf|q7r5KG7cAqeDoWXTP^adt-fCnlpH2>H4NN|m2rXvN(T#IY5??izC(;z* zKPHV7us$fbR&Lj64mH@T*TaSfz!|F7zE(O`S-!$`4e8Z6m}}_a6FMC`3p&-m>vR$v zRuo8s`7Df7>IjZz-#?hJS~*j!*Ok1u(d!rMR)yJRRlc?IwLPCTa)Nasuj(}sDpYM< z{Pc`_gR!3&vhqUIWd!}OKSius=m77^jRGoX%NPSXC{`rZT@@#>aOlS2!>+OOH)cIh zDel}4fJBHk|0+xImRUA#4qI7ShfNh^c=^Mk!q&hitBoHDCqPb|PM$9gF^DH}pglol z){n{c0Nhw~3$R(hxaenTJaWM=jGblj9avq`;3a$QVj#mvyHc3z#*B7W+u4WG*y~}I z|GXt&FWXxdeZV~U!L~<#KiU7*4Ost;l#jfc^3#0Ob(Wi_Ni2>M2pErBhbsd?a-}Kc z(%uBvmgYpgHA|LJF?LN&!*md-cCB6lK*ZHo{M{Oq3H63S!BL3;>!Q3I@feWF(eWNh+^D zV>I|U4tCiWO2E2(iqu#PLTP~+riB(kC!rLmaYTlXhCT2Y3epX6dx?iC#2_p$f@)m-I z#Qvz}l;OB^vQgB5Cp2+7omij}5x8|KxS03LQF3~`=ff5}W)lEDz=@mD2Z&x|lCSLj zBccD$YWJJXchSxXVZM&6&~$;kRJMODyk6kOor#I#YK{?6F{vp^1w6SYXV3#Cu|gBy_1>JFs{Sf}=(Nk7li0C1xb zBCika35k!EvJ=N->HV)>_ zTKws#E1@?+u4x6+m3SC%B&vdW-7kxouICF_oi22e0jvG<=9t|Hh&{5OvVQ zI6Jk^S6A_AV0y!5aPYtmp9^ph5}a?tx%o2Mdrt$YAC`=Vkm(|2Zr&mM!QD!PNuwoa zI2b_G3u%e(RT_edd39mJ(O*Bl)P=O#binNZ+HSP4wSX9`d`%$K(0I&Y0&Zh#ory#r zEGEpg--;s`?ppbeb>Kn+RlkfhK~v9*TGd`NI7>B&CEnC)Eo8FlW}8BpZcyV2pnm!rw1Lvjis{)`k?R&lR8SVb7jauHzz2EJWc{&H*RX+d=|~aQy@p{8(wwE zD8Mrn@6ceMp57xQuW6^app6Z~yJ01l9 z*)W~Q%YFUV;^~|?50V3ddftX>4Y6jrDJIK(mKddHd&BE4K_~)9oDKm8y9vrnxntTB zvm5yRqG?Vfu2;IcRA{`@onV{a=iv+TQkgQ-O9j6SZ1NTjN!=2%g095%yq3mh#$6Ij zXtnI_P-;J;-%FApcD&lHmy*P)y%aci>@^>kudJ36&KF}cu{#~~hcQxoej z?-{uHu$bXSqgUYao{xICSD))o$Q_fc>xk(zjde^PRWz|L2YWZINy$Ke#kD56(iQT2wDWef9?nyKf$LOtt=9r^E_-fW&-e}=U95P@ zb?8Y*jn$M)TD@V?lM8Rmg+y zQ~jlqG-mww`Dfexz#q+T7*{kWdKUba`{<~WgQ};!)AWgQ9i&??m;arzu6hohX^l;8 z@}5cGX12L%0dom#bQQbiP)pfGQoYpLavj`=!4Szoh81P{U7T=H!k{SJdTDxQ(Pv|Q z+uJ=$kZ7qOK*F|BJE9%r4H@9D=` zD3V()BjqkzFMq^0!l7Nl6J$hEp4x-Xs`x~rr`;}0R!PH;f5d6ls%AROEz_Ic!wz@A z$qFtfD|)AGl_SlB*y;o9SB0!c?N=Q5ZBKHs)w`8TH60(`Nn}iiox#+g=``pMCj~G9 z_?Te$=@&C8W^i)T-QTe-;FdUgf%acyd8F5i!}GVz*Zkk56|!qbe>jIyyURNvgipi8FxlZ!8SxMut)n+J$kOiXRE#^c4qlA(l4 zQJTxGf9q=W&33^@_FAO+A1rof!h>WB_6&)&G`@Un=T%C648JX;Llw2Y*ke;#1?3A5 zEo_AraLMc>oT@J+cTW$)Ib^6O9qxP?N`poZKuH?cCGhLx4;cotv&3_FI>RfYxO@6M ziDa%@c;L_?2EyDk9jnkEKq>i-Bwox1uDpWRJM2o22HMiHCFQ=t^1d&TVy>M}5rEvM zq)KOxMZ-<5^Od=TYKWdmq{|f(YBn&NmZX?4nO8doA`;DGQ(m`qLQ?4Ngcp0W-P-aB zuM12`E_o46og=ovsju|Faxrs|K{^tR`FmurrsL8an`vi?@{b0lN$r*5&E^<` zaD$k0nSmP3=tkz=pXm*r&6N4ne)^qf+;GM-gZ_3=hR*q#_&hwDoWy}er@CDI)(^2M zUjm6cZTuHIky1%4_Zl4(e=Vpb+MRQk*mbk;W0C6*mXiw<@R2T-GR1?&U|f}0Z1Gn} zZwq`EYvz_~0cod`)w{|96s#8(utNP8) zM7gV_cx!y9V;lHSTOjMk<0(b9jgvsMM=qgKRf?S2jpjrGyufetL zt)FRu%Khx>ElD*nj9O^dGpLK;u?kPhG-VWf%a7BaQ;gcn6fN{n&-->VF)~X#w8aig zm>G|i&BpnJ-{iDNPi9!~%q?P>{%996(~BS$cRX(#Rv!~qJrI&Nderlzt%X=TqQT;{ zNzYorqxaS)`tt|gjjd)}R(}P@K9Q4k*4<-%Iz=FGY_d*81W$BeZkXmeNlYd9+Ti4Qlt)beAoy+r$i7 z&uC^ozV)2=MPu2Ns{Xj^fT~zwPiN@tES))v4G}zd&U$h@50Ypo|q&@$gmyGeRKD`ygWCEsbN{RvMwaDsSx}{gtiPpbtN6Y3euy( zC8G#PPs+smhO9yeOzK#Dj|nbOJ)pJpzi>R!AqmFn+pSYn7mZ1I5FysV?#2EM7Li*k z>ap7^>Y+O{BBmjD&!#Qdki6M`sP~CpT9v8X@G|HjZp2&WZ*Tbpf(g}w6e^nwl@Y-8HX-LFS+QFf$tL2ixL&dzA!PMj(wu!m8olXn-@a1Rj{YH zGQj=b4UVXXNEX)l56ki{_u=%Ja~)cWI{cJ>?t*|u(X&b<4(ACEb&0^)IwP%^!T48c zbkiBjBGjhRvWMA!)EZr2uvRmg=)f!LBT}^UXk=S5ArmXV#lo&{Q zjweiC7bMB12)@CM4s$+tWuE7f$%odYg_bTU&LmlsbWC5MqyTYJ#QRQ|=Llx8^Tg#P z(7nnNjmHPIu5nfnd^f!@B*yWxvmDo&$_>q1NL39~QwBnkFl(bVpF&b_){kOG3xH=3eaEDknD()mn%hI)!h-%+Dem?N$n3fDJ89!>GdD& zZuXXK+jfd56cVR*fkmw+kBcmnL*PHEdRCMLj$j47W)^{O?O=SWha+a7mxK;0Sjww3 z_YIy!M2((J&}d_3kp2`{`uCX(=_9Kfl=5Ra3@WpoS{N+Cn4Z6D%ovk9HLGz`EVynE z)uC@*RhB3RwRr0fpVwOc{DF3C-Fii9FzTWo;-wOOzEjBOI4mavZZO7m;}y|^#PlK} z>l%ee;HiTLR_v*k(n$E_aVqf1&@GV`vx&IAu;O(mVzJ)ZPBp8}C34Qn#tj6m3q?-F zlIGPTh`QEoT+?djc^mU-Od__3<0xJ{wbS}$4VN|5udT8E;6k;suElo)X0Eir_SFhR zPR=_<{_cH}M{0x8qFAs0nQ0$WT{qQMx^v2p}!461P+0H1r%qUSpM%3T1SfMo@PYkXQG@P?(-NlKn zRz>uo*N)?ljD(u** zveUJraaOC@48-&9l2*9Iv4(58AEdMOa3-SV7eHOALKWZNJ+VZ~_?HQvZ8M*)q`bxA zuJslWcu&Hp4l?9B?FB2%3R3NPBfGraX07T`7N2Qrt-V#nUYs2v(%929h2lYO8qVyM z-BD&@?bJ05?_glI>~|K@?h!uS@XnU=rkO;mkrw5pNar2st3p7iLHET@gMt+x6-Kzx z4l(B&a&+VyZK%>n%3U`VL}kS@MfufHs{1OfC5mFvaK_~V6|1_;9&HkByUl(Ng_{M2 zT~f!=E8*2BljrW#l&~HyupysMjIy)dGP^ZVGzcSJS9fPbf6ykLy8mc|eqaBn0sJqP zVT;0<1|AuMz!D6Y{)x)$Dj#{a&i3;=sC>9aZHL70#xmCH;$^CZ_U|S97 zFiEd^6NW^74ZQiYa!2W{Q`n0a2F})bs zyUE9B7RPehh~8hYrtrc$+o87jb&u8*_PUF8VH)?u3p@Lm7_cs^t}e}Kn7fyE3mu%$ zZI;~H4KUXoIk)0GCL|?lWBT?@oSAw)p3~h1RElK=Ht~AeCwyVyJoGts8w!Lq3svJt;<#p0lgBR%CW?U8jTYDi${)GFa4_S6M4}6t9{BT_j@aWu z#|AcUM->3yX5SYM&{&2n7+1jlz8gf5RlP!=>9K;zN@* zUFa043tbl0AvT%36H$e|c8X(S`wn7jVcCmZCd-YMZ8nk_4<+fbF3N?EiHk4ik`aHp z?(Yh*;Ba*hgK5Djs1A!JE2O0&rK}`aoP;=)ZUTMrHB0d6E3KZfi65`|r+Xmtm*H8ZCtE3^7bKKU(g=KdI20wg(fR zZt`9xJ0(T>S+7>2uFp)DFg~F%&6&Iqv_VGWOcHTA_Z%$^NO7Z)kh5wi$OZKkOLcs% z$y4xNG^5y;B)YF}m9>Utl3_95mA)^MRx~>E!`zg$z)5e5UOPp)OX(tD%@tiyUQYh< zhIG`n#*%t8dz443H&v{@vIe%bS2d_A$zlWv7gZhp(Dz8V#S$a2KEI65hr4;wFX(o| zIDWbszbsh)jK*4&C}^q6?lkC%-2m8_{s!?LN9;lNmkf7|Pm4q|Z&QQ4z2faERvevo z{{QlBo;cB3&`AJ*|FUcTpLe72|9Ll#BLD!(f4mzdQ87Vb6?r8|IdOdj89}-K6NAY7 zZ|#OH?tjD}j(sj{*b&OSY|h#fqx1GU`Sr4b|V5!GBN}}%_osD&PUb(E1X@_ zx#|A9YC~ljYFK4zjDx$aGa7=P8~*9e*$62Z#YfLOw~-k0hhe>azlbF z=%Ri+x<2YS=A!T?eG%qDm;YAgk}39JPY;e3AU$0$`906Gh`zQU4R-YY1F!dH+=zxS z8@x~Rwh!Vh&<4_Vx7fEkf&!1sdU;Y-vl zApy<|O31>L>X;;VuX_8@+NPm@3f^^jInSw@Ue<{kD~S3jOW%GR>%s+296Bs6m^6wU zM%?i)m=nAF=by8*S^DnGq9H3Dh!4HH1HG|+x)izp&(blCJ^q^^>&uA7=;PY%pn4^E z4<6heTz_C2GcIJ$+3V+8{RD07poG`<}otum6R75ov$xwahHV*4?ZBZ|j>6YRat&37K4Q;~Mh%aB^inMoFj-P$Qi?nG5ebID(pYKiNaeJMF*WpR{H63Ye5ET8;?Wvx63c4;Z6`NPY-Fz-j}> z$jjJqo()7WYlOkG!Px4{D80&=Vh2BwO&^4!Ot$hN+zV4aDpuIss9=_g3~a5oE&rMA z2(`+S5#k4P*#lbb)kt3RKhMibfAE0X|A;(t<-!Qpyt2b(4W)%N`D%;A3jy&0NDO-Z zycv;&GGy~@1S+yX3e}v@!u)cb&<7|b)xv%^AQLVXAZ*SiE8JC^LE4Vk{oSOVnmGfV zi!qPYsjWyRt=I@b^8^!(Jld^Lt?Pd?rfKVxw?th_*{*>4~`l7a~)ZHjhG3acgz+he3i1)g`D{;N0(SIv%!P*~; z^BtDxs$jlT)W``69mFlE_X)nAqQ(E@+id?HKMq2ojfOC1?iV$3q5>^s5B~rlqal)M za}o<;R_ORlii#8^`&Ml850JI(CHDjZLCriaAN+aO&m+SZJTBrPFcyiCcYnuzt0dzr zc%VnurI`{UcjdmA5|sYQ^Ajdk)r-dRt(@UVib*o~F5Cg4eHvp)DYLM-%dFBR6_F6* zXGiG*xRTQAeTaa}z#>WK<=xyNeaj!7?F;b^=)}u7v}GNU2P{Mn#*_($xnjgn{`FA^a*_)WY6d;=gm-Ezj$&=Wq}P+e>gkLdf-Z-6 zfgdo>KJDqa+cuN5eM8weYaC_%!_#N3VT~sHbx5G#Jd;Rm#2i0;Dp4D5x`9e309IZ54foZ zI3t}RiaH|)`4(Y)P8ChYVy3~KxLwJ|9u#kIQ`1?2&~VTo!UfK|aM!BKqr-!7W)cBe z$>S*9F~)@%6;tr#=6=1)JN~rA$^#Q}RG5)g%ECU4DrNg1PgT0V^N)a8^f_aFr0<@a z8WaH=DPT@F!#IXrAw><)n^|$B$uY2dH)t(IpxNjU11g^9u!OwR&56J`ayo9O?Wdw&3cgPnbuHTkGu~;@iUogu$RyBP(cvyP0N@ zk@c!5IEyjHr`Ma=^brnN_~N+`B-9}G1-K_pFO*reZGpy%DV-bZ{`_TFT8HIn6m+g3jCQAsk`WJu; zUPB%L>goIaIM$WVv;FCEIRPK%{vgqPV>xl)}D|;uXNAu;(H$?Zt}W-sL{g03H;| zAx^rF{Bm*@i9v`Mt(`lwd3*UfX8;2aX#VStH7$H%ZE-5>&>W4l_`%26yvvEJSAAbTfZb>kbt`AmsDO+Rh^FFbvh z%=s;f)p!<_7F&E~kXFcD;)_q|;B^6LLjno-CZ11=&w<8%YsZFOGasRf0w?oY->WWP z0Y^RiqEUM+>s;2nY|IJ%n+ZiDM?o`hK3M}k6t}FIDzZAPyV=VNy;SBT0T*L=MfFrY zlgv=7t0U3ro^+Q+wrMVzN82(<7Uw(dOk4JaE}(gknX}&0SmT1PnJX zvywAo3T3{=99xg$Sv(n0WV@t#wXwD{NK4`BuqbGJivvM>Fj;xIS45wo+jC8)ZAxJ; zYt8^T$cEG|%KHg3!kbUdQo-((1<}~=Myg%skL8zB?@P< zUWpZXf)Jkkj$E2KA*MHUH1*gV8GOh9>SB8xz*F3Y3D$==Cs@h$hYFiOo^^nb)ThbS z;q3$AQ@#yw{4=#nlrJ)DvV=o?{4#sU%XH76A8V)^6OtugHL@Gceo?)c2wZI9ATC<* zO%22G%Nujd2^16DNH#-5=)ExC4w%Rn)3g%gf>i2Ti~f()1f7MAjNE*plu6+-^dy&k zGp+(a{#S!sjm9*jIQSzWh=)A^6xCCp4U?f6pu9j!*K$YUg5{`C z11J(W-ou)5v17cOCkiLIlEh~u6^`@GlJkzNN;Gv7Duw5t$o!+*bO+_}VH9t+{pCM; znfBwO4c_TY)~6o+Qp(=bJs&v6aAbu@!czu8x)7i6DNxz!7av8iN+SgVlDzBtdgPbw za=r!r8M;Mw^6@5eJDBZ3cH<-0gfStT*n)EPS6EWMz}0>)uC&N85j;D8DYBHF4;FXw z5!;O#PRhTUVj@Es9d8`SuZ)|l4c%Q;3C zoMx{&j}1q&xSdH#dRTZ*bTR+8X(|PmqL(D7wp}NVzCg5DEC~+W4_Mj> zx`aA9oqlwC(MZFkW9?6%rPP-=VJDql!mAY;S#<7z&c8b;6@7Xw!aB(Z>Uuf&iz*tG zl1Wm}kMkNAJEK{u3GmGY6cI6y0&YrF94v@f1smguX&H75I;@#!5qqfUeGY@gSrYj- z216aFd-bM4K1OVMt985Ui##MILL>=F#vau`bF6m6G&j0X!fX-p z=KTd0QApyZau5RQj|Kw}M$oVXm9hlmgZJjSb9|pyZ>aA&6uy}!1}cRzaEKTmg7Plh z+cYndn#_VUur0ao(-8Zi+w32?&Ki0phTk+YC{!IbE(a*OEq`D9{Tje1U#ekEt4L!7 z>5Hf&iZWpF03SsYw;xbR<)@vr}0c%$@+VSLZJf_Ee19#&X6C&tsC?=?ZF7eYViftI>|Uw5i!V2Uglp z-TRwvuej`3WV86HTvyewb$_Qd#comKN_96MlUs;`Wqb#*WDI(VRy$fZ>bPEkNrVR` zyu?GB6MTz?1##u4v`E~m5f~~{KJ|Ya>4>C5@&A2FYy7UfaJlbC8m-Y)xvu?dVa2b( z*UCO$e1ZO<{`9SlbzI#ugAJoE+A_sNujgOTN4mX*EOBYq!xej=nEmt? zs)TLxCh=}R9sLP*-8XwP1~CgIV#ME zyqYK4q?a<@V3=SkRPq>*c`COI4y;LdnGF7`s-sZhS1oID6%JTF4>%Dfv>aYL?25V4ngXCW1RBU7 ztOiqZ<6DEw2O}yJMDvy%`{nQF&C7$g88B~!4WNlQDRg+XB!bvBFkGrdcy~1ouPvnW z9&z2ILKSJti|cTCF5FCQL+91X?vt1CK3+YxMvH1z1GI|_7qlQrk%fP<3bK($TaVB0 z{LD(C*(a|CdhirtRIq*_dakBJUxe4Ze5c6-5yr{w$)WI(GlL-K0l+ zAZbihmIbstuDCW1C?9F8?$>}E%^ii}XpMj}Yef!*K>J8#)Q0(f%(pWC%eayK26AAU zZlX3$(i{?}6N~WyI}@tBfcR1Er$flauF=BJGVw?Y8dpOum>JA0Zoh{=jMWykhB`K! z{2;7vW!FA>J`OYV>h$%(yFz>GeMfhh10Jl9mfRzcpUHs#p>xnKK-zYCpJ z*^Z^zw(tdf2Aq>Hydmd9sUpu+_;>D2p|Sh-2Axe*T!$%VZoCzE5ah|Px+cPB!S>4+ zI#3Cf@F~vouDu}loI@wyw3_XD>8Ohb`EO3d9@fj7CQ|G?EoN8~`P{)=l+C@Bize$! zC4HIrBo!y1&~m^p6M{5v&IMHlLwUnTPZiZeUd^QPWrg{!N9G@=N=RSKA@-O}A7>0f zRf=GS;)_DZi0b4^6-%+nOI7BSxUF| zwkQv3un@+_sfmSFN*ElVLCUP>DmMhSndiPEpe!!$U!8;66-W@uRfl%gZAf>YR_hjnVKG>Ww3bs_lDz zmEES&7oNBYZomAi>RS0P`&S=`sOi8CMJ`fjAY8oVbS|b{w2=@P$?g(Yg70a$K*9)Z zm{Y_jGE6c0AwidfJ_P3DJMM6v7VD^j##KFF6eq`AkXm181)f7SSf-y3uB*W}ltU0W zeg1%%0s?_rgE$PwzydLW;;+b|wr`u-+Rb_z0!b2``Ip4dduaCn|D+bZA5(Fjct>9@ zFf5K}44lpiWe`_AgE%gn$a!mnnyC|tq<{04u3#Jxp+s=vY1;{a-JvYw83p^7$5G1M zl8enlsw&ot}wVf(QoLUs>usVA!97Qif4KO`9or zEYMdzpAN_Gje<9nvT#At>6|x69~F^(T+#Bhv25SB?65lnhFkS#Uy> zF@c(z-;JRquVSZXz1>mYv-HnoF%(?#4o&q&9kbs(i%+@982N4=Yw}xSEk58ibNX_( z$ZxvoQ^`F3zriBDHiK8>=MZPzJ{O{kj4z`qWb>oJ)=~IuWpw?XoKVTSLelFf%uTHS zc*CdCWD%Doug;6#XG?lNKN0&^5{B6z67wDk5rz$pI|P;tXw)S1&T)GEb>O#z56WpG z8a+H+z|8QJVJJ<6=yHMs6pGDs`|A}5TC8?kpL__;UhP|cimp-88qex(8k*IUuCr`d zT_=C!3X0CW463;|U6gL4Z=GXhtxn|pl|>!SnR zNsT1!P`A{kDn}MS_f};-e!?~qovo&rRvRc5<_AnaP>|B=oaP>>wPpQ^D|PqOBu>Af z>8%PFgx$-esW&xoetPCwCF$1e`tItF@78x%C5eaJTsA*?>sg?L1tG7hsevb~bn622}qX z1T?M|kd(XgI}kO^D>p$ZI8(`s*s$@0XIPdW$|<7Uy9I6`$S9Qb*`V8LiLJTLYr=4$ zUWl*r!?8zUCw9L9c!7cYv#M?r-Z?$E78KckPTZHolXsXGRN$VIDwe84;M zrkNl2Qc2ceqOFxooW? zL0rHv0B_d&A{5Cv_dS1ttpNY1*%)L#kCT}zi*Z{zN%iMYkyC%$iMn!heul`{$yJWn z1AVx_P$U=-X6j?K_UC83V8;X3J-)qgD$LsJ8Iw!0)!6(4ANe5>i}XLmwKk%#<}p`&FnZ5!R`uR#Z-HSRiDr zw&c>J31&v`-|5>mxjWmt*H_U_i65i@2fAP?@E4}@(*_C%9STG zRJYx3=qB&eUf(fVW>cP-z~7(kUr+3A62h=xe$Itv&@${X={Sd5Ch3GRJwI09VlYi~ zVyLi25@^3J{c>Qb>mhrTLL**i#HlQ%qR&W=2B?njL%rj_U%w$X&I8Hj6sjfd5Zgbr zGDEkb;RC5fs-Y+F$H%TYvkuM7XG{cqy9oVm3B{_0c+h^T$Qsa<6vA8yhR-pT0m0mJ zh$M`i!O*PU(| zLorw(nk3?3dta;jMErlWvRsBA4D(j#=!c+cm2U_#^N0gg$5h3ujcN;CT)wrYAIJHmG5OgtlUs821YSC9yRCq&W=t7l2^q~fzx0R^pFK@Qyaxg%+k zx367>PpiP;i^n=O=*$^9R)sURG=zb<@Zgdn4`#I}2N)0d`1R2?G#UeC5z`%6YB5_u!!S5# z40-YnDJ_LwR14Q0R==cLhI!-n~-K6O+HmAQIoOR zPQj>7JY+emnbM^?-Vja~y}Q&xeXv#pV!#EYQdOy4mg8=6M6aMt$TrNi)*9)u)y)#c zLlf)%y>@I;EsesW%pp5pJj+!7%&z+M)*8|RGJQcGbt4-{IkU&H*N90uY-sDcm*l;0 z#J|w4#(mjo{KJ>_IN>$MsXx#*SdwTZ5d9YdpM{Pec5&HJd15nUOJ>Ulxh zZoCcpR>Qx6A?J3>d@6M4k?0vskl!lg$dv|m_6||!vLg^+(&oES2shVv`!X_Qd_DHn(#q67oI<(OxQD6TCbNNm#z=9QWz7eX>i>nH_` z0xTQtp$Yqj+(KaIMB=zA_}2{Jd)ed}jq&>T4C>!^{xLsrDo!^2DZMOZ$U??sM&rxD z9av=zar)hdLsgv%b4T}Q6kn~AdmP*b0@Ah}YAlUKk?_(A3Y(Qmv{gT?@KR8znvz$2 zMnBs))z7ttp-^iM#;K8SKz|AtwrfT#R9J{7mR5mKFV0!v2E|XK&l+1Qa87vq>p@c= zNm=6Yc}S4fkcggLr-)}E3;uBwfM)nYAAvz`+w3D{KaHPJy;ZYT=7YW4KFr!)Hwj#u zojbMV&?Q}&_k-lK5sTm2+G?M*iP5gh=bG6um@A?zu+C{poF~&|tj!gvvcUZ{lx#A3 z_7h>mQZZ{1j_M0^RadJy85;E0`-ORfu6G^mpXGf2P0xW(OHf zAqI7>`BZO9Kl{l&cS{%VEU^!0bXfVof(ASRJB|7Gc+e;=kZ4X&EKUDDw(cOWCnhjT zN9=_-e&0=O3l|{hc_|B1j&d^k{c*y`YSIo!>RwU++0+C%G`Jq#Em?k6*jkA&6+kBM zTJJ~fNzsY(^a*hPRxjnOr<5V3(i+Tujm>-V73`T%=IJ`Bs#8N{-^SA9RO@ z_|RV}1|i$Hg6i|5J`)(*w7i~XJa05!(@f#DI< z9KgWud{DobPA?o}PTpA{KbJGt`QhJ{cpFMQFh>!3P)`+xSh)C7+A_9qAey+~g}EU2 zfScmo;ewxd>AC6vPA$@M*jGJ&0EcZ*Ogl%pAJ&2_ zG9hmxc~Bktvw@)#fyl7j<60fwiL$HOL1GyfR(MMB=9v~s@$CBNUK9^UioNTT>9L{) z<|t@P__D~%pyoQ621O~0O&RUAIUe8`%aGv2$R#DgCBOz=&4ds zyrzk5?y-lrUdo59^TZFw>JW^{?AgSGrR0;j)o3*CPt!Ssdy4)<+i&nn%}DMod6NaH zuXF&4@>4ipk84|UeKk#YU|K}611iZ?toBe z27V%tnL+eK@pqTS`54QNWa?gT^Oi==+H7UR321b?#O@0S6L^efgmfjf;Z8QnR(lvA z*F+Rc454a1)80>@_m4`h1(Jd;hq;M zEwcd(Q}QN;_=OiojN#m9rZFg;BsD+4KCOH5NRL#SDzvd#id?NaVnd?QvFh$I7j-`!5ser7RC&`fqvGy;l z+71HcSt(vZ4p@{fTY2)8yIqFbpU}JmN^c2~9h4>+Cqxxu^R{zXtzg3Z*;|jqbW<3(RJ%lPDKHCMzV4C@p-K8(9a;(#%Gpc0bg1 zhVVi{MPvQ1Tuo$s=908fveOM1TH#)|m>~V)}Q6^iwsXvb3^G6;p$X-D{6BU1T$$YEGi?tmvgVU{Qtm1O(dQ z&_Ye5g6ZO{n~u4Bs-kKsWb6H@zn;>MmO6&l^?0)F{3Z_dJw5hjg9uTZuQUPqz-j7v zn70=91G!{%naURDQD(=Pjq<3fV$Zb4e7_?Jca9RLgp%cahf7|J z@DZ8varw>EYEr?Ik6|t0SySIsStWl7$)WgPKvQ6kPh{ zIx-t~UfFi7$h-CG6SvZ(&4_tS`Xlj68{-m6{&7uE^viet@Nc?JqR!+|lVO8J+{mt+ z<2+GfH&p;i`VFa6ny6986VP`Slnx z^O+cg2&WS(%wrRKm~AZ?&*zJX#;H}H_?f(|r*;FDs;?3mp)!>Cb ze@Evh7o;A(6EQ?;=n_PpInhaz-o{+p0n9R1w^aqi|5k3n9RgEKq)6;f(gJdEnWaxQ z^3_$ucyE+!9VQHz8zoF4b}ywL{#_lJOUG?Rv@3HO%!6`=#+hW`#mh4C!PTOChKqeW z&uETt)uC@)<#t{=^g{SF2I$ARsJcL7m1{(~2t<|To?VqDx9OWMp7k+3KGwO-bYXnh7QbrKr(i!$?|5KV$ z+~Yc1x3r1RLGwI49%PP^QlV+4r>&bJOdD#Gve24%Cu9G{>|R6al`fUb>m5}oJqQnc z1|2*v>P8D5FS$RwuIV=;2SPY#!I=8TSTxwxM35GJv+Og)-N=8%TjaJkk=)llaScm(TVK5T=+} z-P;)>UkP6!aALl{hyVH3aO7Huzr~!~0D;uE!Hzz|z?4WL%jGFb&|;mrx#qc$r@3W+ z2=l~RHg_vE`__aI4I&k3f6vlFK|6d()yiivvc3~$-eE#bg245^;}9?8H(L9;Ml;AV zr((r_QB;^_uhjBEz|C0-;Vf8nj&jY^S~9sY(Lxq@m7YFdheNS3jL!(C;UR>? z;#OR^%18!#I-!SW^vY`q#sb8M3JIjLt>b3*P6Ttd<&{8YvQF@@M_J$cFVo{RZ0Ar* zNmlifThSAQXTcvwJ$yYnRfu#{Csdg><5?zhQ;s-r;p;yHV^!hy^KF^a%ucyXK#qla zTkLZJi1nE!lAf58R+s4wia}|mmQY@U-I44t(zfQ-dJ``@MCi6AuR~E=VKU(oM!U8~ z0J%yWmenkuk={HWs&aTQx=ysD9r$MUZd3FS%k~6UAWV!m1WnW{>f3NLDA676CXNfg!PmZ^_MEjzjW$QCp75eZhu=C9U@zH^S0@gX?bpUFY~1{H zcxniKy?4>@B!i^x;X~qqrs{ge6#$TN-U3(}LbzyK5c^1w;n_SZ-fEnD)#M}CN%(^c z$~_F_j!W6^=ZrVpP*8J0p!g054~!wwBr}ozQAP-CvB&xdCWlMz?EBo%P|a-?IpEpZ zOujXXst-=8yrTkz0~}EFj?=j8L|sm%MQ>Am3-l~3e=>jooh%|exy8C$x0dZ{KIo!j zE=^z6R?VNH8pOf`< zx#`$HDZgoZmYHv5JMC!IEBgd`lZ^Y&-TJmh!`wPVpZ5W)XcNGW7SJU_cH^h`cjXXel9bEPMR(aaczq5aGgS7Fi;ba2u^Rk zw`1N_hz7>W5$15}vLpt;;=;19y1$!StL ztO;{Rv`@r5Im&s$E+zCJ2jmDI7vY3rfxhwqcJ>|aEL*c9&g3G6+&H4x2^9VQjuh`psT#32R~Zp6sI{|P;$O1!C=pSZ zc*;N_@KpdUmVC<_ zMc7<-lj?+fEbvAY(M|NnTdN!0RUCM({kMi1jm~Zr34WEw>||`tYL#`gwfSf`)|f)L zDg{+w)zQQ})2B@{(CyH|V>p*so6Gtd=BXLcO&NR;4S1&$=Jd~`4XzoE*9V5x9*W%o z`dS)nM=yaRo?jj-Wj^+`>Na^l|kM8o(>E-5f$MN&PGwBPP6Fa~!RFJ6vm=d>mdMS3moW zrQF%^;_UMXwe>YZij(sL0v`;0mLA3u4Kbz-dHg?Cr$717lHHyikNTHC`#X!<;{6daU$D4)6+z&zQmEx6jqQwY(cA0prfz_pA-XFLbi>D5(r%jr zKxmW3hl(h*T71XJy--Hi^!G+>Oq~|G>GMrBV}?)|`WkhXF|8bo6$;WC_RrgVd`h8DP{{$ZdrpK#}pqNe6fTLN4jGlOy8zB9a0_Be66c z%aMhyS6p8yiigjY$%e;>fy|$pqLv(@9B7mFjNXOdo7{u2ub$_$BFn%2yuCZ*gC`J# zV^+V(%qVP%bIxU?3wTQx_Bij%@)1kLtboM9fh|ztp z?AtvJQ~8S&fBjTt5&rKmn@TYc9(m-Ik4w6p>ce`={a@g?V3I5i^pp}XOiYvI&Fk%a zT&k9_N3ykAhU!wMRAz*Sx`l$`Z4+zo$MJjMIu8BAn3BdkqW-w~NK0ZmmRZclyiSp-;q<$B;%b9Ko}*9cp=o^GE} zg~HP_;E}c|S%Y$fhFe#{{Am6Aomc@f>9>}q$6SCi<-szB-&VaH8i)zV1$_dEr4=V_ zM0lXDvcMnC99G@8p*R^}_k;8cywk|EO`DW8Yp@8WuHgBR5nO1~)ZgVy%L^ICqfQaa zNbTL7?wVk+-6=E;UbRc*x=a;K88!Aah6Tk^b+#0hWfjzF5;si@1RSQ6Dy7|w z3nmbzls0pd@L}m7gHU!oc7-I!sGCXXCnt4G{X3dpiRrF2N4D9*9m6c=b8~Y zqeJ6$szK*Y(H0XcO4jDeXeQBtGof4X|DaTrYRD61Fi5E2=W@6cziBB#L<6T2ople4Svio1`XO;c zy=QVi%^mR>u2(;jH;nVc$PuwmUKwfO1j14im323cCvt4c`Uz+`c}h$y27ccJsTK5- zo>5NVxCsH?GPVnyL!H<<@~`m1w`21_7TIx42G;qs2Dc}i#z;?AE8LqCjMsLYku>J7 z>1tuG9jooTuyo@M1HCKWrS0zSrp`+1l&;P822Fx78D(JSdfWZ3aU^~8aVU6Pz%7a? zsTX?F;*N@=-T|-e@+^82u8V4+t!gjo>g04m&#HipN3v$)I;BF@lM-sDe0?tmj|b^^ zn8S6F>uSEV284Y>In;pTamD3nPgF>y`#Ny9IB|<#C%Cwp6!EVUs7ue*rXG__O#EGj z*-uL}{vDCMGUg7}35AL-X#rO`6{OnaV8lPk*`>W+b)R;ZKeH_!S2Xz|v7iCsm$D|X z9orC(sF$j}>fM#^G7|EK%fs;Vdv}^)XS&T;jg<38YuFQ= zZYSrr^}aK@&s$m6P`jkss(7}+Se_^qHo+4)1xFSqBq$~(9<;%LnKOD^r`zQGBJ4=i zcezMBBi;LoZ8tfPeryPS4g78ltgG}6QaQqH>HRWUwhddW z=_POdMExRC%robqn^923N50=O^NlrQjydd=_Ur%+C6{2#-lhik<2=CGc@`XIfcgnD z#_gP*pYt3P++X2p(8t$M5iClaHlEin4>n{zthf-y@`>0&hDGZLSqjtHk52m3g_RFNO!5qde#1%k+o~BN&eaizTG+;cXN{2{oBL<)2yn4b8 z%@*s_&ZlGXolCCm6>eVSl>*ld{gDLM2>O~60#l40Vk-H!9j6_3kiw<;rs>_7s3J;*Nj>^asgWf-t5%ClR_)>hVx#qnE8NxfVm}XGCRcU8!~`&cN%6W zC&$?+yf#6kvT|Nn)}*Fu>c^bG0eiFJ*%O(6P`_l(l%P9B9DUMP`aVeGF7VmVo1o>&rzOuD!?x6 zVl-$0yMP}J`i|!yT`eZ0tlumE$wK|hI6|bO_qgED(^_?Y^LHogNNtDl}l;wF`FKK}pbTbA;6mV||f8^5c41*(i4H!i!Fly*^@{_e< z4kT!iQkx3bn&+zp=dUb(^L#;Q`%J2#MoWDwlb;~yzXxX)@HRt3mk#|kMe7N*MK;N= z4~VpMYUvp;EC|u{zYS-hDkv@I2_~TC{6>o{qOomkB;xTKnTIS8hhTK8{K_Z zKAyLP2hApn3|Zl+IQVOifDVh%^rL;S<9Du$4f%4Vk9QKzIK_fQ?1j7yBHe-bRu=?+ z^yLIi3&_hA69ij!_1nCxV(d^JgH_l9_w6|PtZFqFMCn6YM(}|Jn6Y2YEIKtowP73W zR+)U#pMDZ!kk>&SY}Epv8pz1oEmqOb{F=1-k_yx zA!wHgRmsv-%fgr8Rt#`N7~&^J(}&Mh10x2*(+p`v9ciJ*`()e`E%3Z%r*>Z_rKpI< z|4mWNNRwem_AEeIelVg*(0{x!{&@Y&J!L;TDAGikDAQuS+*F`*Tc-Ps?bzhft?zJN zq&9p*lZg^1rlACJRDEE_Es~1(6nNFw$?;CabM$_=D6(Vn3&7Kb+RY}XkyM9)%i%Kg zuHWE0P~Dl}5V3rDKZL)v*f=$I>^5)|Ub~RDUN3l_9JKlLYexBt3UFFr3DpWlEjShl#J59%t(@4Iid#p=cWY} z26U?QgQb4@z-j}&$YxU@bkqTi8MWILIDhAQ@h(9EVjc{$lZ)Jy=aYi5Z;Yj-IQjz> zyE_JrjB(kFx#`wK6+TXUqC{vOK6UjT)e>y%6%f+9H{-Z$Xv`$f~3^<7j1if`={LLNyPu%NwD#J zf%+mARO8&w#x`1f-GsG>99V@X7cAI0A)3m0qm&KSwzE0ax76ZPTt<9!lyukP1#$(t z2V{<|E!Q@%REK_c>@ql%3nB6mTpq;6V8^N1D9B0R4Okq?$GX> zfQ_3pvhB1UVzqGvLm4TWo*Tk#NQG+|IWRf#GL%C@O1bZh`wtJ;r3Zt;R_g1QCyIkM z!)+(85mm)Xm6i$i{uwpMz_~P*KIWl^CqqplbB(WCM}J!>WGg8rjjqmiGLyAvQ#b_9 zm*k2+%yGr59r&mZeG$5C!4!>s4Ou)}XJ_1tArr5$}waIIIuJr6XBmDQW zD)N2_{+wj{7Cq0)+VxL6mr+-PqG3r&ZSD8_JuF*o3(Xg!v(9T_5-#^9-tPQF8mYcR zWmEEJB&t{vG#bD_8n8*DUWJf|>3e&yT|NUO;#A#mFE;TyqxZO_6{28bH!d-?rjGCO;`_CH$?ASDn8lfVPeQkVaJUIUSUDlKIm&d8qf7-fQ>MQ(#fnxT`(gTmW} z8yBc6dl05&FqBvt6Rnr2TSD_CsW_Bu_@LBbrZOe0oo%&v75^>T=I3z>qT)4RUdd6M zCMg|IEcO~Vl6@jF?anZL?cgnTU20hgOYjjqeBy^pj5HlfGX2*PsI-``Z{cXkrb@f% zocz*MQnA8st?m3N--;JWzQ`Ro$=2uJD_`~OLw5HX-b`nbJxQKpRA6Q}a;sU_MKyB= zi9AOTCd55WgP1WIuw6?rTIS+x)b#=nVFB3@hP1tuP+#&dR2$(F;yP%`J&zps;UM#^ z#}bPyDwcf18nP|Iz$^3)fepbjM&EQ(7!*HLwq{+%Ic!#FjkLumgJN1dvIJjHyDTz$ zPy$TGWlYjT`g^lJDuLk)f!3nTkVzwSos;OK#{}@mxvg4NagoavTr0>=k#&DEj`8zaJBBQ zeL=0P!W;6*ZU7^DwLNpE+%ns#mD{9q;s|3QLohs?G10jpH!sV)VuwKdL58?NC zl7RlS{>w_*IduQs1AgRfAXoYR7?uT&FAobAV~EfTJ!GZPOLz(-_e@6|E!WV7*~ofK zsMsrmGb3v;s2b92?ws9J*J1JTPnF<9cpvF4r<7;Wd`Dq{+!yA?t2`Bwnr)nnWw6Ka zlZh2e0Bf3(<>=64LxngMxXwL%mE3?xn9;-YcsL>#O##Hy3kQNM-Y&E-j^Z(1NpE&P z@P2ol@bRZW>cp+z)>C|+?CbFKI+`k;+^c|X2fo|lcU`89KU@QWnkg^y}9uM;)wBd+Uv6eOr8<MV*i-?fFX8Tii@MjIe=jPwFi3F0Di4lZX(c|0dStu z;Sylhh=h8*UktJNWv?XB>lMSs>sw$wFFXuUKNoqzfynU2xy+t@%jfC(`RlEt)$9ld z$Bn`?tqC14d0}to`y&jv=h;1rg_+L~?53!1Xj%fD5@(OaMU^R3WwP?;xoD&UJLX1% zug3=&p=jOXssj{Bk#sVtN)qd49D})_A%zXvp!h0HB;F8As*48zs~yWkJRywJ>MagqeuNc(v_|aQ>0&q zK|9w`C>F@21O_c6o^@taeZ!n6R8z;UBfEm!fYbLp{>zv6<_(f0AO=WW~EA@Hnls$lzCq6DDu28&-nezY?L-(#2! z=O?C&!gWH9-`@?&X(=Xt?swJv-~M(_yVn&1e+IU_FjHPQalEfsMK7}&S0TpdhD1F; z)r^M`j^ZbP^GRyb`072rDix9+QPGIyazB?9>X6U1xH)6(F>msYO!T4-M%t^a@ zbRiuM8`>FTD*=-e3jEizhOe|AxDzLqc&<)p#6A}}Gyi@rW=*d+ENgywuC(%P0#`uK zoNLC)sG`7WgdJXh-o=c=tEcwD_-3G(p)81$VV7ihpBBajWlSl{(9U8>JH zz!qSDrD=Ad{p4t zDQB~4@hxj666Z{4ex368@^kZGM4C&0ioDM;{9(bNkS%7Dhne}1@GpG}N(JxEaLlTG zR(6m9Va%*i4}_v;^On`YQzC>p2zZruXEPKrNMt+J@8IuW1?u|6p5i^FIeKb>EyxVn zS8i2zS=xN%|80u>dS71VJK%3yD7_XjP7Rxkc+|L4z+-9$1H@R;D^cI-@9hn4=fZtV z%lFZkaCjL-Qgg=dKcx`{6BiF?I{g;npE`fApNz1RFoQ9Pu4~t%k9-DLSA2=d5GNOV z8B@WX_KE0k#`RR@ZK>agl4-&#^3QOD-Mn9{@Av=+{iGlfWYPBe`KnCSa}{l)P{p5F z19E!%AW*~xV4O{^a2xoEZX!xm3y9Epj0Uk6}?So=rJ z@}u=Y$;v|&H#Xldt0L=t4K??0-z*6vN%s5KQI1Tfp3p`OVP920`)?K6%>#n2Kq>#~cL1mphs&3YNztnwLDUMIRq45GWbcxsU;S%+=#7}QNb1ap zOfQPthGqDgGG3LyX7@8o#v5#5%hX4z#ZX}=ufVtUY}PHMKEyegEbft8;U+e`zj?2PN%8ST?^&6>Vk{{&uE8=u3mmN5Yw?YM020kT~ajx&I@RCb}- zU>TcXu^UOjG()GAYFvx*saLxgVUS&Jv7<(6)82YI zHlTi$AMxNfefn}c*v!sqQBo$&ttMYFvC*8YiGfhE^5`rp(u~wfe{3ME>`HyEN|nx3 zg40QUbHe-bEo>mYrk~fz|9LnPj-byb@PHIrhq()`o6?MwcRVkcIjkPEXD?FK zxqyZKn^FqHo(_+^3J`Cae4GTdCrbmacZv+Y=HYmaIrZ4(*hYAdbCd0!B6pqO6Q+H< zA#)IvRB7!<*Dpd=NO1q0S7nQQp{;_|6`>`w7^3J&UUlt07V~G^vJ$$9(;)dqs-TVB zAJ@D?`AS`4tpA0o`mo`)M^5f%WxDs>!`T68*EM_gCq?hMn}PSGum{fKfR@w$x3=WtY9{_uV-wt7TBbgT~^E6W0IiEhk57saq zAP-9PZtW%lLymsh4Uy)Hl5~PERsyOqh6CB3YD^E`rbxI!7l6~z90J*K=N|4Lt@GLs z5*`TchK0i0MQ!*d?Qg5a8j;=MfXFnk6?iGG{sT&PjYERFkGU-^;eKxia z1-AxyFlEzqd}ei-fEKhjS<7_B`6e*nnu1J-3d~)0xF`U72sILOd7TP7{=QbcgWCgu zTx7xw4vu~Glnv9fAswa!mppzcJ`Ua&`hU!m8<$4R7z_Zwe=O2}o2RA!sdu~*1OR~U z|6-mToehotxhXSS2MZHNS}P03|MpOG<^ScOz@%dSR}U4eB5S+J0Mq@dZYKsR%D*qFPYa;ELAaWNkA1g1#vb65*`nFdLYTUL zLn{qTfui-PI)kzS0?$-~t2>(@znT#|FN8kcJ6N~qp!*2Fa1HxUnAKtP1l#d!{cdUP z$yU%NH2woKO-Pc$DB92gL{nOBOhkrnysWK@c)OH)q1 zGR&fLGH-O-nulYgUXxurRk77Fe^J4yc3w}IE>*>)B@V@NnKDf}q4$C+ibETN z0Ra9ZpZ^xt>i;CH@PEHh{@;aVXJGcfYHH>G)71aLmwT&h#$+-ec*i#VG89wCmkBm` zlYso>Uua_yj>70y2O_eq%l7sdIqu4)qHi8DGmnNEjZ%gIwGcRsY@}?r-dR!Q6H4S6 zkp5lkr;|#A6{sr0HBS)A*D~%3fHyDMU!NHv2~7nO@p&qmBa2)#isg7GdYuhrxi^12 zKI7fkOR)HsJV3p)Nwxr9Z{vL-2WxMO9&W3ET>;Zsk@$mcMG%r3r*yT^hxi4Ux<0@s z0)L2^%#uEYEwzh?wfx0&Hy@HN_i3ht=w9px;b#k5=}0mSO~xu3&Cqg9|NN@Dgb;w+ zWg`5Sa^wRvFaR{G**rikDfvEu1BKedK;8ls*?BSDpRqQsS7&K+s2%1+VY&vy-iI@r zw~cXq|M2%Wob$8MtWf;Cjo<>3b#?uheVA)0zJUuC*$`0Uv>@*VfeczsPEg#^MOC6j zi)OLZ;wR^MFXZMRgPZ>fZG+SH>wSBSq65-vJ7Cvwc|Q$ljeGBQzv;uwM-MJ$d=%}I z)xrIZZ$F;qHsPvSBXC>v9gH81gKNq_YbKR1$#vF; z>VFFj9LCoCx9<6HXI}jEYe<*MV3pjIq?c|WxBr|o-E=cF+pz8|^p}tb5CMV#kjiiP zx%~v>8Io=xozx^xaog*(jlee08zZ}cQo#OPBsPK0OFg{$Vjdab>Z#uL++ zw-=x}vM+&97QQ^q4H?QsviyH2d&eNbwq;wmY}>Z&s;=s?ZQDkdZKKP!ZQHhO+xmK+ zbHBTH+;?8YeScQQiuGrW$jCV}N6tBt+N^0#MO-OZ@gU!U)zs@?vIYP#xHfHNTNRNJ zCqB2azzEeoS7L|ytbBEbT=EQYJp+eutR#=4rZ5DUiO0M@(2#XMFVeKpfvER*K#|Rp zTi(}-zaUgk=oVGLzgqR-yjrH4_q3WZALA8J#IVgoXb3dOw+~vR)(_$j6rH&pe2{zG?d1o@1f&0Ml8W(qpS%{>t z${RJTWkyr~kx*vw}mSb*H?3`4D^1uRevx2ddW7n|xaYeWe|B6& zLBA6ep?%vZT`H58Gf_qZkn^Ziwc4H|nM6y83)pa7wG4JJV>td8jeBj031y!RC` z7IkIrQMwi+8BXV3^Z?9uAVGcMDPX}%OeQQiF6({Sh*a4h%k+z6;MV<_k<&Yf@ag5{ z!bWNVf=*JBzaBsc*O0pmGn5DA=63?(%znS6TY@ag^~QDb1A@7me`v%a03nk-EN5#~ z7?fRaSoyOkC6`2wZ4Nla64ig75i<5Q&=c66zQVCXjuGwz%fpe!&OXyt4A*InJ^`mC zXN)#&QKPvYv9fGQ0B2zp?!+zA8Gn2=#c(&d^#}fpoyKk7L;!^x6Mj?CdGrs7cK)4_ z!v(JH@PJS%+9d%0+_6~A{Sumx)Y3|<{^bZ?A!QV0X{&@feZosaVmi+fK#c+U9Om(W z!-W|{*SJglTm{bW0Ya%Zdff-r-#sPkR5#iku7Ew1t?=xKyNCm~fnWk-+u?)x6~i=zF%CM)(K`2nMzyL;&WTUR zaTxl-a!CAZ4pIae#wk#||KUTG<`s%~k6fL?mQcCVe+IXpSAhBUoF72NP{`K2FjA|q zW(NdTp%Em%n&pT5HN8l zIFiK-Gq|^MADMGylJ($du88QXM=zuyxkf%FndfGKn-<5O%gk&px&m;|s-1&2s?+ax+P$4lxI zI0Je;k`b+B_riY^rO>Y$8+|sQ$?TcKcgtQEFDIwhuvM&H5itd2Mdw$<5zGB7!Zx)Q zyX*>k=_gVr`AfxAuK)r?>ZGzqcfudMJ|4JeL#z6hV~0inEbuIBP4X!6VpxMu{c^tv z(`;|hVW(4s3tALUqehoD-X7ulvbQb_48KtBv3GIQup=vz#qX;5LpL84Vdigj&Wx9K~SvN7@{ zjbi8SCW46rtMarv*x>2B?Cew`#Z0+TMYGVNl}Whz2x6=o`O>ECYKj=yLls_3&o{3q zK^rM6(`_rTFhMB2tv9#lr(2gUOLTT5A=9}vFdM>>AQUvkyx=7>7*5aI8z**RNkI2n z(9Eg3O2#YMfCpOKZYtr#{xx;C>C_l!Rl}w~!msL~Z<7(m9h%3F@8;rA`O@QGE^g9{ zWh)5;MO}@oEg-s!pSj`WHL@~sUr=7}L7WgpPv-L>VEby6Q;|Ci!vegw3@2QRBEhiH zh#w~FUldAD(B~h&Fg)VNXp8{BZNh7I6&@kFBX;o}f@P!Q_T-y*SM)kTuA%W>cTlMmtl0`3fs= zzw%gdHxMPU3Zs%+ng~Pf0A^Eknh`Ae*Yb{^z*#|sXlu2_u+y1-ut=SlnlbvOg%~vU zO{v!S%95#k@mxHEBn>-~n6Fu}%_rur?CENDRH<90sZ!x3yg@Z+{>+`s=N2z;D+}g2 zk^w&*wp3PdFKt}~o$c8uQLWz}#6_G%+Fb;th{8O_izxI=pXTwcN2U^_BE@4bcM2wz zggwpQd?*-!)afB@(BW&5j;73C)bQzAswN-#O1g-hEhkkSBf}BiJ7`JMrzSw}pJ4io z5cCjUzaRICx4Tti^xDfXS+kxs1W{n7iu20qu3kg9^hV#(`KB6Pt$)`#&*--^wRZcR zeO=*lZ~C%{_ojp}L7&HH{8d;*oZN=h!M0RYA#~!le5_N!e&vB$iv#g)GYnnQY}Ww` zHOM7ea<5#;;e1wev6~|b+ODl&Ed7lj`pnR?8FR{Ax2f~3Zas=5SU97M7`eE(kw#q8YcagyDJuk#7SIBok5zAAfRl#`%_b zW>bvE&y?q`NcuynA40ndx%FwuXLy*L{6~TCx_yQWw_*>x`{Q`|k5~eH10v;2a`5Tf z;j3bigFzw=!OXYFPR2=U07q_7SdL2%0N2N-x1z(UDK_@g@YaJ0geZ z)``1?SArAL&XV2CIjJ-bs6k30Q^q%KNj*IRWnyAtG_SYPR4m&uw+pjirG7476OZW6 zST}1n*(|TG zrQN+=k{LJtf`6S`|I^V=YJ)%kdl%0oR+5je8u&8?O5lDF4_gV2Bb5ETO&e>HgZR4;@p%{cowpghlD7^eMgZbS z^8`j6ETXaZ1E*p96J!AvDM{?ayRZpR{!Ep>0jR(DEi%55XbNV0!rT2sBVMMOsn5HN zErC;MkkFyW+l%&$BaJr2g$kggzChtSO0Af2zd+786=9JAN>(t_in%aM?OOM2H8*V4 zPH{`w9H_b^Z56X0^<@1{+zVwpFIQTNM~Ihnwdt%#*&k^~<>{BaWmRX94s(#L;zVWF zy)dH3rt|~RJZHXzhu)(*LXE@|L{0}6+VBLL~;Kpi8g=$0J#2B zNn~bgM{8*7Y-ejj|8GP44?f4y*vi<@$=2b&3nho*e-}!BS!Y*h0Kh*r7y!V(9?lAy zmH`YX-p{Hy5(1!tx=(Ty$;EI(IffH_&V>bx^O0sG;_H{&^sG(6t-Pe<+lL$R2ilu% zj?po{1)((R$RK`mu2I1pwV6N}qfI#8WY-oY#zwyBx7;07T*|lyV88uIJ+&4Oid5P= zSuu1Yffd1NNWZ$maA!)^k&g)v7(fkIE~Aq3GLWRli|^=}(KFjxOB%){EpmBj7O_`{(+se~r98_bY$YwOgH07+cj1K<&HZlQPq5G#|GcfzOYNMv)DU{!HR zVH_0(8wdQW{d5^apP&;Ke}_frfNl;P;`N( z8KhCKg@^ZFCQe9ilxf>?8wZt2NejI}+67>){Jq4O?Ut~kNKBWK4tY2l-j{0&JY4fH z-Vs;8(2M%LA)XB2r{DiYphRk|s7w4Qa{uz%-yTi*hey%=9C~2;fA*-8gR${{d(-#7 zy!n^?AKr{ombd-Grg$H#y3)i8K;?U+$MdsDXDUgbHPCsD<&6Q7t2x8;$;?}w`D9!7 zumwu(gWh;OZ+iVQ^{A}~A4@k9+woFIo)7?n@cHQ+HPtJG5-3_~s6_|Jt$^iE+QKr` zmmZ9)g<MQ2MQ{ZulFO;ABt$M@k2QcMsv?79a%y6dnRuzn&*K!iYSxfDn`&R$`AUx{<}I zVF&E}sMBqHVg7J2`uZjpNg*&1JD8b(5P(JkiYc5!_M?JFg*1YO0*Q6VyDWg!h|QCDw3_=UPmy#KZ@!^n!^^iL1)-JPHp#WRDuk`@}cZ_@|*~g=tS(@ zngP~qPhiuD+~7QeWb7c2x#C8X8prP$!P72CyXfc3u~aNN4_PWSNpwd-rJ#34x&>Q_hF~gmpyTLOanV3TMu!%(d@?!ss&xKU3i1MO7g{qflspr`6spX}(J(%U zydYkfLwE5CxELE-Up1-$=M=I}2t{&v+iTdj+!r4rm_d$?_mB8UW2go}y%3{2Ry29( zr=Xj8VJWcGL20(+vKkmS%E0m8LrBTzm{rOEK^XdmoU5f09{GnJrY24WuD}ACseV1x zL}Q5S<0;ze7twkk`KI=Kj83^Px~p;xgAW?#jkEYBMKZSnpXqp}pi^1Rhvh$Nnl9c? zuzW|k32zs1(D(S=f|&iyRC3b5pHx!)*WVZCV}`HG*RvG?@Kts5Wf>L%cyi{!fAt^Z zArgy$5@r}8a)7rvua;=TIxkret;28IyW1Z+h5C=+g4AbHH1p8ZO#p?-hTo8MM#wfD?(5J0|5LLk$=ak zY4_iR6v3Y_od3I6{p%PKt)a7{ldUzaqoISj-QR?If5pES-|+u4Tr*YHqt`iLyrxxa zG6AiCYtu1l{7aALLRRtBa;}4j6q`&?Mq>}OYA(C0X~vTaWZGyyVpE9V;YS5YWry%U z{dt#OK4$lNz4L}VJ=Fn>lvo?%DM`^J3pvxA^5Jf(xq(*V1vLoisnh#)pUdBFP9`lv z(jXhB{a93-o1*4G&v35JfH6p20oe!Jyo!cP$v|x*fzNX-6i~F1IC)xH+6nuEXPy?V+MJ&%Mro5+76gP7BOGOe*8Me)3GyD+>Xa7hz= z+9xZNfKb4u*#Z3xfdR_iNHDBxylCNr*8>7}Cv>Cm#!?lBQYgHM`@3LDzr=q0kfHrO zWXe6h@>0`6Ssh9%D_vTe;;cu18d|e;l<#4N_reU*%z?6H;K8TH%QEut;^ghXYAy@! z$wK3k2E07X>7n7CFZ-?$NszW-R)lj$z@f;@FbN$Fve7eS+%oqx z8oLX#rDUjS5^Hc~$xBRHINYV#n|4Br3On13*4`jpd0tD@b!V4*?Dy7D&YwoX?Xj|- zu>)r-0XE7#_wWYCycfcVJ8H!r>1E(qDN=7YXC@iyg6pc(?G3aWTEtTkmg3|<%b&br zN2dwhFgj=vE4am=AVU~B=ftn-bBSL#7pqM(a? zE2&9GQ4H;cK#Q+aOO-y)s9(;&kZB3iFqMAud_@xaz#BjUTjPWos90`r_H@U^iL7uK z0j(gyHhj)xN3ox1O)e3AH zF_Z27Y28B(=6FrE{0f1eo{km)h#G29goU9plZ>YO*fTL+D`%5nB5A-%WCMLlIOQEa z<$;5vJ?sU9{y{Tqz=LjcGOhD-K8KGhU(=&|nLgtCPq2bR;KL6u=qme#)zqSAbm8~?%Z7mmLXM*Rb!!0<45Nvu$p-;?JOQ1F`EJzd>H0nQ2{?c!LPZxg zsw6xO8eVva$w$>kn97EJ&IRIhlh#M-Bv>j49sZW7QpcGy=O7uyM7g&$N73m;M?x|} zLK$*=0VN^rIg%sXc1ZBS8|o#9gszhTEsqBFN$$u~7df8H6~n*@!&)5b1*HblNsBf% zzJd*VhWW~L*WZcz1crSa*;d>iBIh9wATL{5_UI!w6iyu^q02X`u4-LGtT3ga&nIGs z98(`4Ba_|_9YV)4uJgPok}Ia47|y^1UQFI5c;Feu07b0ehnVzhFk13mNOe6UlqT&2 zh>ns$)cQ>zosSp|5&kss)NJipVi&CFXU8Z?QeY6aD$gAdjJ6XJV;jTgkfKPxo4vJL z%pR)3_PB-Ddo8~?p=S@c=2gFUH0_4*h#d}~L~L%?VaOo8(zVa1(HDCbMu@DD!}9U= ziewU-<=0h_?}Sa;)HK44C2Z>C5OgQ_vxbl*t0rTDPJKws&{gkjM>hk-XbQ?fyql>m zslpEsxE{fFiyoYNF(0o)@MErPE}3e#pMGW9pt2%JL<3BD+gRaSV4i`#yJoBiE?hlc zd)hCdo-=HW`Db-_yvQyqP>>s9$OUxf#OEXyD8>$?uq2KB?{w%$3%Ghi%tgXo6%Z8w zjTyq-MB^zY8=GC#{w1s?#2k%^e4~318oAyB3-;K|61cUB;;=j^VB8>)R&4Dc3EKF@ z8)<{KVcOtLEgE(>eqyyHHFc66bxaXIjg5p<);+?M#Q4{unTilU$|QXTJ6chF>gs0A zWI{ib;!j7BeV)9L5Kn*>sw?~OCoO3iIaHPvjR>92y>eYmLp!^^h!^JcqH8%XL}B!q zTP5cMUx|6MFLDMknslELAicd;@gz@JU^KJejSc&qznaR@KHtoLJ=f(RGdKamVxxiU zkwUS-^;zv$+t{G;-zFVjvmT!ayVi~-w2+pN1MQ# zTT)V=UEQ(>Sy_$3nIo^ILFryodyMU)+o98MIMtRQLuW@*c!8d*Qkw}-!rkZ%T14yB zy(E6crQW(V*lj+nk=_V7pb;mkiGKB9171D0Yf=XL0cKTvs_yn`%!G}ukH{!Xf2=^$ zf_!94w+fBJ-rWcPJLvkiOI zMqf5sMoG4>~^7OW7AN&5MS z*{bXuR1D6^HSz07wyleCz@rEvX;w@&=JM?M6YBLx;w&iHD^EZH_V?} zU33N&mX-ZuA1r9Ps}Y?hWp?WH2Nl0tXzfBO_jbZZ=g2b0jxt)ajuXh=ZvbIzfD<(I zAA{dDh(?|=I?W%U+nxH`-mOo<7ONLRgtggvHabx+G-u_!GK})QS@ia{$N5)l>4=+^TFI-@G%M8_dXe!zT%S;T)=s z50t&SzqEI*l!Hn4|4b6QP)5e3C@F_21hTyLwuj!9{+SVI-_u2Vd|4tw#;a{PKq7v; ze@~ODF&^8ewoGqSi@dJ3H>%=g!OWbv{h(ISqZwPll$!8JGr^`ti{^>P!fIWQV+eLd6-t;h= zVh=HV`K%YpT+nLja^uwlZYshXPp8u0iWt?>%v{4BfPD}`Tqg=VF^v!jiWlO;#ne~h zUaZ^H8|6)SrKR3AS8}Q=+_zuKp1t863nTV;MU~Xf%Z}S#>o)eRdnq?-w%9tUf}2Ef z(B%aqp!S=YoKu*Wd060?Q{Ib=xXCAd<>nS{yE}3H_Hz9tjaseUC!efOKA{^m26im7 zqlx2q15)ePxl31l6<2 zCZbdZ$}ppioK;N5n_~9vEu7c!#Zud(UyH?lvy1%BX4V}he-ED4{{48NG{T+|A++@t z4{$@U?sV0PxI=aW$3uCs%_wo&JUOxaFl0l+c&_^Sg=PwRP20_=&kix6XG`QmnkDBT z0ps4f8DWeva5T17*8RO-{8pQ&Vb95v8qG@3<3MOZ=9_nj{s?__+3ii0J09NiTx?-O z{vG2%r0glXk+0gV*ImX1$K(wV^CIEM%L89s6&3|&KmLjCy)-?Im1V3NcdTbhf0D5X zz1oo0+8=#}1DIyyd2Zyq_Y!nm7u6(@7#4`r_r}b)Fhzj}$QL(%Zwf#}5EKthV=PjZ z6$KJ|iE>@MRTfpQWX&cDKto;=W}|Ztu0a~K8}DIENn$R*{L`zhl%f_EpfJH!cKa?1jgl8oaQ^uQ4Q?kI*O;VOP?(W@0Wwq`7}$}r{_URy>*K~vm#Ztc*51M7Do{8NR7;i3Y(agG_2{^( znOpl?D_l#D1q|U3|8d}=_a!^(`s44qaA<{ZXD+(;Z#>d%i;;Q{X`^i@0qG>N*B%71 z8K~()r|N9UP(ds8ECIvv-KSXME31#T%36z4Gj8!QjL}Vdf-G&IU^KP`5L+Z!sbc=Z z#ZDV`A)uF6$7brwyTqu{tL0B3$9uDp^5bEwyD^(j9hz2R^&LO!R?gt8Tg|vg(83Oj zVVOaz2#_nv?Z!nTx;b*sF^uM;OzWi0f|-_WT!!Yb-A}vqoFc_M@4)wO3R!r1S=z7D z7zA?W8yj&eU;;&}oE&3QGR6%Q=cgD5r6~yS?Mf!nOv=`f)dQxCu)uXH>gEY(E@(DdTypEN4*D& z8}ijhtpnupT-^x!i7`q{rTpzD_QjPn0KXF&{KsFWvQ zUsQzNK9Zzhdl!*Vp#vn^Q;f;$jzFvB-SpbVuvl*DEi1*)XVbuP%EZP{X5i1jJoP9;7M%qx&4dPlJ24nxms>L!4OCu9MK zyD`<#$Tayr(QfzqhC*dob;1pOGI_`y&#O})d{qm2z!9?B9iAFp9FAt|SK^Mn-!c}F zd>tTd&R?&pyeS>kno(li75dyAytA0Kwg+}WRtdQIO5*(&TM6h?dq0-u138(oxwl4g z*zy(Wz`&qi7ucD}3)V2)fF~Ou0bkik72_`Th4`=+u=+~w9;KiIbq1ktw`3Zs+;_>N(I(O?WcT!1>|7SLaPO=X6d=#&SQ5lbei00 zRmx|_|9bnn*IQ>lv!X?MGxUW}ZY>=tUG$hd=}JK>^O;0`uQb98Tn=vuSLKR$r8^lm z8e)gA;HhrF#|A&HNYTO*ne9Oz9$tgV_3)D#>z#qKuqs|Bekr$wQA6cHVtHNeYYP@} ztY>gSrFOHZMw!2=K=6<= zzCKFuAO(Uw2FF%#ScGNmyf`RLy4U`Lw!e69J<1``=1|qn!6w83>Bd& zCSdA# zump-xse-Rq!{x@Uc%xLRgcj4IE74s1TnM3KpUpuIK3`?c;6bDLA(tZ{>$+pDh!LJ{!ltRR& z`BbAl=1@s)%vd4Gc6X>e{*=;kk{y%D!OFVrI&b-UZyCd!D1IVN4!9wz2o_JOz?Uj} z+P?wdv95D~>r$(L%1*Xe`Qt*N2)eirb(K!{suMkvAK15_hEK0jw=dB~K`s2IOB(H) z-R)vYl>?g@7lat5VR;=8&NXQ+9{B`3@$NGHUpNk95?*4@XkB89Sd80)6`lAo%v?sG zsJ5IX!0g+^mk0p926Vk7;6{_)-rOf`tuXBI`L~Juecm39Pz$;mN%(_!IZjw8c_I!J zIWy<1qBTY0@6Uv#oPSu;YlkOTh0~K6`7uzpZEK%9MTL|*XTBN?_ie+ZWS{D)K~qGlcQ0WB>b`jL@6cdh{M&jWC^#-m|4ROFq94mYyK4xVb-Ru$nK;( z{%-S(_hl!({4>)jbpBF-7_JUsQP##@t(qrtz^{)P}#VFmY_Den4AKRV!W02;XG?gW> zm6`2nl5=v7_ezpu$iZR24QhC`4x6hPO>{zQ3Nr%P*IIrg@>~RI#?-)hKLRxVD6#II zxgZA;?c8kGd{zS!iGZu} zo_Ib|JF>W?k@2SK>v(zRXxo;3=Uevf`wi_!S^%50eD54GUYDU%uw;oAx|ZE-QZ9Cq-WAYpr7 zq0ZqGbKGS;$9PDF@Qugh&3k`OCS|TKCVqqdMWK%FJYbL_0|5NR!u`$95&wgq!yp3y zp#Bg1+)sTQ8)FChf1Cfn&H?}5*||oIa~T|Nq_3C(ZVpkaHg4|SI)yJ7z@#2KxHS+W zg0*oYmOCeGyA#c~_)#e)pD9hDKg%{E+i_>pm`n3z7r^YQweWy-^0@NhwDRGZn_KKw z(YYmKlvf`%kuKla#I$srY`aH2&iO22D*3?fp{#L`E zq=Y{SeV32qt`E|T>+W_uCMekV>od-=O=a~f;iMzcs)fwCrLG=M7R!FNS#N0efSbS; z&X?hvixZ=l$IE@_MO@~kHDQ=|3*v;6E+=%WY}NP*y(?0}sZ!0TM7N#aRDzNPsV7^w zX4a2wc#|G2roJuZL0OB)Y34h&RQn|PQaEvB2Y0C>Ozn75O7HK+$dYa&#$v>3lLhAQ zw7E!;z+uqU6v$3l;0#(0dFiCuya|J!^^tU4L&g~KhB(N}Z6lds&Ao_37}EkXQ88&Q_(?l^+w-&`pfsvX?H<4}*^Yd+}ckD*3=P%7(5AV+5%1jafdU!QG-?*RCLS7!Yi3BoYv4l`eT>t1(kb8YVsTLStCcalJNEdXWXKOPc5jO(t6h%cF?W z>9L~9^r6a@Ve{1a*WA!>XdEw76DJ2oFz%y&PEuaj)@Y7Exs~!PQ~)(Hi#TGV;zrod zj6`x831~a>_;yB)+Y!Li6EZ?sQD$lN zc|f6~paORfl(Vx<2_TUcs~`n@%m-(8rP^#lqOQhve53Iva&(&Ha0QzFn?rPszk#41 zPKZRglv-GL`0Py=y3^-re1$k9^fxi#-D+>}@+pS=i&yL>5 zcjbb&&3l-aS2qGTqvmMAN78GTf-{Vo&!FmKh29Z5U3E(MQzvepVt+EzK*AE?^(X1v z*Z3l0IikUeWf=vz`>gh1Ci>~3*Q5Ekl>jAcbth~T|9?~Ed8p{cZN zRdsK7ua#=mL&k-xT@RJPI^4Zx(yoM|sj2O7>Wab7aGIW(8r-zJzT9#Jp=K@tBR{f#!D-#tD6s5sD-fcwiayINzBj!7gu7uYB(4 zXI}?r4PGT?kH+~7Wel^*)pWE7)I-EQc`PzO5{`qLixfQ@VleDzvTqDLQsc;xkI|%K zhX;2jC*kO+?M&^~^(bbRG;R=Lo??))_a#IKjsDE1X5RHXn?PyKlQ+w}_-fcW2rB zxt?zlti8n5V0{20A-u1Tr|I+57|K=l!c(#`bfPQ7nJ1rOXTWiO_`Cu}ET~RY3k`tt zSvm6xv&2^>4pK5T*Ggx9#$%+nT4o*)>h?Ne%`P|t{K_^vDIu{ui|8p%fvNd3@~0zp zCTaF!aZmZYjufS>Z#LzQVVOjvDLq}Cg0GE;oHd`*jVetV)?%FhO|y~_m9zeL50V89 z-0$*}^1hL#Vy)WB*)ex&Lwm6xh)l?)F_m;>>EAjk%>}m?`nx~}HD*}K6I7Q~byrlh zRpG0YADPExR?~AS@f*ihZo^_s<0p5~;q4is{;~1gZ7?g+d1KP);mVn2*oE||8vXeB zILW_E4&oKS-o2Qr#+yrEXn0B;Y*XFK!@H}Tp96j#Zew9leWOM`9R1=)CxCty8P(Gm z!hy?ij}w^a!IpqirQ2r%@X$nM#zv^O5#Hf`>ixC=am`xha5x(<2ko#4KH6pTZRj;j zHE<@^ajxbgT0>H6In~ zHwDT1R3~=WPo~ey0`4WH9lLW|va|k}yCJqNAuL^4uU63}MpP9}#rbY*=ejEXU2^;0 z{diW~PFnISt8*q|p+x(_w&8}sfz(IieB7e9Le%!uS1-U4T$8kpE6O>g?Vl5}nUAKh z_nM#^(8)DXY8m22D+rI}gOMTjcki}*&uO5EMXI)m^%b(h0c8z9X-k(((C2JP^=Ihv z9ysx`knmVpy8W=1QD*&)!7V`9;EXw-JZ?2(DCa}Lt}30FFFC)Fu5hksYu#w29c;+ZNXVW>C z#ChpG#(HN{z6DDI#Oj4y!B2mCt?VxkbXgz*fHNBfX)-jw&A}^!?S=OG&3+rDBF&A9 zIaZPDa_h9@@2-+HZDqU|k#izztO5M$+bRnOg+*kyQNqz`fGh)+qotJRdY9=R6v_mv>+jy>u}E~HtSY0 zH|4y=@Jy9vOC;dlKtOw!ljBgHgUR#x3%HV!`=^YB>1rMO7S$73Rx)etyoRdz;^T}R zYvz6U>$~{KLDi`(4D0)A4sKptVuiSd6heSiXo(-(sWf-No+W4ZR_V#Ldr^qOqUjw3 zEjB|I+7`K`77p^LmrWo?#O~yA*e6~(ilO)E(69h0KSkRg@Dw-K$6LOLT(;^mg~ZA& z>1e~x5KY;`BfxghEwa7ycp_#-DZ|qqv0u2@*sUq7GH!a}tmlN}nBTD4oc< zCtBjn3j+PD=dW*_Ch(EHG%>LnKfut_SEXX0_u&!*$+%n^S6Na3944mSqKY6n$^#b@ z?_PW@-6x3B6w0HMz+#-+jE5{w;w(VXQa2cX6_X@wwd^zvDPx>!9b)T+>~`f~g(FRx z9`MOg+l}@jIfZ|{JXOLL)z38i^6l*%He)im83udVa^2gbOIkP|{LCvK@1C5UXtR+1 z5@tar0-VDqdJw_+4(4;l@YtM783uDA(ON4e4U>Ij37e_zCNeZ%o6jUo=JFwfyMrux z0Z{Y|n-8X3@&jJbDP^`q3#mt7b(ea`LXBHRB7r^8-_K zd!qUXz4>JCe*4GfP{P22dql%>* zFDnA_e)O6_U7!!-%!( zYV4+zZ!0JJor87*rjD^&e*=i|P*oKZ1#ae!<;cLZa;9N3Kb5@4;GysYJgdmttLLY; zchuJhJpe*ukB(>1L?`~ehaig~wwY=&KD{lVoY6Gz+sAJIWJc8u4|a`fb}jd1hVt~> zP@x+)&3dRf{hi4m)lAAhc_#zO=$TAzP`tGAzSE~K2%iM@L%f$WlE%!KGH8jKZa^$& zZG830QeET>Sg8%3Bf@!dgPFZmz$;}b1%BQB2-`qdb@E z(&d%=1U+L9c=ZaPwS}l9w{kk+;N@5)x_au99s_g|gn~`uJnY1er6igKChEBh<%e(= zYN5$!<=YLQ%c;O%+pftjtU=x%ORHQqcT5rL^F#TA#$+C>U|&`yE|39RnZE9{DQ`1C0F|T~!xxyw8z*f23LBDB@1WPc~H5x;Xj&Vp{^|jXctSdB*irB!t z+Q3f=QJ(OjCq#`2nAG(6sKAB5z)ND%epuu2{|LR{o`nNkYC{EqseA~Caa{g&M<_o4 zxju-B5n`Gy$%cg7C$Bd@zl@oUOFC2KD|1%%R!Zx6d9@W%Q21~wGt^VH=KAET7`N3U@4FVL?A8~e;^QdGdT z)t_b+D0|ya;t?`Eg~1f?9{J8mCi_^{T4!H9;@q9RynR?Q${KB>gpkWsk%E`)s!$Zm zGn7WPpya&BJ+iZbOC>kfWJ423V$%$`wJxIP-tgRlm@|`hvQPi`nIiW2lFhE`y@K5IKPZjR{2ezN(WV{w*i%&VvT${P_ zt8fjVd~HHoBQ8yVzQtuJ0gi2hT1uVO=nV6-ot5J|tZI1&o$&Fg7#`PaBcuk0=X#5D zETDQ0p6eyf8mz*9r|tDVHBimn>FjIP)ncLXThkIr6a)ols*=2sN{Vv}IMZeJxwz;< z2we(l+UYY@LA{DpQRA?%Or29*|GDER!!!{JLivo~rG^SE;Hzqm3x9?DoPQHO&5+A7 zBCfwM>u{cAGhlP{k-XaQ>k4Te&NkEBH58BpdyHyCPHQ=KB7=CV!$N%cq;qbK=oVNGiY-`w`Z0Iv23)cyK*Ki(6Vrl>7&dM$rIJ=t_@~8P5Im_ z-v{YQXHA}xzad9m$N%-swh6j$_P1>8;gh!!GPN-(P67ITS!93BFW7m((0hjcd@m)H zM>g8K?>cts3u7`bLMmMoC=%1l_@bTV&%U8?IBXSvQ=i{VNmAHZ?Q@4m2<&TNj(=PMaOyP=a@=0QFLjYU`00!s?w zVWn(av&M>9*}gZXbl4%Q{Y6xiI}LXgRZIQ!y1mQjnO4Um+U{VZ4_U=T>M8PKW+lh~ zdYUAY*7cZywiV|$jFfkCVUnI8atq!}ITYFDfbN8>#+0226%MTtG0#V8^ms0MMM9qg zkf46I;Siulc9jX>wpbybp3he+dWOzJ1VNdrxCw>Jd_N#_twD7{7MR&EjmH6K~ zCEAN4|b^6&T!Qcc?TAxEhVR zho|}SjbgR*QR>jhYNGp#eD1FbT|+9xbWSaWW$n0NQta+%JYvJf>ydL2g!@N!;2R>Q z~~HuP2|8``=^%zAz4bs9a-hLFvefGz(JAtAuJ62{4 zCcL83YGKDIcOY0uUJd=`HA%srG|}?hW^0D99wqo42#X<(Z~ICaTb|2%zwqMfk6pk- z;3p}#2z0q@41cV}hs{+}=ZA<+c;FM1ckDExmWnwRRDY?#x%^dHiX#A- z=TZF(aA;x0eML!`Y{_@qved7_b;e@vr+hf`<~0p#$)YhXsz;NrPd4ii8?Z&1zk+57 zHd?&xcyCl|@xg2k4A;zAR0axBMgtT(U`z2sW(D_a{Z>q|U!QFvXQ^P!BKaQpXsd(g z50$TD2+#87UE1^G&Ys_{gU4S#;=?|^)~nwGP4KtjL=?$PUE|Tg?xl6?FE)*cM`3SO z%`!o>uxOLDG=EnZB#5hPiMhgM@4rkHlbdeA*7qd#?8sDyzp!*cg%WuGo*Fh3MY%@O)0sruS-KwC6d=ZF@ZoZ-gqZse!Xp=D2 zxyKxr@ruiHgoNxiuuXdBPwhyU?g!WUxyfuqLOaOGhrVxHn{v1vzA6mG-VF3L-G$&= zV=)tlpq6ugzaS^7P1k4YJD^`@oSO=h7L_Wkj&|mP$rV5t0G?^8Yvq?*Ks9Kw@69Zm zzBe0R>^PwZO-W|Co`kthqXH!^se@a!j52IhOL>uzqX^wOVr(qbwY3W|!7*h1yr4#b zCvp{h?GHrQGY;mqP_z?dY*OF=@&?2!_%y0Z981Q*&`!o#-w2xJlr67uCLL#tRfAlW%n3#yJGd zn*vxd4jQ#H+a{8S8gsjH5$4PW6d51FPXR`^WUGN!?zn&nqwJA9PsL?XuUs< zsxIzUO~X%JkxiTKKb#z%NPA}kQn{BVYGNtd{8Pr?O&4)Mx)dKVfbR~CDUV0y{TJIz$U{y(IdiVH=9+VQLb4zzG1Ntr zMp=fp=H8MQUw-~)@;v79;R2Z4NMth@n-uH_mO=|QGyBCjZTovTnSw}{H!~jNo5Pp+ zh_HEk9a<-@A7%rKT4q!K5)6`noZ#@!QQD&Gg*saB>n!4C<9sEzzf6;1Jggn<8SqA; zrqg_?btWBYyZ$j8TpcX>Hk7$_k&Zc{LM6vR*!ru!orh|_Q7eNEDv%Xbzm2r{pP8Lh zQ@o)9thpi8(F6wLGPGjpJ&3-3W_+DSjnly}kR>Y|H^}ZtN>5`y>Knk5xB4;VJnhi= zG_4gI%*-jK#luVlb64qRv5YX>yD5ULs~xWxg>Eh3pVT1rzYAFRUMDOKK@L8XsPHGtFYnT zKVVx0hbDSB<$_@3HL{AkgAk=FNW@Xx7nW6ZOK!>bB=e%4QMjthvRXJGl2Duw%gOZB ze@>PH(1^}24;7`1qC2O93ZmL3>DAR&=0~C!!K>YCa%ox6tR{Qa6eI-dH32ghJl;ih zKCQ}Lo9E$lrNv2*)LFUR;Rh`bP`(Iw(@Vh>)95`^BY|L1dL2wWqQW7f@;aMPO~s8 zUTWwft*Hg^ASD%;m(B2xjTQOBUt`PVF|s?*$0>^^Odaftu16)!<(o93XDQSXv~X;P zFD_^H1!xrv4cxcR;(Bixo~k;YM`7}3sC2FlnSv@GGUP!u?y~YchC<%6yc1v7XkS9BSGg8{IGRNK zMe63Ke`uxr5P5gwlSIItuyJ{G_sU}?-~xU)!ujDkFBuMorTLIXXosN<@3r=l4IqT znT?N;9&FEBb7ykEu34VMbZBLWMd@^Np178k##_fRQhI43dCy$(m&=eS;pC#FRbL*$ zO$|YFp*8%B?yvW6Uy%h5CEm~nEFZF{>fZj1c&FJYkfViZLp4rpywXeBeZBmsXs61k z0_fm4p;M#bK|Cq8mxlBHG`JczTzUSd*BjQ>Z#sEMAfSH$_rD>X`adAuP2|5rx|IF5 z!JD(uKVaR3;XgnA3#23c3)25a{h!{^rS6CQhB(TXK;Vv%31$hdqJy@$Izl>lOSnzX4lq>&xr&a5`~{fB84qtlol9K4iNH zPyV1roE+skUbzK$38zMR$anl5ArOI69YjcuR~hsa+~tUMEJJJlC;Z(QJcxr&nBFLV z{>pr`r>&<=Vj%2_&_??9!e0koR8V;3`PO-RV-{Z=_C&QZM<4&K#gWCf-IGO zdOMT`;S=H+4OfpflmaWc^x&A8(S-S|kAi`CXsyxJ!QR=8V=#Wf0bt(48Ysi{`V7rz zdB5L{=XCgG-jb0MUmoFSHsrzWDI+Wa?7+&JtdG-?N^lMMDnlYr#Ek=^{(3YMN0L(SH!tzVq!Fh{ZFZ3DBsv^cd22MKjt(OL^F^a!YMq4H9 zNFuK^Mj5X^K)^ze{3sp-Cs+8Y zFwjp4 zxZnG9V~NHH62{0q6l*4}X%I}2&d0V9q4*fS`WF%z>tZfD)1A1N2| zZ0lAKZ*P2cWM}CC*KW)P>`1WV+;s?505N(<-(x7kBW=J^gzxMtZZiYKxr0SxMqrTa z9wcv`Hh`YHyPFd)S78dUcX+pN>+Iyn&X2F}Dt2!(?(NBy7t`B?`T5tGGtYJY@x_wY ziift0j~^=!Kep?%49QiTx3(p&^nNQ%ah4+a@5xXI3BBKp>;`0~9>l2)ZZM=>5g+R) z(aQ$LS$V_n{;Er0iZM<(@@`2(z)GVcD+DlSN#ONgFI$`AsilT)QdmO?VdJJG9pk7D zQ*vtO7GH&m6B>}b*d}5ql*tDXHf?z4=1n7316j`^L5mBvK{bQ0VK!K0%<1*m#Vr<&Xpg> z>~<9whN_jf3-UP)vnTb~BKbo$DRoNN6h&%LivmPeg{XbYYU5Ve>8#l^>9jV<%ALwh zXO&A-uXG%?S?5+f^D7V83Zj%zq3Bwhkut}AX(gsDoT7(**dvQAK?*#o^WpFjaE8n& z1~HU{s3w)Yp;Y<8q$bY@3J+pHh@dQq^A&HJ6y2A3%C*>^vlvhtx_3-p@u2mlizdy; zlPBP#8aA<3q3TBv4>%gEaSpT2##n6dlsrCV|R3L&xxDbTbqA5J+w`7cz~og*WlM1@17%; zg_PeZV=pigFu$yGoSXGWGRzPwSV!7{S8YTmqLZx@-&L3}?7c|@Qt6ZN`0?XfDolUH=Ir8-z2T6lPHomzxQFqU z5z12l^p=DuUs95E_*H;!W_~J#x+*K-IuIUB1W2vT@5^h4r7z#UEWR)fZFqmBF>Ic@ z-4x)Udr;j#m4-&cDExf+lED%Dv699UIO0u4@+>m`sQ>uVIcr*&dzW3C)NoiOx5m!? zQ(w&l^#xezyEWtI#%d%`tv!2Srn>A`yRE}eloYRLW@iB^89OplUn&yxQN5)>Z`9}s zQy*L{_PvfE`=GImh?7}r6?b3-rJkbDL9f2o;(Nz7EJI4&BiT8em$(x$NDVyRx>52M zClVi407lBds_iQ#0%gW_hGeuw?t#gZ8lISAB)>qoc}zl)Xw)F_Zb)px+~DP4WhbC1 z5xf$c5VRC8{_?lypS4jybJ9h!LP90;ua8AZ_4jI7v}noc$%x_MHQq2!c-%le zo^$TH<%#&GR3ADvc;E#g?1xZV@&)l;lYZ%ZsJ{?{?kI%P6%W)vc|eD5Yo*m-FbOCV%73&ieDi1`^tQsd&1l)R`?~}m{2ER znBCc6Xr+ZrYAQj;a=rzK!b`mV!PI>CWqMFq#H4lzi%9lMd?{Sk_@(wvDSO|j=r7%S zCHDbH5n!Q2JKCN^)0?^4E{Ted02FwN9<^}EeIGxCBUy4aa5jz`BO~yk+e)Dc^&7I-342y4*wMVBQC8+vkbaW30mV$04y$&F}<%vU2)z5 z0#^w5EQ9!%7I*^v)yuQOk&v_(!n^30=TmUq8Oe7c%c~(<^XKKD9Y_axBVsIpp~^FA zst+QI_JzJvJAz?LI7<#n<)@qg?7B-+Hz^FXAK7c|9knA}|5mha!bZ6|0ImJ`jaVaa z=w_tM9Zln|x1S2(1lhb;85_ClwM(gKQf5-8;HYzZxX3*hRxN+DKO5uZc0Gw^(4nIn z#*glU(I&enurd5z@W8(oW=1vUH{4!zv!YsR`Ful&=GATlAvaen9-;z8!TZC+jSmwJC~7@m{@*R>e)V*)3idY z^%brW-<*YA5WfwO_a^$O6X@K@_lE<}Ya!hnh8Usk27&_1DqFIkB8;l=D#(=Sp?Ym8 z1f$d{L}F4pvcP}|D-n6rpz8oxT$jR;d>?DSg9)aOZf`LuiRccWl%ow4746>%EZ7xue}v?!v7f zM;}cRKR+q$r*TSnUta4fUi{qtEIeMnH!%9{q%J^kq{V;+u#yTaX z6ovdOgUsuQt)MqCs3N}uwDiO$xCDWvv?7lQ0%dg2ekQmTW`;c&GhSi(`)*Q<3gjVnpx#lGW*hTj zl=)RL71yddG?&I^VKp@-W{9B)jBH?(6}|*pGRL3{@8zm~W=~N%`q@7HNJraF-Gqss zhmDc`o@51f1RI}H!M$Wff8S6${adm*I}!2fYK$sqGK zc}o@!MLhCF0N%Zk2ltEY`kcIS&OKY29va+Ex}$X%#~|evzOMV)x(P)^6~3g6uHBQG z_W2sqoWtR$cjWBTIM=SUcgy?cpGHmSGBET&@IXNSD7F4wgdKPOdzC$#>c6kD%l<2^ z=wI*uQhF&B|IgSyP2&IfZ4YDHIBc{&`uKzMn11_aM!5v}J8hV1?c0vntq_i1U4_|T zMvIM3nT~(Q3tOds@!ut&Qbnia{sDmvUm~9coZ#kRd>niB=w2U=!~Aw*9zCtrBqRNL zruU+x7B{&5Gu0HTT-}vwdAN|=sJVEvb zAV7l3_d{&RIowOTB`0!etG?tU5=%5;B~Vsz0k~8V14$8Y+a}XOdwHcDIJF3g(Fs0^ zm2-^3O_Erevx7DdnmI%{#1gqw$3cccYBiDryjU43bB5==`I6gN&kH!-Y58Ck-cvKw z%;M0c#BVRWC8EHHWw?~-ooGEbWK$y9)A_SRNn__WNJt)J3Zml^}bed99 zoSORr`fsfLkhGrD&`+@BC;m2$Winq_F&pf+pYVlqmzi%>ynv7YPTNjyl*QL|nea1` zElSau!i^R6(Wy8BFJRe4p-Is1*UZAwCsiv&AmD5*WQ|%Lrz@RrrlLNY$lxf>mExpt zdS*Nzh8;gTuD$^JmR$7_wuwo#?wbRP^&9q5ar29VzAdJZRTm!TRJV6h*!I3FB&lhdNE0Hqa(!ktA3+F1_?N~w zL}FK(InAEQV(0ycnuD^A@p2`8vGb!{1)^43yx>SovrY9%G$aXREn5VTEQ3;d_{Ve+ z4qBt5%GrS!?yY(=zuQtUcs26 zp|va;z&a|Nak5HbEgRWK<*~ZgOT)3!;MWyMFEtrZqOgYqiL)i*=6`9=|&zey)f zaw?#f_8WVRM3LmFxp=*_dsR~#3}v>xt8fg;j6w@N;vCA}tdmMBOL%|iXzdiGnT^Sb zkGq3AwzPFQCA72>N!R;SvjNBX!4;-w&`T_U!j|fiAPKKOcyOrJz}4)wbyuDVj8wQK zfj`6r8C@)@V?1F{99pa^U>EX+v0(v*#CU$@<$K}iBwIuQY`mhZ6{YKZL9){?5^gJ% zD&Y@nq|@~U%HKl6+!W4qC%7?z(T>wb%`Oh*oX%@>q$DjrIW`B1lC?)V+P~UZ)hEUE z7;z!|({t+9C}Q#8rn*3=guH@>AS*YWr_HgmdxNcQKlVi=q<3QpNxZ_ge0MFbuZh-| zoxGt*Mi)s+OSIuveR|8QWu3Fg2GQCcDe$}RbY$$ie(g90ddn0?zPDB~WZIefDZ41@ zNM9Sp8n7aez7J{N7zL{eqo2{ZQCSv9X<4j$5VyItm@OCU-zZ0eLSTlgF)4wW?lLhy zbMarf)`j9GV?-^*_EoqXBsQ4!r{Fq+JYzSq5MmdLtMaaHB1vbpm=4K& z6q=TasnB6fC1`#k&RWx5fsGeC^b8AVK2Q`GkOMc734wf&ZI-`PV{ox2^W}iX4pc ztnT0H@@3r!rnUj&DwB@%1Ky`l9~VnB2Bg1TQG1!;H&3jR~4J}&_xvB%O^Jno^x5|TCfrRn!8-y!w^8~9Z$WjeV z2$SHrOj~B6Q`%}z4_8=7y`5HV?>p|lb4(yEao@YbRW*t{ihkR;zV5Xd)Ex-bHF)^H z?C~-C;c<5eUsMC%zA+R{BEPE3nAHCAfWNXGX7#DUepB5`!VuPO?NwJYxubNaS2Hm@ z#nmy;$ERb=W+nv0Gf>(n{+vXCGv3b&Q~sej=)wNo-*I`SYeB@pV`r ze!7jFrMHekDYgtNHJYj}j30v(eA6M1T(OdZVL~k_vCcDRL6b=MjDbJycifrr39p<$ zTKTeOLe?um&3lNnNaayY!Hecbou4vmL6*vp+(fImUG2#9hgaM>o$XxD5(lr;*UaiZ zRgYO2Q@AL*RZE&pkqkPcMKW-`-aUL~`)}lP^u}z_^7n=YSyOq%EN`ks{GpC=MrkIzl!Q%tK*e6fCP^IJV^g{jU zM_@&2w`fJ#)z2YCGxIB^a+Yq2^XL5IQjL9P{;WUzC1brO`i>;=i8H;OZers(V!C-C zUrQ;243tEJW8a0sdh7jsG8ocjjx(SCwo1Hu@YGoqKKMxI^tBDP#JcEP-q>jj&or8-nj!G#7O@3nAw$-I}I-)B?fsC$Q+tmbrSt=dK(rNoz;* zzU}vnK(4f1e%GKIq^+)G@}g!tMqdA+a@588w)pQzG}Oda!G6n+;x9kqy>`uoJJ71LNaKGUgNj(dK;S(cEu2gt>47+{7-ns zv0n#x<@fOYA5`$)BvIl&I`!g2KtL@2W%w>{=HX&%=IZ*rQ}yrTf1!%v|LW3b$^Rd! zc&YgvFV2qinP)J7jztPCZH#z|Q5JPVK7(E@7tJ}u-X31C>PjM;hMQ=u-28IgMM4!- zOf{^<#hSEfH;v*}vc7>M?;n$~lJA(E+bomZ(QB zyViPel+wTgc%=n^4QinobYs0U`SEn;^y0nql13D7 zoB<)`+WPqW8PZnEkZnwrL~*L>QryPSM!Sw2(31VGK+48oxdJ^&xaTl|tSk{r0t>Ea z1`XJFCpYhs_-nIThq}a`6Qld*%`a(zC&I4WjPT{#y4YL6%WDeJn{?vuX;F!}yjNn` zwDcuDmaO)0!zpNuWEf-P2az<)gy6GV8kv2ht}tK;7q>FwMPGY($~DpKW)?OIu{U_S zUxRrBp%S|A#7oHW#!T43k~KiLywDlSkK_E|7+noEno&LlA61 zJ;MQ3J234`mkM+b+?oZ70FgCD1%VypFBtmlas9fL;7_y_+qBUY*3l-lZen)mFwz_K zDl0p@`bSDqFAzkN_}fT~DUt|@VxD=pSd3o*=uH(jy>$Snv6ES_#rN zJl2O-%^)oZ*i)q>D^pWM>Kb zt_$;{CTzv>boMMVk??N~CLab@sNPaD4!C@9tSWy2Zofru>Q%n|j@q%ug2XO zUeUE&FE4is^ypF9EoAo*myIquG*DA81-0rwMvkp$T^aDllbk)u-kkL=sZws|R%t~- zp4Om16Otfy!P=OI9&JmQYw~&K02-KjTACqSUsr#4el1bxB0Kq8C3rr*;%&R#7T{%O zFIFMsB<4D%y>pH!5m^jEgKU}m$A*}z4|&VLDmUyytp_6Cd7f8tEecE0^pUJ9Ss+-4 z0=dU@tkSD}`@kOkw2MwsARP)MG+D0Gye=3YtPjaWEBdvPk6FQ6hK4S?Zs(iiiqZns zr;u>1qA?ekJ=GxV?;JN(O*T0vFK0awcHx!i;r4KKI0PiQLgngMQfk_Z@gifwZXJ0% z`zjmLL(y)p_vR6KlTF#>)oKi&Snb(*IbH)jCu;MW7Sbpz1{-Lc;Tu6QWRi_UeLSaE z)TO#T{%-zG_*o5gj!oj&P?r+$2F|k#R2nXI&$U#77{S4L;_U6AUO(5?J-n!ePALY z50uGtq?6BiGx2f4gDm)DeBDWWl=5$}L>eZe@=dy|#(j+ndZidnAg?w>u4|K;X@3uB zsnR%0Q&w0hmZWk>phtw|2ybD6gvG~<&CETA$=)*qC4m?9WT19vI5- z*1Zm6`tTqh9!P!|?R_fc$)*mYokQAL;JaiIiEZr4tr{g(R~GONbCI01@SK-OhaO`L zxGTxgvxVeAtBIS~f}d8JTD>R~=!q&Y!KVw`fFqpjs$uN5r=Ui;)Tv$qT{^Rv8GwKe zG!Rx1!{{%$9>rkL4G}DXD5aTQ3POVU$!UZLIih5A?-{IQ(3#oI|K=v?-8#vJJEC9L z3^((!o7kfq!RPDtB!`tmXIPP8CtSrF@94(MlyM=m#Rf&ULH)uObtYkzWJFe+huSi! zrF+TWpMQ`*dOc7em9M|$;}QWfXp; z9p`h%NObDw*Opc&Pk5rY>4@KCAOj8yYS-6f2GNb$6UHBzhsz}{FV7x1dY*8|`)h`{ zIGauPOMjXfe*T=uI&QkADCGiNxkHUo3I%dz=0#Iu@F<8MhQ+p($0$yK{|89_ z8@^fp1HP%n|CN39zw8PX{}Fh_@XzOe0qyTdV5{evcq@0;|1(-Y!m&}o;kx5ddm>2T z`}W$w1sNwNkPSQv>^evbMXhgZxp3JDGI^}x(gh+=s@7snZ=tG-!}RI0nbzLvHT4jcC%4Y*B=k<5ou;nh_k*~* z8Ec*q!0=cRt)q;Z#|MQ3^WMv2T7l z<>*hE@X2$UJ%RaV%z)uP-pdJ6E~@&QBal6?A-9zu1u&n!ovlZ6wv?UrK*K)Jgy zzK=$!k}}MnS;^s|PVPao&|L?)DL1!W@7EZR=Xg*LfIzrVhvJT`yzI0!FY(b+E}8Ug-0+JoOpAv7|b9Js1vnS=s=YGZFp;l2dduH16yaf>D78qJt;HT#WJ zP4p&FhBmIC&^i<5+_=M=;)tY^cbf#o-Jr z?yRC^l32$5`6nAAPtU-aH}ju#bjgF@*fPcCK-4$T)S>Ik?vpQSGR%wpo!e$om}!{p zT*i}(X4f!(QLKHm*RgKdr0GFe)qv}Q$WihDLRlJbi6TCWER4cT4AEsXvM44jf=cM9 zD6}P1J`su-^hi|`-;COBD6SbbHNwUlGK)r z=?SuCX$h+&4>9K$%m|bGxREF=a`R;4`RP!|hblxAfpDl8SMx0-rzUjuCKA~^iJpzL ziP}7z0T6fn9M;>dCMMJZ3DPwkJs%mSMT|wLk1@iq+c6P`pF`ekZz_%crrVWIh2A6T z1JKrb17|JKu(e`V1agk>7DT1K-1A=c3)xN$(?dH<$M%?Sp+;G1L52LL4wjud(_`Gb zQNvC$FAHF&OK-uaJ5V_+f0)3%J|SGC`W>OwWrj)*+RR9A1;#3I)Ti9U%bvrZg@P7P znyDlA<&~xpaI&u6IcCg%O{AffwWY+?nYxS;LOu4eyJ^u$u}x<5X|vWzo-wTEizP+B zs~dGLLOQ!u)KICb3i0gLm3{O!{#KHnly7W(j7}WCS6hI85`7wP^9;jV)}skqOZmwp z1whp%WBmDME{X=o;KiaN2AZZ9uuqck(6IJ!+*Iq~6Wv;R{}_nO)ivy8Ce}Q4J($P* zqZ2cU;0w5JgO`hQKnB`^{5>q4qk)A^ob|&uUNrIui5gqta#7oO%L-d`qkOT593@w} z5uA=%SdH2+SaqJCInf#gTcfAZ`?;xx4j4>Uu`Uxs3*o-syx2kvRv@gBaBdrorPOjM zh&>+{rm|7mIHIK*7}Jga>`Y0vUWb``W{f0D=M3kfM;Drjg*?Gi-HN zC$5M5D-cUPmAtyO-@t5Q)3Jy1;9$>hD?%w?T^m242ETDVx4Wxi~%zwW5(TGd^vL_zg`vMV~YN37K_7fZr zyvzwPl$-nA(KDPK+fQ#mO~n*msltUeVps)nPI3%L8i|Ocn>|8<40pS3Is)|RVHQmi zT^uizQvWq(kr*lZFp6-^6)jCJSo5e2i>kxmFpKt2yTEAXatCshrNl=-ho6uqTuZiD zZtYBOSa_dfZEfP;L#Ce%CkM=AB{qw^hb+PYgTGUomX?~tkk$f;`avVadab1-DwUZp zhQxBtE4%xUmG`TKI#RVs%(N3FxMEM$MB2H~fk!<8KKZPtn^y}EWm+-G^}C^xuqLCb zM_9l@EZQ)$=~a-DnNxjC3#`A08^-3rTZl@l@zi&U_^T+SkSgO zhWM6P8;aY!)|iD(QdOFA<0*P!BBmw)th1=l#d(4NOwxC!17LEqrbB!>c1YAR36=rS zg%!ho9MfHb->ny^_E`>w2Tuy&ZKgc;%Dk(V>8;uWZBurV7kku!SjXppI?Lw5tgHx<*$`IC`I1U|j=2 zF|K6qVb0^@6v-3Gv2TgUagNv$Nw^AYan3=E6k-(KRko+L0nL*pm1YgaPT>iIM!_5$ z8!AF0Il7J?BhnMyjaI_!+t0hP-!!DwlF)WI@8iC;?#c9U`IRBt64^DZTN6F9o~VNb zAnW9=pLL#suR~p{th2b#ZJW3tEfR(Tk=W+@lTJv&j{3 zV0S7?H(`M1DY_d)gP9#EK_#2ZINUitY_T}1XeBe5?P?WQ%+7_o zc>`{=@_}6PMw=+t`yA{YWDFDO#HCb-DjO>980zC2>FUcm>a8i+s90 zB1blc)9eg_qr`B%8_F0vivcHKj zbZ%h{ZcG7+yjv^Q$LW0)UPjWxyO=xt5IMl=&W0o5OV=^tRYzpo9G;X8{k2S@?GSM> z?-H*hkhWHqw5jNPaRMECTxs*;t(t9(S6!OBRmea$a~;ns9_Zq#dMf4|n^n)R?jXt- zlB^DFClhl0N6lyDgO^H)XtCW?BrRjqP3wA>itvs{RgNB}grBMTQC8fLwfc&c2>X+j2>WsEDkOTNEoy)u>bTj*I`4YNqOV#xE z5;<24rQ~&{^E0m=)~0N^sZ02bjtb!7!{um2$cadfw@XIdcJZ*@P2ZU5rCL32l zsnEK0hcy!`Qg-tFIt?95-pKa2xH#qV{=ob4{(E)#eon0xeqwMPWZtn0E@&H{VF0bo zGnJ2ORR7^`X+Q1SQqqBvHq5fmd6~+5>OEvuDT*PjP18-z4hL{gt3yaNyrZV8zRT3% zppHpua{^a_4|y>1=m2Ljqa2B3O{3^jYHi6@@Gt2Oov3(?C{x97ij1X(a~=4)$$D>< z#{Ni?>|xc{lDeQlb}Ud-+&RuIAL9SQ#e00UIH||rWavXcejYuD^<+YwGeZc5Pulw7 z>_UfArVsQ>K%j=hTTUu^kxPTimjJ(3_n`Su%;q74o9)HQi4a0DD1i~jLL}u(h60%8 zN4hZ4M_#W*6ghr~<85uC?*W|fi7NyqMd)4=reC28D#gMD#LZQ*weCU%RTfA?89F(C z1R~hfJ!nT4-2^N5WXn~&!k!@5^IzfKT;dQxOOP$Z!d5@v1#a0uMrxC|=&cKc8*}8# zNNb2{w0B9BL)R%#9w3b^Xku5N5bJM`ez^wvD-OA+_14y8@$&U@qu+FbSHA#<*Nd9XyN6U?*1O6pioQ`>6-j(;yk7XHeq>aYdwcEv~X$L#uP4A89sO+mmrf z{VtA)7RYDASS4GJ_$L9Sk8K=tV0rrlnHTPgYg1eGCTp;2r)8j^a>{n$)iRY4cJ16T z(c10jklnL>30Z(13mq;XyXz4t0@CsZU3`X=#xs`CG`1$2HWmp@pUzvB1$iLwpPClG1q8*scNz8u$KCIJ=SWL7SK*H zOEBi*jEY>b{!bf$vu86HKQPn>M_FbL-BV|swC%L zB?sv!Nrh19K{HM{9HU9`8`{b|;)?j#RVy;U7Y6hX4QcUnSjP5G ziNI`iDdNPcE>itMwN&~FILijZ&1h>WUqVbqLVARcH8Mn}mq*G@P-?b5c#4*s4t&_9|+6*`zJ2_ zHTjvwja=OPY0+GBw;cohp`=yL^bX_}>nQ z0^k;p)<+gkq;smte>{jH;K(9im9MT@s~Llg@t^!fj|JrRA$PaR8rs(}P8_eU}TRz=?Cs1KEQoblR{$g<;ZUvJJxSh*0 z?34J39TR(Q{)g7}R1z0GJ&lxq>GzmO-&b6-#zERujNj5bMR1~xNek{SHIfd=(N_5c6YCjqICh8LV=j4%3Z*L~P9-*>t?(TIE4`O#Yv{V*P zIg4>5lE&biSvfb(M(qNBW6&pGS9UeX@$E!QIA*KICaWaLWvwH9%-#Z zgW#hISq<=w!$9w~gllt$g`rqL!x1=U93)AG5?prNpZs20yNCJs5C9@fS`vSU9yen- z6c=aBwzR}JA((XlQWSlLf4$!udPwQr>&QRwY?lvzP^@2<#)tAkryb27K}yZk@=|1` zwHiGNUZUrxkedxN6o$rH%gdz-ABj&-J36b2?U~RWwmST~E`@!}o1~VkCs}h!lgJn{tWCD-&Zdi~> z=vI281Dh}*t_}UWn#M}p@!M!w1&;qOnJo1m9V4F8Gvs`NEp&OrSM6s{Je(gbA=yfB z8)};%Ai*Y(LUi;zrn7rnEO3(UP?%lb&cNd_k6=)(^$)wP7aOltWE^|* zcdG;*P%$iiV*^(Agka%FF8wYUyhaDZHk@Kxw+v!;9FOqCXS58N9-I;5$cd~e2kA|cJO5o z5$aSHZJS=&HYKrps!=^4e~wxZRB>opoX^!FDa}N4(%KzWQCNYnVsGtrN9j`~4{;o2 zV^(K!KO=!AEqu>|l|?C&M$Sw!e%=4d5ghaT<@TYJ7oJa?{C4?Zin0eb1Ps_8e0qF* zEa04a;sccrf8lJ@V46m#%3=1Xs*o6dc&%}@8_KPmDVi}IhF(8k@1m9Y6xHg2Nk-Xt z81oYM2BraPsC4aOZmL^EtQZR-b;t|;Q#EvLU=%E;GrzW?4|)`b)fm_oADb=dqhhE{ z77i{i#fR*1Pp}9nqt{1fX;zVAOBM}PWeFVELu4=MH9+Kf@94QT%CcVRk z2xmfB1ChG8WLDmUv=oHBHms{uTipl}GL1w57)*ks`+WuSC05f7G_xHdnW;vrfg1uR zwck9rB_xY-e!9zgfk~%JGP+4zTBr?;`F>rPL@YZ=YFbuMCqS3P*rRcnW=z}akguQR zl_H6@4bawSYuvPIQc{z?FvsbeV;B7p@iH1RRVKSbsjR3&W2L_kevPvzMN>1En&9u* zu8qzl?0?G^hu|5V&VP%YWU|1IlKQ?J()3$%;5+m=8yRM977jC0H_PqeA((=V&~ET+ zSUEVPe9+O8;$*N{E^t1&Zg7R|2Jt!R26%1y2bb_mo;F>q?j9nnQm*Wr^g#1XnrX4& z1#E^ct>|=;G62SNz1w(iwJY=&o=QhN@9j%g@w!{d0PsKLtIF&`^@U4OU@oystJJ{| z>G3y5cjksDGG;z=QnM!|ToFc3{0IEMyi#1|x8<6Q;n99OREL^)WZa{xZX3an+E~Gp zqY!l!2&w$aFo)%@B^3)hyT-t#43Ff8$Yq9ID6$(_(B`nJHtA{!#Va4wOf_4n-LCD{ zjtz!XUNWcR!B+YMT~y@J7}*Mo^GS?JGvhP|lgoyi!^*CZKKmnV$=%N2+@CvBYoQ8of>0%8nH_B;; z7{ZIiMMKxB-y<)g|j=%hWqUf$^vqF^6$0f=Af?G zd}PTh2zc5S*|DeTism=_WX z%HXEIC{p>ojU5|>uu}JstFuQwcpBcg#yXMCf2I`UcbD;{S8dn1(OV?(O5Gw&Bgv?M zt9WTjtcYU3t7VK_a4()%l(C=$0Ek?crzX$GPb^TNiSpPi;T+eg1H>L145#xW@~C{_ z?x{f~iOjtyMtQ$3X^OP_A;AfOS$Nta+Pf9-x=M><~e=|9ZPMS1#e{50$X$iA_;*7v{i^=814eBsy#UI@S?c z#=!_}0Zj`sl{l!DC*p!%_WrA2th6&w%#>byVkn_Xuq;%f=5(G@jznPQwS1ev#>j2y ztsn1e)et@@(i+!8amoZB0kr+8kMGBL#%eS_^J#b8owDAYt)uIT(E$^^zFBzIVhJ6}8#7`C*LtW4nO3C{hG6x^K z#9F+$%^O%_1s(6M6J#1d(9pvwZ9xFTYpRAKgeN?wmqgds$N>4+Vd?{#yJB9C_5RO9 z%z34kxwLP;tA9}6f0JVC{~*Qm82<|D`d?!em2X9amA!=v!~b{qFO)d)U&9sP?a2Ra zMNp&p?ZwHC)O(@vM~EebH*U+-yYZqulXR4y+?`&D6*p2>DpD{IT(HXc;AQR-;*}A*lu| z-Y|E3%$bY`j)Y9g0(}k3X#j6bHqvWA=iHIN;9EhXBXgC#PRCKVjcPoMZdWLtZRMrw zlgBE+AzYIYXtj`bMrlKtsu~pkM!(A<(_z)$QJH3Q?m+dM7Pyp+r~f`G3J#u^*~1dT zr?iF{IRbZCnj+JX+alJ~%8{%S$`Uj?6X655f8Bv-zX3c0#ZHFktT7Y zh?Y5ZPlnAQoA#ie5U3<#KToxn$zCq0E%(&1Hd5_se*Tb6eS|^3H~|Y=cRWj`g|>Jq z{!_2P{dlGG`yGZ|+72unTE>TejvTHX@u)pjxl7Tp!=nObHuegwhpEzQ(IhVw(@sD^A&uRP&K zf>a(OhNE7UH)|c+%F0b-0`&l8BJhZ6%A;zyA!5{bx+$TP16T>cVn5Ni>VyK7 z1ViI4W;>h7BpP?LJZpVzD3ab}s?2+;XkrDMDMsmrVlhJB*F(}jMJnY8`%wa zh;TvKO~#>I?c{TFXoctpq~6Q2N;9&wE~qCQwGP}es>bbHN`HM?05*xJs;VXHWg_Yl z;ilTK078bgNx8$M!6{PZzA7^T%4-2=GQ%-WwH2wTxyJNI@8hPHX4TSUO;k6g{kqQ> zMeIx88zJ9Y(RZ?|d8?_!YGp_EpAU8>?zZ(9Tf93^H-iW$1CyZi6|d1X(W|mPO#y)k zXQYTzJ~Q^($P8?U2K4$JFL7ThNGzrhld0Y`fwC}`7z*H5GZ?JyT;4?dfd_4;Zl=p_ za*JrN*uiu+ULlZZg8g!YMWxDD`X?g!Lmwd%O-|ylydYf^sxA*tl1ss;R2! zw+Cc_7FXR+Gg5E$)+t2!5A+YJcTWm3$EJKgjC9v^VVTUt=%14>H3~U0A?tB~AKGDj zHMni}`LYrt7z1LKfa(FM%YhbhE~N0o81ns$>l>ba8+?Xyk!L5K5vOSlF#8u=7R*lm zfIPMT$JRRq$rff?qvhJAUAAr8wr$(CZSJyd+qP}(vTfe#?sHFf{C)CaWkg24WJInt z$C%&13iGvg$}{;X=Z6`MNyi`Sw%Ncm25dP=LXW9D7@bpEP)6S~{o zoEd|@rY!fged%l8$)J5eWWVRv$II8iNWXF^QVKJ#+xrcTnoH*_)=nJCrN`>j+|re) zgM)QWhlB&M?h&=}d?PJ3n@qT$RB6t@F^MWnbIAh z49~M?HlOe6ILE~F8sC184IOEB9{(Ik&2vB7ol(CyNbm+i4^tu*TJ_zltKT{~8~MH} zq(X-O(&$ZTSUc{qA^d3c^2F^e|9}E{CRK3WHCqYTC}dbE@R`^~=4k;%?hdNqX#VzJ z`*^k#Z;p?ioRH#)va~?Zth!R^;o3@YaJZ8F;D(hIKiP-pPjQOCzP=X}3)t`K8VC&J zcJkvGEXf-%qZ3_xI-SIS>hG&&6@`kj!eH1NfHg9-j20i=AUY;_yM$Yj@f}T0WMH$%nCc`HHX8=8Za@_;(SUd=VzR467 z!K$cHro}~HBW*=ROr{X|%oy|$j_!b}hKMB-Wb59m93h6h=Apry*itVO&J!3r1&aYi z{Rjm7`G9x_$ZEn=OO38p9F_$+k0G9w0fij*QlKuK2NOQhOfWe@mLsy~rp)G^V9yuTKmOuj0 z;w~jDi`o}hln%q_Q9>NGnx7G)$Vd5sh6w3N&SPiq9`Ts*Db`>fgcEeF=|UN(y`I1| zh%D%*C|866af}{ch5@CoZnZeUg?Giq=Y6%Kxps4n1}rN#v%AaG7nK#{Ei$(DZ${OI zwge<8fFKU1lJD}ArYI%8tuXHmeM>NI34JCn0zw=lFuwsrk7AYpzhcGhf=$kv)oB#gk-n=AS;6 zZ)aR7^8q1pGqXs@L)+@bb!(KT6wA-^EcOsTl?|i6rM-WOM9y3 z0VtE$KU1yPgsRHO$ouIc`<2^4;3Gx#-X-W1)xtZKw?R`Ijpslk7gg28A#sSXkf7;* zha`En)T~7%LJ5$Tm=%fL*z27l%t$ruA0C)VGoNBQ9QK2UnB}xu7XK~yaFEp}l!XIm zlqBAy3ULg{1|HAGR#2>H+DnZZ4yVYU8%?|PW*XRaN16>5WjRrF;G)At^pPV_^qDFS zE)GVy3=dn9Nm>0k_vCdsMAglGKSP;XKmmTrui8JnL-QB`k5;iUEATH?Qyitn*6u#zsb)+o5mtf54Z@y>|+PRH`(N-MJyFQyclh*1c zCYw16l#AVA=I>29xb3CbXoKrk0Q#GFqpfm%!jLA)#4+_+kOo%U%GBs+V@ApZ4eT1M zWqjv(W{a9>gFJSJGL-d1xi*lNt>?^Flm(`7p`oe54Y;dac3WZyCawX-#EX!q3*62M z9D_21n#lr8qnSV2RUk6JRe(5#k21AH>lOK;99`YlP}J}C);YCPBmXzWvXR> zyrHf4w&isoi<)@!uoxPkQFQ^3G^OcVLrch;M8rk&Pc)RgfLcD|nBL4k~# z---8h)!C%G6)zO`LKM3EipNQ-rl!b)WJ=b62e1!W^GNA}#y3fdf-S|Symd2nGWy(6(7c+K(OQUUL72VzlX%^$=U?M@;W zHsxTvDwI(k1F;tF*r4GWoc<&}%JYPdTEF_+bzjtjM7(tA!?}bdjd4uX#R9_!??yw1 z()B9K(x3sOvhQ%d>S4eHkH=`G&H00st-8p4m@$!G%d@p;t42RD5L`E{%6%E+)Ta{@ z&xG~DV@B}*ih33&Dju@pgOgWTB&`L3Xi4SJuKwkOTa?p8(SzG9vy&Z8uU}&qHh3J1 zZ%-4_@;JwrGOyfJ7+-tait*ScbVSd}(~W5y;V#3_X9{9BnMYWnG!U8d?squ+1dP2J zTsb*YQqyKBeyW%Xz8dd4B@D!o zaj7yvO+cDho`BP7c&g-vT~(*t)DTE>B>6nuc=Vw=of~MlEm%NI%yFzr7q^$!K8*}I z3lo6lG4mGkqB#M9Aeyp$2K3Sh1Mk|%{cF>iW&`_SLzfS3;7ap@Qtv=hm;yqJmZ8C> z+P21%4|QM>v%RDJtoaoc!pyQ$^FXaiRv2F9Bdt|of5@wA=K6#kQ{F3c7J+y=v9e9m z5I%%Wp$jhPEcPf$cYoGc(RL{I@CQH8a26w$n$3d1roJdSg7HH&7k?{F9Tf%$) zb^nV71_M@+jjiW5_&?63_hW>}KfyyPlZB*!YQ6dg9ilw~tP+pr#xKiVVhB z8=VsCu$iyQ9I~;lq9YJpUL^|6!mkpG>;}<%z>M$_*?DTZ^}S>t`0GtJW)4~DH#mQ@ zd9-`;UK)VMH50e&AHuCFH5J#H_QJz2;j;^fR?FLUdiVGD#7+lsIiQfFnYr-JU zjo{!cl424)x>~_wNa?i-BF+Al({!RHcm~MXox&*f^Sl38{8o?hf!x%;2l*^>y1Ae0 z7J(+8VwEl05dRP4WXr{SV%Y(4w3i%=*)UY80?~pm(`NcuRzdwqTk zMWL{+5pOMdUBpsy)WwW`6_TQ0NrRuKQMQOjff2`}P$5E^M|g*Le_&S8nK{qe_H9EB zRqGjUO5tc}*CS+ZFp+`NRZoD%8cnW{mAQPx#>A}8=mY)BMMKoM=|NOQ&`)y@c36=* zmRb@DK;Z@^!YpQNb^5#VF0~U%tto-nfS5lK>Ti)`M?S3GxI)MtYN63$daGZ)FM7S? zTz|xGBZa#e=|vWdj=98Ay{3=2_Q--sw77<4KT&-$F`FGmPg2nU*8u>t>hOTwg;yjk z0>xG6C-@MaSj_pPe!C>&?|g`mXKxZ!7&op{g&F|fBVa~_HQ6w!X`;X+#GfFh+Y@+0 zVZ*o?u}Ist?)ba3@F37O+j4e`I-yVGYNs zi}9s9gU`M0XkFup>NwD!WWWMt*)lPiJ8=yBKz2u*1MIq?@g(9d3W|lTJORxd#DKbp zq4L^al2XeT1O#I98l%FhlSDk-5*`Ma^uM<4zi$C>u{k?V4H_2H|IFAriZE&~K0Mxk zDa^ZFuhx;=3-<2+MFu>Nda|2|B!X6&8x&xNUU-LDHi-;m841>Ut^JAD$}S46#XbKFG0Ci#54O> z&`(8#j4W>7ab%YvoAUjyx>=fqbP`xSEW+Sb`&rtCaybt{27b|^pXTX-*2`_ zU%=X?Vu$5{p9j0HRu|C{1+Z%S+sYw+aHGM+~{_ zVAA+igI6k$V_RN32OpMGs_4fV;X?@8L5y(tsb1PW$+J5&F@_@R6@v(ObB_>BZ}F>@ zCW{rLz}tll;YqJ89tPN9kQz>!`}4SX&!W0Rsv#Vj3ZIR^BNJ#IuZ4;=TT%A`@EEPD znmzBp1dZ%yeGXUirt7p?oOd}Xcen2rLvWqrEn+DYq7lh+vpOLM8S;G#t&UnP?O;!u z)ISF@^LaqZEsMS?3057+i1S8tM*1}kvm9inF7V)S%v{_j{}eM>dJq!Ck-T-yGjVED zVX!jyq~^+8YPS|mKGd^tQ8K%p&oO7uNK+eGueRJ%OEy(?d4tUyG(wTeE;a+H=YXo) zN9lI(FVpb%LrVfE0wWdSY9iE(f{OczI+=@1@5&aNW1W20U~r5ffZc z%HFZn!M+m(GSU&G9NW`j8ECbsD99`f%E%KLnUMLYgyduayOo-CSu!dVn#}R|i84FE z?lMC3P!&rKmBzg<`3L(q$bKGi-E9RZAXl`8k-<~DA|?$ndTYw%6vh~`p?-9HBiMEk zoPEX`xjbKe&U{5skz?^Lb{rW=8^BL6-{%dqBj~|jrkEGuEaaBPqAEa^9j3+TMvdrp z`#AsI)gDbe1)o0X{{^Yhiy2Sr-BJQBCyxR0h;A8a;kce40;%MVE5iL_pQR>+S*AMl zk99MiGLy2Cn|tT?JlE7(LDZ)rU~^prD7q|o&po$T!;D$K%-)$xr7RcSTo(*~Kd2!6 zKCyOE61GL>IIG(AS&+`N053yYKoz2R%=MLPn(rf1W{vo>p4InV7J7`t6A*8`h-cI7 z?BAp%H1dFuam0PeZaVLqUHQv)dU@261q4Jif-awoCa~&z zt3D|#c~?OehkfCX(_g;>>uL?FgwpaV^=zkP@r~(m!BRvZOFv~3m6cp) zFxdQus^l5pr;Fv8Na8F9bCcKCy{faj$xb3nK>dc6rBgbO&Vo^4+>vhG&(a`tr@1Zl_o+&XvL(Tlm3XewH0dq29uezYYcCobRDzeMZtJF?4$;aO+I-j>qX`g@PB&rSvVKQweTCP- z{!@@OG(&@J5tuWDSsD|t{atCw|GYD917!Rs2YawjeRj+DPJdPunUvMmS+7-{fN_v; z)+bGWZ>wX+XK8;h*6e=S5)9kKR%9o2OYhn9^XvHWOoWO&G47lSk6VO-e!B=(YQeDB zvE!U5L4+cA?PSkSXV5+s5kCMu>Bzy*`8RI|A8M2?b^Y3A4i5J^&1>O#5It*V6=AVg4@y3&Ym@3jQa7}aLU}c!e1TBYGE(~`$__c5+t4%4KJI8hvIOc6EOJ>*T zQ-itkesKzWtnusq7 z(hU#XYBJUHbM+~WO6OSjGZ2|xn?{DY+Yo4!Q;N1G5o{r}@R|8M%DXl85ouU4V|gTk1W{7ZDW^WUvPQ~%Q{ zIla0?w82e+cgzQI6&2`X~J#N>$fx!xnxT~S7h1d&&eCH zcqcL>5$I3>2$X+yG8H2vtI68?3{HUV@F^i#h?Zxr_jY|W;M9+pe3n4ANyUsMQjLOW zh7kRCUMy3yeu>Y!gZR)(B_Ll33oGcu)C^;uKu7@uP%~4PBLviNQF?9*)tG7-Kzj>d zbRaO&uW)0UMP0pSG>{p^vhBo;(h@3$&%uf_D9JG?g?r^VrfRz-MzF}Khlz*QpAwyL zqK^P7Gl`_ybl*?)&kNQ^C>$~P+=T{-1RB18Vp+pgl`-Z2JGF-&7A(LZ8 zw*M>8qMIKoAD_jG ze(s%Oh9OK-_hI?2+rM=%9`Yw@H=se89Yw>c9exC5AUgC(7191IPhL|l*p0spa*{gD zHqfCH`{@KI7%oax-^}3dWMaw4LDZR(5v3RFsDQ`d|3YHV;fDq@E^_zd{G$ z3@WAzXgTC#dL^C(l_zM!2yy-5MT^-W)5pciI-eHZ$FfE`;^wM0Jo zBpA^lyrJdMb7184Jk;^g&dCd!_ObQNA&{XP_9v5#K~DPD^1Yv3PcP1@*%y~#1!M4C z=PNq-#OTDvSj!y>0a&^71s7sV74$uHgqhUXzb10V<)lQk(*&#qckNODIiFON9)O3! ze5YF&Q$=yW-WZng9p<0x9)h#J1`F6(DU;wNHDNI&*=?d-b|u>o6GgXKBa9E&lw>6e zeFhLM^ucgl9ZEhxg4THbZO+7iQIp20ydZS4*&|8M-33v&K}u*Kz%QXCNT%cA_igX= zg6Y{uHHG7p{f#`B7VASvugJyGpyfk(d4`|6B@KRx7?qq!C8LXV2IonJ_pN*qmyd%m z+5~Ivh2fV!KF7a>YIgdh>MLEdO z%fUWq8mOrns>f(KY)NtMJ?uz@k7x=nPOOPaHgw`2F0CKM@ms`)oNH2&b^gzy!QK5# z)A?s%0e$V|nI+yE^Oqxej4f@oO8Ai$E%6HPM@NZz(s12F`WC!2XQv2byuN6oNuAlI zhc%2^almPvbdItK+j?^Tizu@TrJ@HaDnrk;xO5b?8$|ooA$aMD!`y7z1X5qmt!f^w z9BDNjt%p!5&WE1^D}iiIIR;XfPy((p&G0GqUqJ@GqK;fIgxs=T*|!1D!&7X6&Xc)q*MP*p5JdSMY0|PkbB!l>xa;y=m4Y$DB+v-BhK&rt&{vzu*EKIe&9j!|ABdpD^-u zlu!!!3TE5(s<9iGSJFn-jIn6kn0t}GthkDvs==_n?Vw?3;JPO@TY3nw;+b;mXdcpH zfd8aD-aM02M`-G4GUHgujteDnXx##Bj?ySsk z${*NyTSV9clB~pFjnn6<2y8_z9e)jtP(XI4{amUmkqDD=x1tE%vM`NCRn#GI?H2Ih z)YZOsX?wG<0hWEKge_V3_j2#pK9D-9{JH?1YZQi(vkeTC(4V%a*M#>G&Gf**=`<_u zE$Ox?;A)u5F)KMW3q=_jsi$WBtTss`ZBZPA%>p5P!5o48b=O3oknFg)e=v?%6jE(q+UwV~$W!P)ia_#mr?k!Ljbn1AS*T8}bx|Hp`z6#ET$& z89%q06&tkqxAX;uw2j5w`bCr*p)tB-y%xo*r0iKOz6~{3NQ$6VY%8Hw4)$St?SD|gSt?~l?*SFQI{FHaxlyPWpK|d5OjSvD#9RIF=C=qs_Be4io`5?_XYWFFl`l% zh++E33Pu?PppIGT=_)^m1BTWZj>3fFA*ASzaD_~Re(Um~wIVwW^1d~&1nomeB@N_` z=_z5`re&SU`n@hR&!CniU;LOws#HT#6QO^v0$&M(wW?`b!+l-(aU=WFq$|N}EJ`+B z>0PO+&XDOYNy2w2?Jt48n(V;E!bpYXyHj4HJ%o|R9JZri^&iPQ$*r5FfJOTeMvEK9 zhxKzL6jpCOYq*%_j}Y?;#tS*(!}zKLKT>Dt3r!z)qWxIU?5>qZq(@}ZpaECQH>JP`1c(LJyXonK-P9X8w;4T65PH&5R&39JzhKTZk5zhB z9*0|r>A9w&E4SE7?;elq7gvajZTkBWvpiP}K_B2c?@C^HmQBro`v*u}Hm-FB$_s7y z-5O3=kIPspOrb!?mglRJ{1u#sF|C)5)Tl*k>RN@%`Wx|)Dy*Z}g-`w&feJZZY-CgJ zYN3+no%xo(j-@8~n_{rAVxID}Ln);Th+@a6iUppB#JB=K2&M^730VNP>H?iNd*o1? z+FSh2IJN$aB@EMx3p$F64}Aslh%KT0sz6D<{$-nDDd48VY+MMU=xO_AM|Mm`d)(cT zB=s(gWJytAtk&ce5{%XUUMD=MEmXQ*J0Jk9ZM;6D!olj*o;N-{TO0NH9Olz2^(G^& z<}4NpYW6JkbIV<_I>#_Y`v=Zefn%i4Eo7}!*0d*e47pbvT_Ilj69=`kAN)}kSvUhI zezlN!QA2J}v$5=T%Pe1d$5!cIfcY{^?&I^?Djop+Biu+1w?h0E%)Ara4*SJYus5>z zGQ#Y2a)NX`*r^_5J4Zyv{i56qEPCckxZae>Ag!8w$bzzfc-AQ(Cs&Z)b90GSD zQJJh{x=GuIoNkoa>W+-}F151L`h2ISUo;TW7ctAMbd&aFCbfe{$s#tcQ_KedqSJ=6 zYmW6s=)^x?BflJ%CVsrkyj+nWb?#!@eB0YpAh6c|Bomwgh3hpbAfQ3Bgea z2bG9+#&RVr7)O0}u)I7y_~Kug)&-mM8ivB8s!$n@4JwK)C?}I{`Z4e(|J3bjmg$bx zu&au7yMRW(P)FOAjVc8%)6qh=>0Ac})6z^JQds4#m|m!|;@*(0I{xkG-j*tY!FKOb zoSmoc%g=u{G)PB5n*l=s0Q>`K{~Ln~{|5%&rUn4O{!bmrzc9F=mA<1RjiR}e@&6te z?Ej$be}qMlMf{h=XG>$tVL=R~n?%_?lu@xdVR9V;1W0g|4jFH;maZ>Yzap8a!a~wo zGlr^61EGKHASA;ZEZgv^`N(F}^YRo|oLW8RFe$do;9^qK7Jh2FM&9p1T}mQGTD)n%{belE96;#51%U6eg-3>ue;W&y!U+R;>trru z%GRbMWE`hT^%3y>-g|LT9xr?Iii?|ozfXGD>$h>SpK`Dte&=X@u7H2${C-a>Aj5MI zEd?GX!qD6&uahRk_>;8-j)g>nTvxnpXIry*o(@n1V+<`Ogu6L21y^kCUuV4gttG3; zh~>%ul%;+;D5X;pg!+lp9_7I2z_x3v_jR-TmBnd?d9`hOJ{nAf19jnMZMZQzK@-{Mw)HW98wK3+m4-b>Wz@y9_KCK`#Vb@Rxnju5Vv?unVOPgL1 zZ^|G9ZXR?_vE7{(Qg-Kv?Osm{6ExhQ-;~sqF@GAFv!N$6NqYqWJ}e`qJ1JpXq<&EK zwJ|ZO@N=V3(5cv19Azq1Yt)!r%>Zv9pd)lvM{fbQL?@c1gl)TQkbxq_sTltuiF$I} z<kNeH@F*}bRMdcMzq!pB2M-;|f2ziEgT-(zfc(j%?Ed#6i^v(~Ba%E1C%DqTN zJH|I6C+D13kBr$(M_m10+cc~_rRjar#5l%g7x9w#_M=P300htHsA8sbOQv^^ymlyT z5Eq?ns{uZvKHtu9QUHwNac?wR6!x`n>gfIy+Kf9UQsIJW=AFN-`Wbt1tRl&_s>>vK z+w2ebjggCk@7UNj`qSO#90O<{agj#$El*l8?)jsonifLK2OMzdpPgyGE3V(-l&G~v zXB>vy9Z`WIh+M2=MK-cAhbr{TyOR65N=YE7pPjC0Bq1ZFapMZfO|NM1|PNg5apPNjfkAFKN1vf z5&(%*d#3gnab!$8(cnbAq^eo-&3lz@W{eMLfknpBmbcdZ8X1YUViVg2)IUoL``geQ z(+4{w^wc+WeE>ON_S4cLIZ@O#4yqX%F>1Xrq<_hZrxuD}Y1f0|{t|&LCW-GrIV{N- zD1MzP2|Hnu*)J(3H@z1YgP4hSSvtHP1Tm4Y!|5%dC7m7$oIuIx`DK17yl>9VjXIgu zN2f<7eQ-1^LnGs}@`jmd?kOYMJK%WSx3`eD9?lqno+|MR?cf1IZgv#&a?D zXe8fdwJTlk2{B%L3Ubee(RSpd@JE^=$O=K%*IXvvW^wxL3^?l90mzt z1=m3tz)zZ<@JGt-wu;H?%#)$q;%U*vBuIgbw&^jsCY)DdG)!b@L!!zw)ja>AO<*E> zcCBJX6{kfR#Q6x0SMO={Q>+L5M&gU(Mt5&caqqp<{W4pu4F)R?dC73=L4D6FXPCZt z=g~6ow*$t?PCY!TU}X zY~DgO-X{SI;MwzO@$|k#;7*qf$1#`-UD41A_deDe!BOjjA!$>x^PBImV)$ydJ$k?_?`Zu14*d z!i0S|ByVbP?6Ha@>lmP$XwuczaA`IQ7R1GGFxVi?tuChHT58BzvY31f-j{2belX?h z2neScBhKu%P)_DwB4D3ss%Wp(w2SR(_`-zY-`WT=#f6I#Jo~+(6+N+$)^4 zJ4>RaW##$9W6U>dw%APR-uL!TYiSdgAR9C;XX1@ov>CQyd8V^{l&%!*{<(KbrXEgg z(|EoP)Hho3J)md0Hhm+17|eniU~sZD8kj7^@(t3xm`V$V_UUGaj`RC4k-J4-38O|S z6dpNgwz?EFfYnB^-wx9N#XVRezXXCqJ7ZB^yY1M+02j}y_zsvNl*DF@q+TW{|AK4D zQ0Qp2HajM86u$3Mj~M5g7}XR?_rv0#){c-+#E1nR$R<|Ka#>c0s>WiNRlT=L0(GZe2%)owrxOm&hr;!kUq#XdNv`Sz$XV9+K0`kx6I8I zNY`d!)1O0vHQf61rD6S;;xxMx^$jLkk9sX_Qzy}dDw@Na%hm8Y#7SR>ua~mULxBrF3q?!MM z#oImqrK1_rxOUiJLHNE(>jchsi&3KdY5rwhh%a!PSF>h3US;1fzZP*|M}iQqB~QS| zql`HBIiY-n=kTuC&<6M*luuqbNOu{|^V=FScpe$L3Xp}JH-FO$HC0b=N0YmN3DxUa~$ExYUe;_$lG~see{$!fTjC--kwW@FROnpxOMH8J6*D;CyJ4+?l zDklsG@2+7|*TX!aebk6#nC|at6UT2$M*4<&4l+nG{n^vFQ5FP0IyAF^Cv1gudU_KY z%*Zg$kE}RIWNctf#ACFHmyNd3)|5fO6~;!USfUYMTc)K?X7v@o*Xw2d0+be57aMy- zhG@t?rd~h4T52!J>HtL{9 zAmUsoh#kRaT0|mgIS@3Qj5s9WUmej*nAQb;r)jVx<58^lJGkRyrWDiwbX=D%`Wub_ z$&8xgE%L7EVoQX){_!92w6A{bY$vr)PMbb#0#4QOyLfSp6oHOo4NZMWi)uvbVL)^3 zrNF+S#lEPJX?JQ4y;5I1{v%5a)Z6LMi&F+*Qi#t>*qWe?UZ;J%m8Ij#FTJN$+}y}3a;dV*ONZ*!gw?c#J>X@qH0 zQm<$7kZHr6e|Ja(;nC=v;E3^_+kr4n+0F_kl%qd_5=nJgq}VB zAfCko4B$9GGw_>a&L9405wMCg3y0TB?}=IMpMgW|tstc7n`Zz~`~{@N9!xQ<72PxT z92^f^$(}B&!lInr#pX)6}|)q^vew|VSq8utwr+Y=1BdZ$meLr2sFGS9#y=fjU%_j{ccc%cX+_K*VGe07x}HL)9F) zKLc6KLfUd^Fq}IAjongtiSv;NC+W2=>Q}BCU!roUqWe!2iA&x+4c%@V?aB%dBO_5_ zc5ez~e(4i*<{Z$Z{54#9{Rr6F5L{oOEIiC7$aMvyvJV_U!JiBu@+mU8jTAc^QG)IO zKzPB-2!e?13deh-nZ_s2c-gJzt}7?Bzpy}-zre+Ei7wz|5yKGt!~r)F@8N}@TQCg1 zK8t-_u72{F!~ws=BLT$+_c=>vPv2QWl2qMR3$`@)_`>>90Qd2SvBECaz0A)JIe?(C zlX`X4Kr>kvX!X=jK&P@Yah_z&9r*0SJblp4(41F!J^eMb(F)AECij37eexXP{v0i$ zB?6eRi{Wdb2zo_&f4+pnXKr+iszVl#Iu6b4k%y$uUS#;}He_Hn7)(F~=r4)Kj}J~# zkD>F4DJp^^1q?=_`EhCX>!(0?>_4EZ{>6iYoB6G_#TH0yxR?7VD5Yd%P%O&pj7GWb z?`(TKo&5?)%jWPDNWFuZbV>WF|Mv&6D+4_azGcvBcE!FmOC)6wFYyV zek#mjp=nTdwmtbQJ>D$>P+4XYSA+n-HF(Zfcl~}G)!VC&ZVrlcLk`$g|8S1013Vkc z<@a&wo1r{JaWc;AG1M^DE|RXa5tVB$>M+np(bmVMw4$QPh;*_{r4{Kwn8)Xsef-y# z5a1-~)mbwJxB@(g8&iSp73*k3-&`TcssDQO0xng*#S(nQ(pzuO$j6{cam)7k)=DMqo`91U_T{NbO`yN_9 zZYGhzEvy6%2Qs=ER5#U2M&^&OH}mb@!GSLAJMi+agaZk_COE;Hboh3R=6b*2COtQP z2A@lmp+;;qU3=|JN#y5+X)=hE89m9W`~|%sC6i0MDwv)g0poFVsWptaP%^Rx;ayO? zxka>%$8OC*QpJ{gP)(}CyP2J8FDg}|LHf~Gt3IW{HPkFFm=-*a>G1Zg0KiF7|JmYs@S?c=AH(RnM2dA4~Ddh$V#@B3d%+ zqLS(Ye7d}QWU@XTT+gHH{vC1=EEA$hcsWY-FH;j^}oOEbmHXB_6M3uDC zvssCPrHLgeiUYG&TC|u12Q;Gby%pe2&9P&yQb;#UZN^w;;9TV+Hycq}gda^Az-d3+ z%+U7uUEk(qO&>0Hp>yE%NLNMXEiCm0dACimw4P%@%C)Qolp$rb!Tz<9Wu>JfcC}^z#KCDWxSCx!FD1a z)@e0CY%aJq!1zcl zI@^&_W$*aid`csPMWRQ1kI@`594T0$JO}3^pU{nM4d)XQC$~O)lB|jt2`I!Zhu5h6 zr1pKYfRsNRSr&%pbP!{c+)?G4ySxjmWxhOkA|FdRRUc8wEOTt0X*R*-Nim!nM*kpT zSfOaJHcUKA`OK;Z96>~%R<5&e51ddfCw&^eCt2hF**2={vQKpMz9JA29~PwHQbGXj zzUlVVSil?iJ1*9A z_WBus8#>`fO}j#2Hcap*LQlboXm?X$2sx3%Nu&z9-J^PQ_I-D?IRnyth>*hp9t-x~ zkI^PtqtkgSLbIIMd5viJs+Ak3%la(l+-I{Orz^9>9kB(NH$umrS-KzChyu2;?`Up5 zTGNLGA9l=y)kg4)>zE?|dB26^t{ry78a8F|`$12hpa`5x+WqH`jh0=Lmkxl8r{}fg z!?lPCA>sm?Uw9U+vWIGWMTwZn2arGsDZBEHU}mX3MUR3|wuu%qtJcDTR@ZJO~_(`ejX)?R7pH!uDBYr-7Y9P2n1TUB&H$~1fc_pn5?Us z4yAa2ORr2N+|n7VwV;`sn7e;4R~nV(5-ZX0H3m-;E?Eag4LW29jOq^Pj(h>+vt7%_ z>k0Ux59F?r!d!cc^-gDbTm1DF*EuUO0l=Off*D4}zokGV%k5^<84kQ02{cvxGT(&V>UH1< z#&-arvb~0^4!THD>&9EbFaCwHDW*2oX~wdOMJfHQkFCTauvSX~$*wbc` zThgSlFtx~vckav~?K9wey7DMFhB}oBSDT3?Fe-pQ0dLinWEFX2orLW~aq59@;*;Wt z>uqfa4>J4&aB%LZZ|~@QL;oG#!|E9#`B8X9o!4z-M)xu6Tzs1-ac~qno0? zw{?>*+IhJa5E&BzF38_d;$8p}ZrnmTZLi9+IQeAOXr7ojvr6m8o1esUa2`J#&Z5S8 zta;bj=jFDe?N}#KPQE+kl9T*(YkM;O_)^#$aHPi;3V#J6y1TpmSmZ>&;F0^wnQk)V zb||GM--s=()I8IG8Gma^Gqizn0JXd@m>ZlokQqSxxkZ4Qj#W4QbE<F ziB#0(j#%f2i5q}CtwW0=i~2LLJM)U^Px~(!{tH}VHxI{`m(L6tX0Hi^5i*YU(59;G zpT)ClbP09-7k0v7kn-7{pdEor0{6{0Diw~cV$km8!|9Gi;rN&yrnar_BQk_y*}1)D z_c_t0Xo;+X@(Br!og>_;S-@8BCWoElm~t>Wo5Efc(Z*S#5?e#kIF^Rv zMc1E{7|AZ4NrOguOLd_J`U9NvZj& zmZvNjb=)*Orqxyiw380m4&o-K>6Ld$fWHf5Ub#KTpxT$op+1gs+;hn@0zoRZ*XtZe zC)Y))HCv5)j;4i?EjNc+O)J&^mQWvEw7lirWq|-N-tEwyiXijSkJYush!@^NZ|wk$ zG4I$0B3ihyX0oipUN=28K3;?Pj-_G}9m4P1IFsuMjd}7qSs0y-YQ0}ovD=|~+7j*T3nmB=Nrj;hc;eog8VEZOol$g>0?$|LHGrw)@G0 z`g%yI+=4+rb|DjhHcqc2d8I}qYq zs`ec2563nl*&KB=T%xX?1e{OdD$B13CKn%PxK6F_VFu^M8|$p}m}oL8+>B=WzQt!N zhJL827P1w$3pdiIE|aw3aC{fuJkJp(AYv_K&n|WLw;y9Xb}p-vdBT?9)&Rzk=}nbe zSAWR9*BcVs8a5Fc?Gut6iw_wHUWf%?q6qv=l)A)CM<~>db|PcICVXu;w}(Ismn?8F zR@M^%HB0bEWPOm(Lh+WI3rT87?lM6lSAbmfn~`Fo;XXUFYAYbCA~BMo)978*ei~a{ z^VOot%jT_$+=MUv-%}iOr418hc9Ul~-gdPTA?UoAD#N;s=)+aZXS?9jd3vG0D2n(! zPkQr3WUW`3RM@W7;M0%@2wy=hxnw81aOB=iVGUH>(L?Gc;avooxeuUat>ZCNy5BDJ z6sQoE6G@4Xz3x!ShY7-L(;_g*JZ-He>YD7`(ggt9;|^(ZozXyb3h)cd)H}V714JAP zVbMRS7a=tZ82e>aD8Ht*C2=po2Fx8 znCKbpan*AFEaf~xxg+md1@h9nLeh=5%yt#A=*X6Bs3zwxxSZfIVFt4X8*C_s&w^0V zlPsk{H8GeP(va0%b+bVVNC5YAWZl{2Q$@0geqE$#S&G5)0v%mH%E#0(G|to~Yeuv6 z@MS^lAFuQGKT6o(Pu+EOFU}~s#r%!jW}5SakKcbnrwg1loKoqS`ztFeeX!)=5S%Vf zb*dc7lRcPVttkpCAP3VdZup8$n;gO8D-tjpu{+4bk#7@{(lOHuPP7d~os!3%#n`A2 z8#3QxTr?wuV-pQ9qgJW|oLERbbRWP6|LFAr?a^|}{e;&y)Xn1ddl^nA z@@8NM@cmsunGoP;ZKuW(*ufdq4kdD&M{cm`4CGOoZ@U(J^brQny2}3sKd|bBXS-H8 z!GBXV+G{}80I4mAWw7W+VZsfLBHoq^XJyHE&d+)ZM()&(%KQ+hUT>^S?%0}f>KQ~0 zjl!;vYl+g=b1qZzD9;L%(rmZ65DqDT$Y`3g0l%-Z#UiQN8ydH@>v14Lm`W?-tv#}e z_x&C$p4csLOwq*{{w&k;)E@NccZm{YT-S*6=@S`{nI6qCEX$+-O|$C()n&4J?G%@yng3>u5WU|-22bE$Cz`> zH5Y5#^B$@>H-=`4mTg!@TdfH-ZzDGs1%OFa=bMI((O|4fz4f}$9u#LE7H0)KG;FQ4 z`$tu01$f!~RHXv_Y^`#h1_8Kzxb2L=)jC@iQ%7~g8STAAg!u9VH~g3y&pkXKO@uZw z_>qu?eDIT=Ztq|WKo?zJC)Q~d6$QXcpR7Jcd0B(1SWIW^%C4mBAWvP#p#1_1Y+H8x zcocmuAW}sfLtGix$*qBUJj6HvBVCY_t)6wW__ALEr6^Gg&L<&Bs_yf6pXF#){a!O0 zY3?eNk#C&IJK|CyW4R@=XN4VwVIqf|McYtU^z+Rc)XkVI6ei}@ z^P%5-#FjU0y~LtIq<{{`tH98TISVgGr*D;3FgU4UrJWnjcCR{_()virN9uOJ9WHmB z-}SUPb{{dGX2pAqmiul`ALn-Xgm=GC;9@IOfg5C9fS0B?pQvHAx!w>0huzlx*bwsB zlvhyqwvNK{@y#VY&&a`{DR=Zo8)=AVWf5N!EOQY1)dt>-vFy&+LmQOkL-BW>V`W4b zor`KnzMB`)mATHPG@(>ONhj$&@Ob6Mi(l!v>h{4Q^DxEla&yK68zYUrfaS&9G3*pm zZ1a&VE4&9=d9KUlkr{lZ*om~D-qVp(SK=a`*p>@YDEhH=p0Fq z?vyb*+zg@bw58bRM`h9Y)>16$=@hPActO^SSWF+nt}U}no`hiQG<^+QX*&wT}ta4(p)I?a90mve#nG=UJ(=E@`Wxr0hQE(TDm2KBOuC6bR&pam3@FmzG z$3h`+02-nD*=5X9IYOQI>t+z9Eb%eXFw`@tjl zF=ph2!v=d;Y{njT1a!LYLva2oFBjpCqKV&G0bCTo2QiI&j6M@xVm&@HC4JckUiTDi zLedLbq$jANm7dHfTEg1@-J`a73k{oh38=xT%4zF^G08BE&BX_wb62rB79@cfo>|*I zi87_R^BP_G8=QEs--_RCiVdS~WJgEb$Uv5!&&;Be4=08;Oj!*GY8FlIymNe#AP^G; z*XiMe5WjL@AyNYtlfwG3KG0!~d8dBB>?)FBGFZ~~P7%WhiUNZB-N|W1%n$ zM4^z<*Yc~EjtJFy;=S&RfiwlcTw|L~4|D<@|WDNlT3{V6+*c$1BqL|UpRNvaf*wV)2&!@~%5FZJwB-|`4;+N>` z(9*P@6?61BE8VbD%ukoG<7^BzaM4Z|{Y!D9XXg^t5g{ za=s+NDZf$SQFK4ylTQtZlFHcNdcUDIU7klLy(Dvr+F=_ab?butIl@8Hjx@ZzvbeU6} zot~l<(#c6W<_j^9o{~4X4N%zM>?IRe-Zo*DOB&~Z#GrfEW|7SVqRDAsMBX)9sM+u* z@3)%G>uBpVy1aq@XkT^_y(X)K?YTVcw~1wsNf{HA-7TqFE1g6n zu}$lTgK4d_BldlsO|a~}l22o9a1`Uj=v}p+O=Z0`vq^6qq`W&5-GVitNoXAze1Sw8 z;b#nk``8oG7D4uCrDHTC&ZcyM$|X^}RZzjE4$fIac3Gf=Og>>f0S^5P@rtkMIroX4 za{bo~6aMB9SpB)f8a(P3VaT}(2{6lm?nGHD$))7r%o&90ZrOGo7)*r$)2Fl}7^O$9 zZK}MAk;esjMmD-FwjXh8QkH}UQ||mgk0~Bm;IL#Ueb6+Rv;!1ovC6yvOOMai7zeJ< z0vgf1URQk$X!GXj&j1|4)Yk>TJs(Oj2}Rh_L@+Q$6(kb<<`0YzSc8&!eTDl|D+e2X z^SKQ>$2$>V!H_}FFdql-287=UyJZQUThFV(+4u!SOCb(9old-_M=F4phl6Pr_gX2J z>jSJhGC{IA32Ruly!%wK=9{F669nYH0!Vo{#JSxh6r;;^#w>BOp;OW*JL?h+b8Uzb zI`F-4`!l1&ZE5)O;BLl zFWH(K7>pxn0MqL|%it5{2Fq9%twI1$paOLo7?bia-Xp-MVN$K2%2uxzCPQg_WK>ul z)u7tP2seI1Rb@)Qut6Jh&gF*d6V|P+N+m$N=l@=Zrnd07N)jp}E=~JUQ~qS~vHRn4 zO=@sL?;c;hwkC`X%huZgs(K`3Tws3~&jtzRG#>9QRpmqkP^xz#)JE6J@M~flGSe&e z>f5iyTCVY!*4mou*yU<7fx}?imh%CJ_`Mv}7eluWxHk+U++>Ub0ir}63n12f~BEH zy=FD@+@?IlS`fA%+8>&hIMtmwzE1(Z;)0(GcMsc>x8fN;iPR=Qk@hpa9UeK)S=+IWi^{T#x>%Dx?mZ}>KXKi zMq|*6-f#-!>wQyC%|A(={De~J*XEJF2R3Hwvf69zAa;i;o*K+tyO047f=$1K&>Z7@C@sV=SxB48XFQV6&Aynw4Lv&*Jo#o(y6$L{kp= zfZ2|$vC~#y97%=j;#P%^0~Led5UGmB-UFi?`(49@zFmJFwMoYPJ56%)cU&gp|) z2BEl0SA`hbA&&_tngPS}Bn(BeX2)Pa=<8S9F)s$}7Kft;HE9v`MS!%649_ODt3=%4 z3%NyK7UPUgk}Rcu!3_BN4LPxsw7S4#zGMPnH?hirbl43s!2LZo-f72zZ8COWwe6aX zu@U=fp5`KoEj70=DK3`YWh0=&P;oi>3t>8`?g5!-z|6?ggFGfT%$=o%kW~3$M0%;F zU~($2Wp|ui-}yG$d8)B*+|_$Q@^IWK3-EahV9@Q1V9`XBF`kT#nBBH(cK%q2=dilB zQK@;@VUkGCf|QB&90a`V+60c@krWfD=I&>2?8KYc8Vl9vxrDwCxS@blPXY|NQTkv@ z&+tZs_+^GbHAfS$jo$$l^(Q&`AtSz0;=<1RX$0qe==eKMZK;qyRb&;Z6}SWMy3ytBIstvw2kx}Ke7xTJ_8dawdQlkN(|r$K z`3g{sNYR?T;3^fGf7$64f7lP->*p0)IwDDiairkg*%z%#5Rl(ZTNEZ4Me+I!?agl(yP#4i@IN$)3sydO2N)TcDZ9eXIgRApVOxz?dF<_b4~ zK{Oi?hrbor&YGFUJMWy0+jZ~XDzrf|W~aWG2dh>*o9TZQ6v&s(NDjm24k2fkEJCgD zg4=&p@9h%5%e5YJtyl(t8*FIiwctebKvF+JB%8BT3zd}#uqm@e4Rax#YHw)JdtuF@ z2SaYA#&oc#hC*!zMlvyPMLSLnqgJ93_x&*`Y9CX2hXH|c?Wz#eQ`&}}V7IGs8CAGY zgTzz2(PJ+9FClm-ICw-s_{piD56uBu|jyIR?hH(b5b4D=6z(k zE0-o#U+HAgrCILkdy(xm-8xZ(&;(bA@&8rl(Yf<~5Lm>}=yV^_2) z#~NJf;nuISMvh6r3ifgy&!o+(zRDu@c8=)IMRpI;i;WbYC6=B>Ag3}wnQVVr820TC zg>0iQmgZ^qn5$ym?$R+sZH78T36?dg4Sk1}wQc!M^r~0})n7>F8Y$%>=3@w6(NP$~;5dN(glGvq zO`iG42JO>+MLeUiU(LQy`n>WTM?OO@iy$9VPWo0lLNUfc8Fl>5PKCc0N*$eYYvyGh zlwN$l0QF$9_Y6D%y?yePg-#f>4L4z{0}3URP{XQA#5hapRTO@%I+vjo4?Sw3|0A?^ zR$@t|7qbCpmUb6z>!YG=bd4{NcDx~oIj24bn}>kq^sux*m4ow%ecrZl7P+NP1Z?Wd zCLPhAH}}85F`u*lSe6$RD_l^#UYtJ5emNHHUXID;R01kFr-dLpLQw5Wc~y{nHMKsJ zy%XT6P6k6IG4{UTWeWL4ahzxF5@TPa#WoxE*sNek#kQnOr@8y&tn@f6xm}t%oe#`g zVT?^6riofhlR=buzT43|S{i1uskd)Lfl-I6Hac_jK8{<nfl%8EDiYNIkr&UrZJPCL;V4=F6dOHqTi+MKw)2+#uGYX$5aO!;qmzpf8#!^yyX zmINU?ihvgp!RSKvKBy1kaW40E3nEk%mN5~VOg>U?Sq&_LPN?kLacvb>H9K5!ue|!l zo}coHk8-xfG^nfs5g$ud&G{n26c49w6Rt2}Eg&em<4e5kAVXxbgIdAx#_Q-pY7L<( z#vhNE_6;pG-){z#fOBLSezn)*5qaK9iJ+6Br7PFc&{>(MOyBKVyvTc(F;l@3w8Bh> z3>GXE2{bNNqI1NeOR2JVz4a&+6)cCWmK#VF-+#QnZ9dxD~0^ zf|8AB{pJuavu|}@sO75`j={2n>ly|dY47^+Omtcvk<5R^;qXFK{6IdShuUR~6GJSx zS(LbwIONfb2;~!v{=8Czlm+PklVmGp3K1hoZ{4!eWCmFHAr&~H7q9}SE#K!e#ojYt zcvXUi%!=p)(^8MTmg7;XzX(|N)--f53ap$y;jcN`UCTU;aep6Ss0_c4w&Mahkhvzl z!@xP21uNFL@=`&c@EL&^xIT-0;S>4_THn8W@XjU^BO1#=hSt#emo{G-&CfII2bCg{*=3`*K<&8E1OFLo=N+qicI^`q8BGo~`wTKsoILWb6_Ltnr2i`OU(qERXuftyoqd=8$m7Myol7^z1c zuP_8|0Er~^thU!tr$x|bb&KP*b7oMS|y{O2#jO?e2 zEp5q~EDMs)w#WNgCj7QUw^N9;@Qg8}+4bZm;{+TgW(}IUR0_{zrsrS=D6w$D<-VYU z{#4t=ZwZ8JTvH7^opV;LwB!fcR_K^ev}pC>;j>}K>aH_8CbVSgGwS(&Sz5;fzc%UI zFT#bWdlih!Rjt=iEU);k)JOc4WB7Q#q3?V-F{*S@hIP^jjyfRdg#|E(%D;5c7JGC- zdYiSul9-e*)qPRF9^OPy6dj_n%)yb~aSgs!o#yykB=9ks;yAT+*UF^=T%}*NNKBw3 zaZui-69NY%yS4d@#BLv6|FPR0!*UAcNc7QrEB==^htFte^@jFK7=bTG7u-BO30VO| z&}H6>Pu`EHsiZSK7vlkqag~O%9xcE}>EEXE-|oioBhO^+tb3Fn-C*IJ5em=aTXpeF z5{9aV<_Ug5Q?IFC2I%Sp&k=*0nzm_ksAv)oDoh9&Sa;fz0Eo9ul zuD-kdf?M3-W67kztYOo~)NduG672%RvZPWildOZa9jd%bFTyx7-<)IauK2>uQ^;SC zEI)m&c^)(@3&nvyE#BKOr(U0rRac}RF^(LVxFV-?q^yi8jQBhu%JURV9-MaS87}hG_Nv0VeL}A55=xnID{^uWce3X{IJTKMx+UK|xq#Y|>9I?mj4+qEQqod{& za+ZSbNKReqc?J$k5`3J3zFSA}DvV8+mFhy$d~8DYu>@Z1X1O;Ym6}=-CMt^uG4UW~ zm=GIw8Lsa=9`2By6HIXrLMFzj_K@Xda0iiIUU%(chE@9ud#$IzPtX@u7&l_V!|>RU z;PjB6sY0FBxx9yKyv1Xa+MPT>lkI|@>D7rU#K|$=5-~Xoo);AImnov7=GflTrVz+A zH5omX1AG*^t74xw3^0nWIzG@8z&=;})Z&&Ob7>(DY1Upxv9jC2N^uJ*4bX*TIM{Ex z#HhMyYo7H7mxtIuE%OwBZO8K8Ya{Ajv^>Z)PFX z`w7TroX(R3zK}@TV*XG4;yv5;JSyJXN+FKaL+Hbmnbtps3aS5q^UNun73y7Fz#4vv zDV?>!$8@QseX?g?$cNp|HBKnqzzhP!^lN=crocHKUHpscQ{qSVrj3EozO2G z=hL!)z&1t0iPj_IK1SgSHHi1g;F_I;e7&8n+6Pb)498lvJ&J^2?cZleGk+cPh{QmR zh?BM}TifoyYqKP1DI?Rl3`>!HFwUO;asJD~y*~5y0PW(Fvi)BZH{ekPj!$w`<#aM;$B! z@3{B1^;EHwO&Brrr*h=GFEIuXQq9Nf(`-R+c_AZbf~s* z9T;xDuAE45SRsNF6FIN?nJvQF8~0{Q`3*6R!!n_OlOiC#t$J2E~zWn#Pb^WxLoY8O{kR7e_QPF zBzOJts-``gpx7P--tqHjga1-6|9hAE&r(FryRI~N1AH!k%7997ID&~aS))OMaHE+9 zo*Bd^@9GIVFI+Z`!;)&d1GL6(?S^fWrzqlJ*TSS7UY}R-8mtB3ed}2a90YO0>4^rj zFN%fS6suAL#*gSbBJxcbl()nvv?a82?_Qc%6R~HBuqmapxgDBwet{I_%`cT7mDhJ3C7AOJrp(7Gq!(!R|;4OB_HU{6xlUj;KADfwsvbfHokj z0LdInI=N`YcH|H$^;mdg@-rlI1M1LPC!F-iuo4&iT&)oKLdp6Y{Y0l+=|o=Dbk_W| ziggv)p?T#=Lz_M7%MiSnC@;_g{hWNY>X6`yl(*Lo2fxbR0=C+aaB zeF|Mom+`#kcaEL66w5AXs6*1vAPF~2u<(PEr#6QvN@2Bxw9Q{#I>ZQgGVfo8`F~Ei zfqXgZ`}Q!8o+bRGNChy2Pa4webbh+z%xPQg|7GOX)J)Ur2_3MkLkNA#!|<*mLm#_L z=WOh-o|Ldk2;#EMno@Q>qvld?E8+esIfHM@8N(0*#Qs^1r&|P5|$EMq`c{l|ld#1&HbUy4WmKsJ@ zKbNTuP|zpCa_K>ByRqZ!@b|A?C=KZj21Ep^Cx(17=SS+cy+WGCf2%<;e<3=UvmJDE zewcosdG$Ug)S!;Yf31P zWnqa!#$1jkn|C`C>+3$o)^t_|U(hteN;drq-gwBNr zu1(*!+M@cCq%5t1hmd4G?Scwm(U44RkE)S<`(;~TkTu3f8IpNGW8PDMy_tL9Sui9R z#_)6L00RWN6<&f@o$U&oIR5DO`YDRinYk<0&H22d z^|g20>zUaICq?QZEYrf;)Cx=FmWYK9aRp~;El|!CMHXh1m4zZ49C@03kzics7W~`r zZge$u_8P&lbr6EoCk7mAkOQwe001=5+WDhZd&7bRDHKJ;1cjC56eVQEb>*c6W&dy?;RgU) z*l%6X1OZTNWyQmTB(nEO006k3049wA0A*1nWzfa{xNa)wy7$RqvEN^Jx>-icnOi-q z8>C%F2x3RxS6TU^ReST;007@<6hLDuTg(6Cr7iVcY#cxw$?rYAv60zdJjkZP|Mhs_ z!}%BkP5HsOP0t({08j{GNizHd>g@KvgW5Y77=o-Yv9U8Vwx_o=d%(dQ%KwfHCHYIe zmxtJ2K-d_nPs3J0*ufxd>YuQOJ${RAt8emm*ohyo|8OGmA?ylBg(QEPPa_>P&N-X_ z0NGEl^xnV1+Jods81EUV^xsGN&s#6TkD#Xt{cXfVf)GQsCSOQ`9GwNNJMNzl1;YLT z5hOoIZ)o!$y6g9j{=s0|8(SJ10&VR6w8QR)AmM%gH?qt_&*Ses4^V=kf&c(8LD9?p zlO3oL|5rPJcE*3nw0Qln;lBTk4JIuzT7*bZVeB9qh(I>nGll+W)!xlf|H_8@NT)Y+ zum{>$(c2r^nc4mo_w7YL_`kn5BMdYY7<6}(pw$ZS2L0Zv+5ge1y3A(xHS7;%9Yim$ZsqfuC07LvcU~JH6zZB3cTe**Ty1xUn*8U3oonZb8&-_o1 zBYr3n-S@v4Y;ustICG=)vLLz~DLw$e@sr1qAAbcFF*Cfcc&>kcOJ#icUR3~hS)Pc{PGV*p!_i6Tkvm3Vy#{V zbY7(nvH&y%Nz{K2yUj1Z8X^5-Yx~jv1GGZc_k|uS{@YDP9OS=CkLlNEph*xT{;vD_ z*Fr~H`W0H%*vZ}!RA&F=KK%pyUE23y?|t`M z`1@j}4~xnDe19c_0Q==e^G8L~|FX2{!+q|)UfshYCO;xCB*%c~A~DM(P=O3*VU=%*WWzAVcM4D{`P05}U~SO5S3 literal 0 HcmV?d00001 diff --git a/updates/0.20/ver_0.266_files.txt b/updates/0.20/ver_0.266_files.txt new file mode 100644 index 0000000..c6332c2 --- /dev/null +++ b/updates/0.20/ver_0.266_files.txt @@ -0,0 +1,4 @@ +F: ../admin/templates/shop-coupon/coupon-edit.php +F: ../admin/templates/shop-coupon/view-list.php +F: ../autoload/admin/controls/class.ShopCoupon.php +F: ../autoload/admin/factory/class.ShopCoupon.php diff --git a/updates/changelog.php b/updates/changelog.php index 8d248a0..e2098d7 100644 --- a/updates/changelog.php +++ b/updates/changelog.php @@ -1,3 +1,14 @@ +ver. 0.266 - 13.02.2026
    +- NEW - migracja modulu `ShopCoupon` do architektury Domain + DI (`Domain\Coupon\CouponRepository`, `admin\Controllers\ShopCouponController`) +- UPDATE - modul `/admin/shop_coupon/*` przepiety z legacy `grid/gridEdit` na `components/table-list` i `components/form-edit` +- UPDATE - nowe widoki i partiale: `shop-coupon/coupons-list`, `shop-coupon/coupon-edit-new`, `shop-coupon/coupon-categories-selector`, `shop-coupon/coupon-categories-tree`, `shop-coupon/coupon-edit-custom-script` +- UPDATE - zachowana kompatybilnosc aliasow legacy akcji (`view_list`, `coupon_edit`, `coupon_save`, `coupon_delete`) w nowym kontrolerze +- CLEANUP - usuniete legacy klasy/pliki: `autoload/admin/controls/class.ShopCoupon.php`, `autoload/admin/factory/class.ShopCoupon.php`, `admin/templates/shop-coupon/view-list.php`, `admin/templates/shop-coupon/coupon-edit.php` +- UPDATE - menu admin wskazuje kanoniczny URL `/admin/shop_coupon/list/` +- FIX - ujednolicone drzewka (strzalki + focus) i wyglad checkboxow miedzy `/admin/shop_coupon/edit/*` oraz `/admin/layouts/edit/*` +- UPDATE - testy: `OK (235 tests, 682 assertions)` + nowe pliki testowe `CouponRepositoryTest`, `ShopCouponControllerTest` +- UPDATE - pliki aktualizacji: `updates/0.20/ver_0.266.zip`, `ver_0.266_files.txt` +
    ver. 0.265 - 13.02.2026
    - UPDATE - modul `/admin/shop_promotion/*`: dodano pole `Data od` (`date_from`) w repozytorium, formularzu i liscie - UPDATE - front: `shop\Promotion::get_active_promotions()` uwzglednia `date_from` (okno aktywnosci od-do) @@ -452,6 +463,3 @@ - - - diff --git a/updates/versions.php b/updates/versions.php index bb1f9e4..3221827 100644 --- a/updates/versions.php +++ b/updates/versions.php @@ -1,5 +1,5 @@