diff --git a/.github/workflows/ci-linux.yml b/.github/workflows/ci-linux.yml index 9938bbc..11b5dc7 100644 --- a/.github/workflows/ci-linux.yml +++ b/.github/workflows/ci-linux.yml @@ -89,7 +89,7 @@ jobs: - name: CS run: | cd galette-core/galette/plugins/plugin-helloasso - ../../vendor/bin/phpcs lib/ ./*.php + ../../vendor/bin/phpcs lib/ tests/ ./*.php - name: CS Fixer if: matrix.php-is-min diff --git a/lib/GaletteHelloasso/Controllers/HelloassoController.php b/lib/GaletteHelloasso/Controllers/HelloassoController.php index f4273a2..89d9e93 100644 --- a/lib/GaletteHelloasso/Controllers/HelloassoController.php +++ b/lib/GaletteHelloasso/Controllers/HelloassoController.php @@ -42,12 +42,15 @@ class HelloassoController extends AbstractPluginController #[Inject("Plugin Galette Helloasso")] protected array $module_info; + #[Inject] + protected Helloasso $helloasso; + /** * Main form */ public function form(Response $response): Response { - $helloasso = new Helloasso($this->zdb, $this->preferences); + $helloasso = $this->helloasso; $current_url = $this->preferences->getURL(); @@ -90,7 +93,7 @@ public function form(Response $response): Response public function formCheckout(Request $request, Response $response): Response { $helloasso_request = $request->getParsedBody(); - $helloasso = new Helloasso($this->zdb, $this->preferences); + $helloasso = $this->helloasso; $adherent = new Adherent($this->zdb); // Only reasons proposed to the current user can be paid @@ -188,7 +191,7 @@ public function logs( $helloasso = $this->session->helloasso; $this->session->helloasso = null; } else { - $helloasso = new Helloasso($this->zdb, $this->preferences); + $helloasso = $this->helloasso; } $filters = []; @@ -300,7 +303,7 @@ public function preferences(Request $request, Response $response): Response $helloasso = $this->session->helloasso; $this->session->helloasso = null; } else { - $helloasso = new Helloasso($this->zdb, $this->preferences); + $helloasso = $this->helloasso; } $amounts = $helloasso->getAllAmounts(); @@ -312,6 +315,8 @@ public function preferences(Request $request, Response $response): Response 'webhook_url' => $this->preferences->getURL() . $this->routeparser->urlFor('helloasso_webhook'), 'amounts' => $amounts, 'tab' => $tab, + //one call to HelloAsso for the whole page + 'organization' => $helloasso->getOrganization(), 'documentation' => 'https://galette-plugins.github.io/plugin-helloasso/documentation.html#pr%C3%A9f%C3%A9rences' ]; @@ -330,7 +335,7 @@ public function preferences(Request $request, Response $response): Response public function storePreferences(Request $request, Response $response): Response { $post = $request->getParsedBody(); - $helloasso = new Helloasso($this->zdb, $this->preferences); + $helloasso = $this->helloasso; if ($this->login->isAdmin()) { if (array_key_exists('helloasso_test_mode', $post)) { @@ -392,7 +397,7 @@ public function webhook(Request $request, Response $response): Response { $body = $request->getBody(); $post = json_decode($body->getContents(), true); - $helloasso = new Helloasso($this->zdb, $this->preferences); + $helloasso = $this->helloasso; // Verify notification authenticity // https://dev.helloasso.com/docs/secure-webhook @@ -421,7 +426,10 @@ public function webhook(Request $request, Response $response): Response && $post['metadata']['item_id'] ) { $hh = new HelloassoHistory($this->zdb, $this->login, $this->preferences); - $hh->add($post); + if (!$hh->add($post)) { + //HelloAsso will send it again + return $response->withStatus(500, 'Internal error'); + } // are we working on a real contribution? $real_contrib = false; @@ -541,7 +549,7 @@ public function returnUrl(Request $request, Response $response): Response } try { - $helloasso = new Helloasso($this->zdb, $this->preferences); + $helloasso = $this->helloasso; $tokens = $helloasso->getTokens(); $client = $helloasso->setupClient(); @@ -552,7 +560,8 @@ public function returnUrl(Request $request, Response $response): Response ] ]; - $request = $client->get( + $api_response = $client->request( + 'GET', $helloasso->getApiRoute() . 'v5/organizations/' . $helloasso->getOrganizationSlug() @@ -560,7 +569,7 @@ public function returnUrl(Request $request, Response $response): Response . $checkout_id, $headers ); - $checkout = json_decode($request->getBody()->getContents(), true); + $checkout = json_decode($api_response->getBody()->getContents(), true); $details = [ 'amount' => $checkout['order']['amount']['total'] / 100, diff --git a/lib/GaletteHelloasso/Helloasso.php b/lib/GaletteHelloasso/Helloasso.php index ddc9d05..0a6d904 100644 --- a/lib/GaletteHelloasso/Helloasso.php +++ b/lib/GaletteHelloasso/Helloasso.php @@ -16,6 +16,7 @@ use Galette\Core\Preferences; use Galette\Entity\ContributionsTypes; use GuzzleHttp\Client; +use GuzzleHttp\ClientInterface; /** * Preferences for helloasso @@ -34,6 +35,7 @@ class Helloasso private Db $zdb; private Preferences $preferences; + private ?ClientInterface $client; /** @var array> */ private array $prices; @@ -64,13 +66,15 @@ class Helloasso /** * Default constructor * - * @param Db $zdb Database instance - * @param Preferences $preferences Preferences + * @param Db $zdb Database instance + * @param Preferences $preferences Preferences + * @param ?ClientInterface $client HTTP client, a default one is built if null */ - public function __construct(Db $zdb, Preferences $preferences) + public function __construct(Db $zdb, Preferences $preferences, ?ClientInterface $client = null) { $this->zdb = $zdb; $this->preferences = $preferences; + $this->client = $client; $this->loaded = false; $this->prices = []; $this->inactives = []; @@ -341,9 +345,11 @@ public function checkout(array $metadata, float $amount, ?bool $contains_donatio try { $tokens = $this->getTokens(); + //19.99 * 100 is 1998.9999999999998 + $amount = (int)round($amount); $data = [ - 'totalAmount' => (int)$amount, - 'initialAmount' => (int)$amount, + 'totalAmount' => $amount, + 'initialAmount' => $amount, 'itemName' => $metadata['item_name'], 'backUrl' => $this->preferences->getURL() . $routeparser->urlFor('helloasso_back'), 'errorUrl' => $this->preferences->getURL() . $routeparser->urlFor('helloasso_error'), @@ -372,7 +378,7 @@ public function checkout(array $metadata, float $amount, ?bool $contains_donatio ], 'json' => $data, ]; - $request = $client->post($this->getApiRoute() . 'v5/organizations/' . $this->getOrganizationSlug() . '/checkout-intents', $headers); + $request = $client->request('POST', $this->getApiRoute() . 'v5/organizations/' . $this->getOrganizationSlug() . '/checkout-intents', $headers); $contents = $request->getBody()->getContents(); return json_decode($contents, true); @@ -389,9 +395,9 @@ public function checkout(array $metadata, float $amount, ?bool $contains_donatio /** * Setup Guzzle client */ - public function setupClient(): Client + public function setupClient(): ClientInterface { - return new Client( + return $this->client ??= new Client( $this->getClientOptions() ); } diff --git a/lib/GaletteHelloasso/HelloassoHistory.php b/lib/GaletteHelloasso/HelloassoHistory.php index e83d4d4..697ed84 100644 --- a/lib/GaletteHelloasso/HelloassoHistory.php +++ b/lib/GaletteHelloasso/HelloassoHistory.php @@ -88,7 +88,10 @@ public function add(array|string $action, string $argument = '', string $query = $insert = $this->zdb->insert($this->getTableName()); $insert->values($values); $this->zdb->execute($insert); - $this->id = (int)$this->zdb->driver->getLastGeneratedValue(); + //without the sequence name, pgsql gives no value + $this->id = (int)$this->zdb->connection->getLastGeneratedValue( + $this->zdb->isPostgres() ? $this->zdb->getSequenceName($this->getTableName(), 'id', prefixed: true) : null + ); Analog::log( 'An entry has been added in helloasso history', @@ -143,12 +146,13 @@ public function getHelloassoHistory(): array foreach ($orig as $o) { try { if (Galette::isSerialized($o['request'])) { - $oa = unserialize($o['request']); + //legacy entries: only plain data is expected + $oa = unserialize($o['request'], ['allowed_classes' => false]); } else { $oa = Galette::jsonDecode($o['request']); } - $o['member_fullname'] = $this->getMemberFullName($o['member_id']); + $o['member_fullname'] = $this->getMemberFullName((int)$o['member_id']); $o['raw_request'] = print_r($oa, true); $o['request'] = $oa; diff --git a/lib/GaletteHelloasso/PluginGaletteHelloasso.php b/lib/GaletteHelloasso/PluginGaletteHelloasso.php index e3815ee..031fff3 100644 --- a/lib/GaletteHelloasso/PluginGaletteHelloasso.php +++ b/lib/GaletteHelloasso/PluginGaletteHelloasso.php @@ -17,6 +17,7 @@ use Galette\Core\Plugins\MenuProviderInterface; use Galette\Core\Preferences; use Galette\Core\GalettePlugin; +use Laminas\Db\Metadata\Source\Factory; /** * Galette HelloAsso plugin @@ -132,4 +133,16 @@ public function isInstalled(): bool && $this->zdb->tableExists(HELLOASSO_PREFIX . HelloassoHistory::TABLE) ; } + + /** + * Database version of tables installed before versions tracking + * + * Payer, member, method and receipt are history columns since 1.1.0. + */ + public function getLegacyDbVersion(): ?float + { + $metadata = Factory::createSourceFromAdapter($this->zdb->db); + $columns = $metadata->getColumnNames(PREFIX_DB . HELLOASSO_PREFIX . HelloassoHistory::TABLE); + return in_array('payer_name', $columns, true) ? null : 1.0; + } } diff --git a/phpstan.neon b/phpstan.neon index 299cd45..dd92411 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -4,6 +4,7 @@ parameters: level: 6 paths: - lib/ + - tests/ scanFiles: - _config.inc.php - ../../includes/sys_config/paths.inc.php diff --git a/scripts/pgsql.sql b/scripts/pgsql.sql index 576a03c..b484acb 100644 --- a/scripts/pgsql.sql +++ b/scripts/pgsql.sql @@ -15,9 +15,9 @@ CREATE SEQUENCE galette_helloasso_history_id_seq DROP TABLE IF EXISTS galette_helloasso_history; CREATE TABLE galette_helloasso_history ( id_helloasso integer DEFAULT nextval('galette_helloasso_history_id_seq'::text) NOT NULL, - history_date date NOT NULL, + history_date timestamp NOT NULL, checkout_id character varying(255), - amount real NOT NULL, + amount numeric(15,2) NOT NULL, comments character varying(255), request text, state smallint DEFAULT 0 NOT NULL, diff --git a/scripts/upgrade-to-1.1.0-mysql.sql b/scripts/upgrade-to-1.1.0-mysql.sql index 514de04..64aadda 100644 --- a/scripts/upgrade-to-1.1.0-mysql.sql +++ b/scripts/upgrade-to-1.1.0-mysql.sql @@ -6,10 +6,11 @@ INSERT INTO galette_helloasso_preferences (nom_pref, val_pref) VALUES ('helloasso_sepa_option', ''); +-- without collation, columns get the one of the table ALTER TABLE galette_helloasso_history - MODIFY checkout_id varchar(255) COLLATE utf8mb4_unicode_520_ci, - MODIFY comments varchar(255) COLLATE utf8mb4_unicode_520_ci, - MODIFY request text COLLATE utf8mb4_unicode_520_ci; + MODIFY checkout_id varchar(255), + MODIFY comments varchar(255), + MODIFY request text; ALTER TABLE galette_helloasso_history ADD COLUMN payer_name VARCHAR(255) NOT NULL, @@ -17,20 +18,23 @@ ALTER TABLE galette_helloasso_history ADD COLUMN method VARCHAR(10) NOT NULL, ADD COLUMN receipt_url VARCHAR(255) NOT NULL; +-- 1.0.0 states were 0 (public donation), 1 (processed), 2 (error), 3 (incomplete) +-- and 4 (already done); incomplete payments did not create any contribution UPDATE galette_helloasso_history SET - state = CASE - WHEN state = 0 THEN 3 + state = CASE state + WHEN 0 THEN 3 + WHEN 3 THEN 2 ELSE state END, - payer_name = CONCAT( + payer_name = COALESCE(CONCAT( UPPER(JSON_UNQUOTE(JSON_EXTRACT(request, '$.data.payer.lastName'))), ' ', JSON_UNQUOTE(JSON_EXTRACT(request, '$.data.payer.firstName')) - ), + ), ''), member_id = COALESCE( CAST(JSON_UNQUOTE(JSON_EXTRACT(request, '$.metadata.member_id')) AS UNSIGNED), 0 ), - method = JSON_UNQUOTE(JSON_EXTRACT(request, '$.data.paymentMeans')), - receipt_url = JSON_UNQUOTE(JSON_EXTRACT(request, '$.data.paymentReceiptUrl')); + method = COALESCE(JSON_UNQUOTE(JSON_EXTRACT(request, '$.data.paymentMeans')), ''), + receipt_url = COALESCE(JSON_UNQUOTE(JSON_EXTRACT(request, '$.data.paymentReceiptUrl')), ''); diff --git a/scripts/upgrade-to-1.1.0-pgsql.sql b/scripts/upgrade-to-1.1.0-pgsql.sql index 6f2d55f..73b5d02 100644 --- a/scripts/upgrade-to-1.1.0-pgsql.sql +++ b/scripts/upgrade-to-1.1.0-pgsql.sql @@ -6,26 +6,34 @@ INSERT INTO galette_helloasso_preferences (nom_pref, val_pref) VALUES ('helloasso_sepa_option', ''); +-- Keep time of payments, and do not store amounts as floating point numbers ALTER TABLE galette_helloasso_history - ADD COLUMN payer_name character varying(255) NOT NULL, - ADD COLUMN member_id integer NOT NULL, - ADD COLUMN method character varying(10) NOT NULL, - ADD COLUMN receipt_url character varying(255) NOT NULL; + ALTER COLUMN history_date TYPE timestamp, + ALTER COLUMN amount TYPE numeric(15,2), + ADD COLUMN payer_name character varying(255) DEFAULT '' NOT NULL, + ADD COLUMN member_id integer DEFAULT 0 NOT NULL, + ADD COLUMN method character varying(10) DEFAULT '' NOT NULL, + ADD COLUMN receipt_url character varying(255) DEFAULT '' NOT NULL; +ALTER TABLE galette_helloasso_history + ALTER COLUMN payer_name DROP DEFAULT, + ALTER COLUMN member_id DROP DEFAULT, + ALTER COLUMN method DROP DEFAULT, + ALTER COLUMN receipt_url DROP DEFAULT; +-- 1.0.0 states were 0 (public donation), 1 (processed), 2 (error), 3 (incomplete) +-- and 4 (already done); incomplete payments did not create any contribution UPDATE galette_helloasso_history SET - state = CASE - WHEN state = 0 THEN 3 + state = CASE state + WHEN 0 THEN 3 + WHEN 3 THEN 2 ELSE state END, - payer_name = CONCAT( - UPPER((request->'data'->'payer'->>'lastName')), + payer_name = TRIM(CONCAT( + UPPER(request::json->'data'->'payer'->>'lastName'), ' ', - (request->'data'->'payer'->>'firstName') - ), - member_id = COALESCE( - (request->'metadata'->>'member_id')::int, - 0 - ), - method = request->'data'->>'paymentMeans', - receipt_url = request->'data'->>'paymentReceiptUrl'; + request::json->'data'->'payer'->>'firstName' + )), + member_id = COALESCE((request::json->'metadata'->>'member_id')::integer, 0), + method = COALESCE(request::json->'data'->>'paymentMeans', ''), + receipt_url = COALESCE(request::json->'data'->>'paymentReceiptUrl', ''); diff --git a/templates/default/helloasso_error.html.twig b/templates/default/helloasso_error.html.twig index de6a16e..f88b289 100644 --- a/templates/default/helloasso_error.html.twig +++ b/templates/default/helloasso_error.html.twig @@ -13,7 +13,9 @@ {{ _T("Your Helloasso payment has failed for technical reasons.", "helloasso") }} {% if error %}