From 9233ee8c42042ae39d97a3534b0aa44e52edbe49 Mon Sep 17 00:00:00 2001 From: Johan Cwiklinski Date: Thu, 24 Sep 2026 14:29:28 +0200 Subject: [PATCH 1/4] Align MySQL and PostgreSQL schemas --- scripts/mysql.sql | 53 ++++++++++------- scripts/pgsql.sql | 44 +++++++++----- scripts/upgrade-to-1.1-mysql.sql | 99 ++++++++++++++++++++++++++++++++ scripts/upgrade-to-1.1-pgsql.sql | 52 +++++++++++++++++ 4 files changed, 211 insertions(+), 37 deletions(-) diff --git a/scripts/mysql.sql b/scripts/mysql.sql index c267644..8c8edaf 100644 --- a/scripts/mysql.sql +++ b/scripts/mysql.sql @@ -9,10 +9,10 @@ SET FOREIGN_KEY_CHECKS=0; DROP TABLE IF EXISTS galette_lend_category; CREATE TABLE galette_lend_category ( category_id int(10) unsigned NOT NULL AUTO_INCREMENT, - name varchar(100) COLLATE utf8_general_ci NOT NULL, + name varchar(100) NOT NULL, is_active tinyint(1) NOT NULL, PRIMARY KEY (category_id) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; DROP TABLE IF EXISTS galette_lend_status; CREATE TABLE galette_lend_status ( @@ -22,7 +22,7 @@ CREATE TABLE galette_lend_status ( is_active tinyint(1) NOT NULL, rent_day_number INT NULL DEFAULT NULL, PRIMARY KEY (status_id) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; DROP TABLE IF EXISTS galette_lend_rents; CREATE TABLE galette_lend_rents ( @@ -36,10 +36,13 @@ CREATE TABLE galette_lend_rents ( comments varchar(200) NOT NULL, PRIMARY KEY (rent_id), KEY date_begin (date_begin), - FOREIGN KEY FK_rent_adherent_1 (adherent_id) REFERENCES galette_adherents (id_adh) ON DELETE CASCADE ON UPDATE CASCADE, - FOREIGN KEY FK_rent_status_1 (status_id) REFERENCES galette_lend_status (status_id) ON DELETE NO ACTION ON UPDATE NO ACTION, - FOREIGN KEY FK_rent_object_1 (object_id) REFERENCES galette_lend_objects (object_id) ON DELETE NO ACTION ON UPDATE NO ACTION -) ENGINE=InnoDB DEFAULT CHARSET=utf8; + CONSTRAINT galette_lend_rents_adherent_id_fkey FOREIGN KEY (adherent_id) + REFERENCES galette_adherents (id_adh) ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT galette_lend_rents_status_id_fkey FOREIGN KEY (status_id) + REFERENCES galette_lend_status (status_id) ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT galette_lend_rents_object_id_fkey FOREIGN KEY (object_id) + REFERENCES galette_lend_objects (object_id) ON DELETE RESTRICT ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; DROP TABLE IF EXISTS galette_lend_objects; CREATE TABLE galette_lend_objects ( @@ -57,17 +60,31 @@ CREATE TABLE galette_lend_objects ( nb_available INT NULL, rent_id int(10) unsigned NULL DEFAULT NULL, PRIMARY KEY (object_id), - FOREIGN KEY FK_rent_category_1 (category_id) REFERENCES galette_lend_category (category_id) ON DELETE NO ACTION ON UPDATE NO ACTION, - FOREIGN KEY FK_object_rent_1 (rent_id) REFERENCES galette_lend_rents (rent_id) ON DELETE NO ACTION ON UPDATE NO ACTION -) ENGINE=InnoDB DEFAULT CHARSET=utf8; + CONSTRAINT galette_lend_objects_category_id_fkey FOREIGN KEY (category_id) + REFERENCES galette_lend_category (category_id) ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT galette_lend_objects_rent_id_fkey FOREIGN KEY (rent_id) + REFERENCES galette_lend_rents (rent_id) ON DELETE RESTRICT ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; DROP TABLE IF EXISTS galette_lend_pictures; CREATE TABLE galette_lend_pictures ( - object_id int(11) NOT NULL, + object_id int(10) unsigned NOT NULL, + picture mediumblob NOT NULL, + format varchar(10) NOT NULL DEFAULT '', + PRIMARY KEY (object_id), + CONSTRAINT galette_lend_pictures_object_id_fkey FOREIGN KEY (object_id) + REFERENCES galette_lend_objects (object_id) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; + +DROP TABLE IF EXISTS galette_lend_categories_pictures; +CREATE TABLE galette_lend_categories_pictures ( + category_id int(10) unsigned NOT NULL, picture mediumblob NOT NULL, - format varchar(10) CHARACTER SET utf8 NOT NULL DEFAULT '', - PRIMARY KEY (object_id) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; + format varchar(10) NOT NULL DEFAULT '', + PRIMARY KEY (category_id), + CONSTRAINT galette_lend_categories_pictures_category_id_fkey FOREIGN KEY (category_id) + REFERENCES galette_lend_category (category_id) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; INSERT INTO galette_lend_status (status_text, in_stock, is_active) VALUES('Garage A (exemple)', 1, 1); INSERT INTO galette_lend_status (status_text, in_stock, is_active) VALUES('Maison B (exemple)', 1, 1); @@ -78,12 +95,4 @@ INSERT INTO galette_lend_status (status_text, in_stock, is_active, rent_day_numb INSERT INTO galette_lend_status (status_text, in_stock, is_active) VALUES('Vendu (exemple)', 0, 1); INSERT INTO galette_lend_status (status_text, in_stock, is_active) VALUES('Detruit (exemple)', 0, 1); -DROP TABLE IF EXISTS galette_lend_categories_pictures; -CREATE TABLE IF NOT EXISTS galette_lend_categories_pictures ( - category_id int(11) NOT NULL, - picture mediumblob NOT NULL, - format varchar(10) CHARACTER SET utf8 NOT NULL DEFAULT '', - PRIMARY KEY (category_id) -) ENGINE=InnoDB DEFAULT CHARSET=utf8; - SET FOREIGN_KEY_CHECKS=1; diff --git a/scripts/pgsql.sql b/scripts/pgsql.sql index c6bd68e..9c631bd 100644 --- a/scripts/pgsql.sql +++ b/scripts/pgsql.sql @@ -65,15 +65,20 @@ CREATE TABLE galette_lend_status ( DROP TABLE IF EXISTS galette_lend_rents CASCADE; CREATE TABLE galette_lend_rents ( rent_id integer DEFAULT nextval('galette_lend_rents_id_seq'::text) NOT NULL, - object_id integer, + object_id integer NOT NULL, date_begin timestamp NOT NULL, date_forecast timestamp NULL DEFAULT NULL, date_end timestamp DEFAULT NULL, - status_id integer REFERENCES galette_lend_status (status_id) ON DELETE RESTRICT ON UPDATE CASCADE, - adherent_id integer REFERENCES galette_adherents (id_adh) ON DELETE CASCADE ON UPDATE CASCADE, + status_id integer NOT NULL, + adherent_id integer, comments character varying(200) NOT NULL, - PRIMARY KEY (rent_id) + PRIMARY KEY (rent_id), + CONSTRAINT galette_lend_rents_status_id_fkey FOREIGN KEY (status_id) + REFERENCES galette_lend_status (status_id) ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT galette_lend_rents_adherent_id_fkey FOREIGN KEY (adherent_id) + REFERENCES galette_adherents (id_adh) ON DELETE SET NULL ON UPDATE CASCADE ); +CREATE INDEX galette_lend_rents_date_begin_idx ON galette_lend_rents (date_begin); @@ -83,36 +88,45 @@ CREATE TABLE galette_lend_objects ( name character varying(100) NOT NULL, description character varying(500) NOT NULL, serial_number character varying(30) NOT NULL, - price real NOT NULL, + price numeric(15,3) NOT NULL, price_per_day boolean NOT NULL DEFAULT FALSE, dimension character varying(100) NOT NULL, - weight real NOT NULL, + weight numeric(15,3) NOT NULL, is_active boolean NOT NULL, - category_id integer REFERENCES galette_lend_category (category_id) ON DELETE RESTRICT ON UPDATE CASCADE, - rent_price real NULL, + category_id integer, + rent_price numeric(15,3) NULL, nb_available integer NULL, - rent_id integer REFERENCES galette_lend_rents (rent_id) ON DELETE RESTRICT ON UPDATE CASCADE, - PRIMARY KEY (object_id) + rent_id integer, + PRIMARY KEY (object_id), + CONSTRAINT galette_lend_objects_category_id_fkey FOREIGN KEY (category_id) + REFERENCES galette_lend_category (category_id) ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT galette_lend_objects_rent_id_fkey FOREIGN KEY (rent_id) + REFERENCES galette_lend_rents (rent_id) ON DELETE RESTRICT ON UPDATE CASCADE ); -ALTER TABLE galette_lend_rents ADD CONSTRAINT galette_lend_rents_object_fkey FOREIGN KEY (object_id) REFERENCES galette_lend_objects(object_id); +ALTER TABLE galette_lend_rents ADD CONSTRAINT galette_lend_rents_object_id_fkey FOREIGN KEY (object_id) + REFERENCES galette_lend_objects (object_id) ON DELETE RESTRICT ON UPDATE CASCADE; DROP TABLE IF EXISTS galette_lend_pictures; CREATE TABLE galette_lend_pictures ( - object_id integer DEFAULT '0' NOT NULL, + object_id integer NOT NULL, picture bytea NOT NULL, format character varying(10) DEFAULT '' NOT NULL, - PRIMARY KEY (object_id) + PRIMARY KEY (object_id), + CONSTRAINT galette_lend_pictures_object_id_fkey FOREIGN KEY (object_id) + REFERENCES galette_lend_objects (object_id) ON DELETE CASCADE ON UPDATE CASCADE ); DROP TABLE IF EXISTS galette_lend_categories_pictures; CREATE TABLE galette_lend_categories_pictures ( - category_id integer DEFAULT '0' NOT NULL, + category_id integer NOT NULL, picture bytea NOT NULL, format character varying(10) DEFAULT '' NOT NULL, - PRIMARY KEY (category_id) + PRIMARY KEY (category_id), + CONSTRAINT galette_lend_categories_pictures_category_id_fkey FOREIGN KEY (category_id) + REFERENCES galette_lend_category (category_id) ON DELETE CASCADE ON UPDATE CASCADE ); diff --git a/scripts/upgrade-to-1.1-mysql.sql b/scripts/upgrade-to-1.1-mysql.sql index a368848..3d51533 100644 --- a/scripts/upgrade-to-1.1-mysql.sql +++ b/scripts/upgrade-to-1.1-mysql.sql @@ -22,3 +22,102 @@ WHERE code IN ( ); DROP TABLE galette_lend_parameters; + +-- Align schema with PostgreSQL one: utf8mb4, same foreign keys on both +-- engines, pictures removed with their object or category. +-- Foreign keys names depend on the MySQL version that created them, and +-- MySQL cannot drop them conditionally: tables holding some are rebuilt. +SET FOREIGN_KEY_CHECKS=0; + +ALTER TABLE galette_lend_category CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci; +ALTER TABLE galette_lend_status CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_520_ci; + +CREATE TABLE galette_lend_rents_new ( + rent_id int(10) unsigned NOT NULL AUTO_INCREMENT, + object_id int(10) unsigned NOT NULL, + date_begin datetime NOT NULL, + date_forecast DATETIME NULL DEFAULT NULL, + date_end datetime DEFAULT NULL, + status_id int(10) unsigned NOT NULL, + adherent_id int(10) unsigned DEFAULT NULL, + comments varchar(200) NOT NULL, + PRIMARY KEY (rent_id), + KEY date_begin (date_begin), + CONSTRAINT galette_lend_rents_adherent_id_fkey FOREIGN KEY (adherent_id) + REFERENCES galette_adherents (id_adh) ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT galette_lend_rents_status_id_fkey FOREIGN KEY (status_id) + REFERENCES galette_lend_status (status_id) ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT galette_lend_rents_object_id_fkey FOREIGN KEY (object_id) + REFERENCES galette_lend_objects (object_id) ON DELETE RESTRICT ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; + +INSERT INTO galette_lend_rents_new + (rent_id, object_id, date_begin, date_forecast, date_end, status_id, adherent_id, comments) +SELECT rent_id, object_id, date_begin, date_forecast, date_end, status_id, adherent_id, comments +FROM galette_lend_rents; + +CREATE TABLE galette_lend_objects_new ( + object_id int(10) unsigned NOT NULL AUTO_INCREMENT, + name varchar(100) NOT NULL, + description varchar(500) NOT NULL, + serial_number varchar(30) NOT NULL, + price decimal(15,3) NOT NULL, + price_per_day tinyint(1) NOT NULL DEFAULT FALSE, + dimension varchar(100) NOT NULL, + weight decimal(15,3) NOT NULL, + is_active tinyint(1) NOT NULL, + category_id INT(10) UNSIGNED NULL, + rent_price DECIMAL(15,3) NULL, + nb_available INT NULL, + rent_id int(10) unsigned NULL DEFAULT NULL, + PRIMARY KEY (object_id), + CONSTRAINT galette_lend_objects_category_id_fkey FOREIGN KEY (category_id) + REFERENCES galette_lend_category (category_id) ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT galette_lend_objects_rent_id_fkey FOREIGN KEY (rent_id) + REFERENCES galette_lend_rents (rent_id) ON DELETE RESTRICT ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; + +INSERT INTO galette_lend_objects_new + (object_id, name, description, serial_number, price, price_per_day, dimension, weight, + is_active, category_id, rent_price, nb_available, rent_id) +SELECT object_id, name, description, serial_number, price, price_per_day, dimension, weight, + is_active, category_id, rent_price, nb_available, rent_id +FROM galette_lend_objects; + +-- Pictures had no foreign key: those of removed objects or categories are dropped +CREATE TABLE galette_lend_pictures_new ( + object_id int(10) unsigned NOT NULL, + picture mediumblob NOT NULL, + format varchar(10) NOT NULL DEFAULT '', + PRIMARY KEY (object_id), + CONSTRAINT galette_lend_pictures_object_id_fkey FOREIGN KEY (object_id) + REFERENCES galette_lend_objects (object_id) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; + +INSERT INTO galette_lend_pictures_new (object_id, picture, format) +SELECT p.object_id, p.picture, p.format +FROM galette_lend_pictures p +INNER JOIN galette_lend_objects o ON o.object_id = p.object_id; + +CREATE TABLE galette_lend_categories_pictures_new ( + category_id int(10) unsigned NOT NULL, + picture mediumblob NOT NULL, + format varchar(10) NOT NULL DEFAULT '', + PRIMARY KEY (category_id), + CONSTRAINT galette_lend_categories_pictures_category_id_fkey FOREIGN KEY (category_id) + REFERENCES galette_lend_category (category_id) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_520_ci; + +INSERT INTO galette_lend_categories_pictures_new (category_id, picture, format) +SELECT p.category_id, p.picture, p.format +FROM galette_lend_categories_pictures p +INNER JOIN galette_lend_category c ON c.category_id = p.category_id; + +DROP TABLE galette_lend_rents, galette_lend_objects, galette_lend_pictures, galette_lend_categories_pictures; + +RENAME TABLE galette_lend_rents_new TO galette_lend_rents, + galette_lend_objects_new TO galette_lend_objects, + galette_lend_pictures_new TO galette_lend_pictures, + galette_lend_categories_pictures_new TO galette_lend_categories_pictures; + +SET FOREIGN_KEY_CHECKS=1; diff --git a/scripts/upgrade-to-1.1-pgsql.sql b/scripts/upgrade-to-1.1-pgsql.sql index 62f2ba2..5f872fe 100644 --- a/scripts/upgrade-to-1.1-pgsql.sql +++ b/scripts/upgrade-to-1.1-pgsql.sql @@ -23,3 +23,55 @@ WHERE code IN ( DROP TABLE galette_lend_parameters; DROP SEQUENCE IF EXISTS galette_lend_parameters_id_seq; + +-- Amounts and weight were stored as floating point numbers +ALTER TABLE galette_lend_objects + ALTER COLUMN price TYPE numeric(15,3) USING ROUND(CAST(price AS numeric), 3), + ALTER COLUMN weight TYPE numeric(15,3) USING ROUND(CAST(weight AS numeric), 3), + ALTER COLUMN rent_price TYPE numeric(15,3) USING ROUND(CAST(rent_price AS numeric), 3); + +-- A rent always has an object and a status +UPDATE galette_lend_objects SET rent_id = NULL WHERE rent_id IN ( + SELECT rent_id FROM galette_lend_rents WHERE object_id IS NULL OR status_id IS NULL +); +DELETE FROM galette_lend_rents WHERE object_id IS NULL OR status_id IS NULL; +ALTER TABLE galette_lend_rents + ALTER COLUMN object_id SET NOT NULL, + ALTER COLUMN status_id SET NOT NULL; + +-- Same foreign keys as MySQL +ALTER TABLE galette_lend_rents + DROP CONSTRAINT IF EXISTS galette_lend_rents_object_fkey, + DROP CONSTRAINT IF EXISTS galette_lend_rents_status_id_fkey, + DROP CONSTRAINT IF EXISTS galette_lend_rents_adherent_id_fkey, + ADD CONSTRAINT galette_lend_rents_object_id_fkey FOREIGN KEY (object_id) + REFERENCES galette_lend_objects (object_id) ON DELETE RESTRICT ON UPDATE CASCADE, + ADD CONSTRAINT galette_lend_rents_status_id_fkey FOREIGN KEY (status_id) + REFERENCES galette_lend_status (status_id) ON DELETE RESTRICT ON UPDATE CASCADE, + ADD CONSTRAINT galette_lend_rents_adherent_id_fkey FOREIGN KEY (adherent_id) + REFERENCES galette_adherents (id_adh) ON DELETE SET NULL ON UPDATE CASCADE; + +ALTER TABLE galette_lend_objects + DROP CONSTRAINT IF EXISTS galette_lend_objects_category_id_fkey, + DROP CONSTRAINT IF EXISTS galette_lend_objects_rent_id_fkey, + ADD CONSTRAINT galette_lend_objects_category_id_fkey FOREIGN KEY (category_id) + REFERENCES galette_lend_category (category_id) ON DELETE SET NULL ON UPDATE CASCADE, + ADD CONSTRAINT galette_lend_objects_rent_id_fkey FOREIGN KEY (rent_id) + REFERENCES galette_lend_rents (rent_id) ON DELETE RESTRICT ON UPDATE CASCADE; + +CREATE INDEX IF NOT EXISTS galette_lend_rents_date_begin_idx ON galette_lend_rents (date_begin); + +-- Pictures had no foreign key: those of removed objects or categories are dropped +DELETE FROM galette_lend_pictures + WHERE object_id NOT IN (SELECT object_id FROM galette_lend_objects); +ALTER TABLE galette_lend_pictures + ALTER COLUMN object_id DROP DEFAULT, + ADD CONSTRAINT galette_lend_pictures_object_id_fkey FOREIGN KEY (object_id) + REFERENCES galette_lend_objects (object_id) ON DELETE CASCADE ON UPDATE CASCADE; + +DELETE FROM galette_lend_categories_pictures + WHERE category_id NOT IN (SELECT category_id FROM galette_lend_category); +ALTER TABLE galette_lend_categories_pictures + ALTER COLUMN category_id DROP DEFAULT, + ADD CONSTRAINT galette_lend_categories_pictures_category_id_fkey FOREIGN KEY (category_id) + REFERENCES galette_lend_category (category_id) ON DELETE CASCADE ON UPDATE CASCADE; From 03d3411cf0172c345c6e3cc42acb1e9591370140 Mon Sep 17 00:00:00 2001 From: Johan Cwiklinski Date: Thu, 24 Sep 2026 14:29:28 +0200 Subject: [PATCH 2/4] Give back objects of a member who is removed --- lib/GaletteObjectsLend/LendService.php | 68 ++++++++++++++++++ .../PluginEventProvider.php | 54 +++++++++++++++ .../tests/units/LendService.php | 69 +++++++++++++++++++ 3 files changed, 191 insertions(+) create mode 100644 lib/GaletteObjectsLend/PluginEventProvider.php diff --git a/lib/GaletteObjectsLend/LendService.php b/lib/GaletteObjectsLend/LendService.php index ac21101..9ff36f5 100644 --- a/lib/GaletteObjectsLend/LendService.php +++ b/lib/GaletteObjectsLend/LendService.php @@ -256,6 +256,74 @@ public function changeStatus( ); } + /** + * Give back objects held by a member who is being removed + * + * Each object goes back to the last in stock status it had, or to the + * first active one. Without any in stock status, the object stays as is, + * and loses its borrower with the member. Rights to remove the member + * have already been checked. + * + * @param int $member_id Member ID + */ + public function giveBackMemberObjects(int $member_id): void + { + $select = $this->zdb->select(LEND_PREFIX . LendObject::TABLE, 'o') + ->columns([LendObject::PK]) + ->join( + ['r' => PREFIX_DB . LEND_PREFIX . LendRent::TABLE], + 'o.' . LendRent::PK . ' = r.' . LendRent::PK, + [] + ) + ->where(['r.adherent_id' => $member_id, 'r.date_end' => null]); + + $object_ids = []; + foreach ($this->zdb->execute($select) as $row) { + $object_ids[] = (int)$row[LendObject::PK]; + } + if ($object_ids === []) { + return; + } + + $stock_statuses = $this->getStatuses()->getActiveStockStatuses(); + $this->inTransaction(function () use ($object_ids, $stock_statuses): void { + foreach ($object_ids as $object_id) { + $status_id = $this->getLastStockStatus($object_id) ?? ($stock_statuses[0] ?? null)?->getId(); + if ($status_id === null) { + continue; + } + $this->openRent( + $this->getObject($object_id), + $status_id, + null, + _T("Returned on member removal", "objectslend") + ); + } + }); + } + + /** + * Last active in stock status an object had + * + * @param int $object_id Object ID + */ + private function getLastStockStatus(int $object_id): ?int + { + $select = $this->zdb->select(LEND_PREFIX . LendRent::TABLE, 'r') + ->columns([LendStatus::PK]) + ->join( + ['s' => PREFIX_DB . LEND_PREFIX . LendStatus::TABLE], + 'r.' . LendStatus::PK . ' = s.' . LendStatus::PK, + [] + ) + ->where(['r.' . LendObject::PK => $object_id, 's.in_stock' => 1, 's.is_active' => 1]) + ->order(['r.date_begin DESC', 'r.' . LendRent::PK . ' DESC']) + ->limit(1); + + $row = $this->zdb->execute($select)->current(); + return $row ? (int)$row[LendStatus::PK] : null; + } + /** * Close current rents, open a new one and set it as the object current one * diff --git a/lib/GaletteObjectsLend/PluginEventProvider.php b/lib/GaletteObjectsLend/PluginEventProvider.php new file mode 100644 index 0000000..81a5c99 --- /dev/null +++ b/lib/GaletteObjectsLend/PluginEventProvider.php @@ -0,0 +1,54 @@ + + */ +class PluginEventProvider implements ListenerSubscriber +{ + /** + * Constructor + * + * Built while plugins are loaded: the lend service is resolved only + * when an event is emitted. + * + * @param ContainerInterface $container Container + */ + public function __construct(private readonly ContainerInterface $container) + { + } + + /** + * Set up listeners + * + * @param ListenerRegistry $acceptor Listener + */ + public function subscribeListeners(ListenerRegistry $acceptor): void + { + $acceptor->subscribeTo( + 'member.before_remove', + function (GaletteEvent $event): void { + /** @var \ArrayObject $member */ + $member = $event->getObject(); + $this->container->get(LendService::class)->giveBackMemberObjects((int)$member[Adherent::PK]); + } + ); + } +} diff --git a/tests/GaletteObjectsLend/tests/units/LendService.php b/tests/GaletteObjectsLend/tests/units/LendService.php index 4b8beab..60e0ed8 100644 --- a/tests/GaletteObjectsLend/tests/units/LendService.php +++ b/tests/GaletteObjectsLend/tests/units/LendService.php @@ -213,4 +213,73 @@ public function testChangeStatusInvalid(): void $this->assertNotNull($object->getRentId()); $this->assertTrue($service->isAvailable($object)); } + + /** + * Test objects held by a removed member go back to their last in stock status + */ + public function testMemberRemovalGivesBackObjects(): void + { + //first in stock status, but not the last one the object had + $status = new LendStatus($this->zdb); + $status->setText('A first stock'); + $status->setInStock(true); + $status->setActive(true); + $status->store(); + + $repair = new LendObject($this->zdb); + $repair->setName('Under repair'); + $repair->store(); + + $member = $this->getMemberOne(); + $this->logSuperAdmin(); + $service = $this->getService(); + $service->changeStatus($service->getObject($this->object_id), $this->instock_status); + $service->take($service->getObject($this->object_id), $this->lent_status, member_id: $member->id); + //not in stock, without any borrower: must be left as is + $service->changeStatus($service->getObject((int)$repair->getId()), $this->lent_status); + $repair_rent = $service->getObject((int)$repair->getId())->getRentId(); + + $members = new \Galette\Repository\Members(); + $this->assertTrue($members->removeMembers($member->id)); + + $object = $service->getObject($this->object_id); + $this->assertNull($object->getIdAdh()); + $this->assertTrue($service->isAvailable($object)); + $this->assertSame($this->instock_status, (new LendRent($this->zdb, (int)$object->getRentId()))->getStatusId()); + + $rents = (new \GaletteObjectsLend\Repository\Rents($this->zdb))->getForObject($this->object_id); + $this->assertCount(3, $rents); + $comments = []; + foreach ($rents as $rent) { + $this->assertNull($rent->getAdherentId()); + $comments[] = $rent->getComments(); + } + $this->assertContains('Returned on member removal', $comments); + + $this->assertSame($repair_rent, $service->getObject((int)$repair->getId())->getRentId()); + } + + /** + * Test objects that never were in stock go back to the first in stock status + */ + public function testMemberRemovalGivesBackToFirstStockStatus(): void + { + $status = new LendStatus($this->zdb); + $status->setText('A first stock'); + $status->setInStock(true); + $status->setActive(true); + $status->store(); + + $member = $this->getMemberOne(); + $this->logSuperAdmin(); + $service = $this->getService(); + $service->take($service->getObject($this->object_id), $this->lent_status, member_id: $member->id); + + $members = new \Galette\Repository\Members(); + $this->assertTrue($members->removeMembers($member->id)); + + $object = $service->getObject($this->object_id); + $this->assertTrue($service->isAvailable($object)); + $this->assertSame($status->getId(), (new LendRent($this->zdb, (int)$object->getRentId()))->getStatusId()); + } } From a109fe8cf12cc2314babc381d900cf1a362ed5a7 Mon Sep 17 00:00:00 2001 From: Johan Cwiklinski Date: Thu, 24 Sep 2026 14:42:28 +0200 Subject: [PATCH 3/4] Report version of tables installed before plugins versions tracking --- lib/GaletteObjectsLend/PluginGaletteObjectslend.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/GaletteObjectsLend/PluginGaletteObjectslend.php b/lib/GaletteObjectsLend/PluginGaletteObjectslend.php index dff2cbb..406d22f 100644 --- a/lib/GaletteObjectsLend/PluginGaletteObjectslend.php +++ b/lib/GaletteObjectsLend/PluginGaletteObjectslend.php @@ -130,4 +130,14 @@ public function isInstalled(): bool && $this->zdb->tableExists(LEND_PREFIX . ObjectPicture::TABLE) ; } + + /** + * Database version of tables installed before versions tracking + * + * Parameters table has been dropped in 1.1, when preferences moved to core. + */ + public function getLegacyDbVersion(): ?float + { + return $this->zdb->tableExists(LEND_PREFIX . 'parameters') ? 1.0 : null; + } } From bf3c04e9395757572cabb7611449967dc7a84aee Mon Sep 17 00:00:00 2001 From: Johan Cwiklinski Date: Thu, 24 Sep 2026 14:42:28 +0200 Subject: [PATCH 4/4] Check upgrade from previous release on CI --- .github/workflows/ci-linux.yml | 94 ++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/.github/workflows/ci-linux.yml b/.github/workflows/ci-linux.yml index 4c8da4d..693a219 100644 --- a/.github/workflows/ci-linux.yml +++ b/.github/workflows/ci-linux.yml @@ -203,3 +203,97 @@ jobs: run: | cd galette-core/galette/plugins/plugin-objectslend ../../vendor/bin/phpunit --test-suffix=.php --bootstrap tests/TestsBootstrap.php --no-coverage --process-isolation tests/GaletteObjectsLend/ + + upgrade: + runs-on: ubuntu-latest + + strategy: + matrix: + db-image: ['mysql:8.4', 'mariadb:11', 'postgres:17'] + fail-fast: false + + env: + DB: ${{ matrix.db-image }} + + services: + # Label used to access the service container + db: + # Docker Hub image + image: ${{ matrix.db-image }} + # Provide env variables for both mysql and pgsql + env: + POSTGRES_USER: galette_tests + POSTGRES_PASSWORD: g@l3tte + POSTGRES_DB: galette_tests + MYSQL_USER: galette_tests + MYSQL_PASSWORD: g@l3tte + MYSQL_ROOT_PASSWORD: g@l3tte + MYSQL_DATABASE: galette_tests + # Open network ports for both mysql and pgsql + ports: + - 3306:3306 + - 5432:5432 + # Set health checks to wait until postgres has started + options: >- + --health-cmd="bash -c 'if [[ -n $(command -v pg_isready) ]]; then pg_isready; else if [[ -n $(command -v mysqladmin) ]]; then mysqladmin ping; else mariadb-admin ping; fi fi'" + --health-interval=10s + --health-timeout=5s + --health-retries=10 + + name: Upgrade from previous release on ${{ matrix.db-image }} + + steps: + - name: PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.4' + tools: composer, pecl + coverage: none + extensions: apcu + ini-values: apc.enable_cli=1 + + - name: Build Galette + uses: galette/.github/actions/build-galette@main + with: + php-version: '8.4' + + - name: Checkout plugin + uses: actions/checkout@v7 + with: + path: galette-core/galette/plugins/plugin-objectslend + fetch-depth: 0 + + - name: Find previous release + run: | + cd galette-core/galette/plugins/plugin-objectslend + echo "PREVIOUS_RELEASE=$(git tag --list '[0-9]*' --sort=-v:refname --no-contains HEAD | head -n1)" >> $GITHUB_ENV + + - name: Install previous release for PostgreSQL + env: + PGPASSWORD: g@l3tte + run: | + cd galette-core + bin/console galette:install -v --dbtype=pgsql --dbhost=localhost --dbname=galette_tests --dbuser=galette_tests --dbpass=g@l3tte --admin=admin --password=admin --no-interaction -w + git -C galette/plugins/plugin-objectslend show "$PREVIOUS_RELEASE:scripts/pgsql.sql" \ + | psql -v ON_ERROR_STOP=1 -h localhost -U galette_tests galette_tests + if: startsWith(matrix.db-image, 'postgres') + + - name: Install previous release for MariaDB + run: | + cd galette-core + mysql -e 'create database IF NOT EXISTS galette_tests;' -u galette_tests --password=g@l3tte -h 127.0.0.1 -P 3306 + bin/console galette:install -v --dbtype=mysql --dbhost=127.0.0.1 --dbname=galette_tests --dbuser=galette_tests --dbpass=g@l3tte --admin=admin --password=admin --no-interaction -w + git -C galette/plugins/plugin-objectslend show "$PREVIOUS_RELEASE:scripts/mysql.sql" \ + | mysql -u galette_tests --password=g@l3tte -h 127.0.0.1 -P 3306 galette_tests + if: startsWith(matrix.db-image, 'mysql') || startsWith(matrix.db-image, 'mariadb') + + - name: Upgrade + run: | + cd galette-core + bin/console galette:plugins:install-db --no-interaction plugin-objectslend | tee upgrade.log + grep -q 'Database for plugin "plugin-objectslend" upgraded' upgrade.log + + - name: Unit tests + run: | + cd galette-core/galette/plugins/plugin-objectslend + ../../vendor/bin/phpunit --test-suffix=.php --bootstrap tests/TestsBootstrap.php --no-coverage --process-isolation tests/GaletteObjectsLend/