diff --git a/.github/workflows/analysis-test-integration.yml b/.github/workflows/analysis-test-integration.yml new file mode 100644 index 0000000000..008e3059a4 --- /dev/null +++ b/.github/workflows/analysis-test-integration.yml @@ -0,0 +1,21 @@ +name: 'Analysis - Integration test' + +on: + pull_request: + branches: [dev] + workflow_dispatch: + +defaults: + run: + shell: bash + +jobs: + e2e: + name: Test integration + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + - name: Start + run: sh integration.sh + diff --git a/README.md b/README.md index 9b5c94795f..4454de4ef1 100644 --- a/README.md +++ b/README.md @@ -76,9 +76,9 @@ bash e2e.sh ```shell # local e2e +vi .env (set your database to 'test') docker-compose up api (worker is started too) docker-compose up dashboard -vi .env (set your database to 'test') docker-compose run api yarn workspace @pdc/proxy ilos seed cd tests yarn diff --git a/api/db/migrations/20200325000000-update-trip-views.js b/api/db/migrations/20200325000000-update-trip-views.js index a71fccff33..60dc7aca4a 100644 --- a/api/db/migrations/20200325000000-update-trip-views.js +++ b/api/db/migrations/20200325000000-update-trip-views.js @@ -6,8 +6,8 @@ */ var { createMigration } = require('../helpers/createMigration'); var { setup, up, down } = createMigration([ - 'trip/202003250000000_update_trip_view', - 'trip/202003250000000_update_export_view', + 'trip/20200325000000_update_trip_view', + 'trip/20200325000000_update_export_view', ], __dirname); exports.setup = setup; diff --git a/api/db/migrations/20200325000000-update_trip_views.js b/api/db/migrations/20200325000000-update_trip_views.js index a71fccff33..60dc7aca4a 100644 --- a/api/db/migrations/20200325000000-update_trip_views.js +++ b/api/db/migrations/20200325000000-update_trip_views.js @@ -6,8 +6,8 @@ */ var { createMigration } = require('../helpers/createMigration'); var { setup, up, down } = createMigration([ - 'trip/202003250000000_update_trip_view', - 'trip/202003250000000_update_export_view', + 'trip/20200325000000_update_trip_view', + 'trip/20200325000000_update_export_view', ], __dirname); exports.setup = setup; diff --git a/api/db/migrations/20210901000000-update_policy_tables.js b/api/db/migrations/20210901000000-update_policy_tables.js new file mode 100644 index 0000000000..3ee17030d2 --- /dev/null +++ b/api/db/migrations/20210901000000-update_policy_tables.js @@ -0,0 +1,17 @@ +'use strict'; +/** + * Cast all foreign keys *_id as integer to match PostgreSQL types + * Current type is 'varchar' as fkeys were migrated from MongoDB + * as a toString() of ObjectID objects. + */ +var { createMigration } = require('../helpers/createMigration'); + +var { setup, up, down } = createMigration([ + 'policy/20210901000000_update_policy_meta', + 'policy/20210901000000_update_policy_trip_view', + 'trip/20210901000000_add_territory_index' +], __dirname); + +exports.setup = setup; +exports.up = up; +exports.down = down; diff --git a/api/db/migrations/20210930135857-create_policy_trips_view.js b/api/db/migrations/20210930135857-create_policy_trips_view.js new file mode 100644 index 0000000000..bb0c4c670f --- /dev/null +++ b/api/db/migrations/20210930135857-create_policy_trips_view.js @@ -0,0 +1,8 @@ +'use strict'; + +var { createMigration } = require('../helpers/createMigration'); +var { setup, up, down } = createMigration(['policy/20210930135857_create_policy_trips_view'], __dirname); + +exports.setup = setup; +exports.up = up; +exports.down = down; diff --git a/api/db/migrations/20211005110112-fix_hydrate_from_policy.js b/api/db/migrations/20211005110112-fix_hydrate_from_policy.js new file mode 100644 index 0000000000..065219996e --- /dev/null +++ b/api/db/migrations/20211005110112-fix_hydrate_from_policy.js @@ -0,0 +1,8 @@ +'use strict'; + +var { createMigration } = require('../helpers/createMigration'); +var { setup, up, down } = createMigration(['trip/20211005110112-fix_hydrate_from_policy'], __dirname); + +exports.setup = setup; +exports.up = up; +exports.down = down; diff --git a/api/db/migrations/policy/20210901000000_update_policy_meta.down.sql b/api/db/migrations/policy/20210901000000_update_policy_meta.down.sql new file mode 100644 index 0000000000..06c923160b --- /dev/null +++ b/api/db/migrations/policy/20210901000000_update_policy_meta.down.sql @@ -0,0 +1,7 @@ +ALTER TABLE policy.policy_metas + DROP COLUMN datetime, + ALTER COLUMN value TYPE JSON + USING value::text::json; +DROP INDEX IF EXISTS policy.policy_meta_id_key; +CREATE UNIQUE INDEX policy_meta_unique_key ON policy.policy_metas (policy_id, key); + diff --git a/api/db/migrations/policy/20210901000000_update_policy_meta.up.sql b/api/db/migrations/policy/20210901000000_update_policy_meta.up.sql new file mode 100644 index 0000000000..cd11427e93 --- /dev/null +++ b/api/db/migrations/policy/20210901000000_update_policy_meta.up.sql @@ -0,0 +1,10 @@ +ALTER TABLE policy.policy_metas + ADD COLUMN datetime timestamp, + ALTER COLUMN value TYPE INT + USING CASE + WHEN value::text = '{}' THEN 0 + ELSE value::text::int + END; +DROP INDEX IF EXISTS policy.policy_meta_unique_key; +CREATE INDEX policy_meta_id_key ON policy.policy_metas (policy_id, key); +CREATE INDEX policy_meta_incentive ON policy.policy_metas (datetime); diff --git a/api/db/migrations/policy/20210901000000_update_policy_trip_view.down.sql b/api/db/migrations/policy/20210901000000_update_policy_trip_view.down.sql new file mode 100644 index 0000000000..4cc1c6b2b3 --- /dev/null +++ b/api/db/migrations/policy/20210901000000_update_policy_trip_view.down.sql @@ -0,0 +1,80 @@ +CREATE EXTENSION IF NOT EXISTS intarray; +DROP MATERIALIZED VIEW IF EXISTS policy.trips; + +CREATE MATERIALIZED VIEW policy.trips AS ( + SELECT + cp._id as carpool_id, + cp.status as carpool_status, + cp.trip_id as trip_id, + tsc.value[1] as start_insee, + tec.value[1] as end_insee, + cp.operator_id::int as operator_id, + cp.operator_class as operator_class, + cp.datetime as datetime, + cp.seats as seats, + cp.cost as cost, + cp.is_driver as is_driver, + (CASE WHEN cp.distance IS NOT NULL THEN cp.distance ELSE (cp.meta::json->>'calc_distance')::int END) as distance, + (CASE WHEN cp.duration IS NOT NULL THEN cp.duration ELSE (cp.meta::json->>'calc_duration')::int END) as duration, + id.identity_uuid as identity_uuid, + id.has_travel_pass as has_travel_pass, + id.is_over_18 as is_over_18, + ats || cp.start_territory_id as start_territory_id, + ate || cp.end_territory_id as end_territory_id, + ap.applicable_policies as applicable_policies, + pp.processed_policies as processed_policies, + (ap.applicable_policies - pp.processed_policies) as processable_policies + FROM carpool.carpools as cp + LEFT JOIN territory.get_ancestors(ARRAY[cp.start_territory_id]) as ats ON TRUE + LEFT JOIN territory.get_ancestors(ARRAY[cp.end_territory_id]) as ate ON TRUE, + LATERAL ( + SELECT + array_agg(value) as value + FROM territory.territory_codes + WHERE territory_id = cp.start_territory_id + AND type = 'insee' + ) as tsc, + LATERAL ( + SELECT + array_agg(value) as value + FROM territory.territory_codes + WHERE territory_id = cp.end_territory_id + AND type = 'insee' + ) as tec, + -- Find all policies that appliable to carpool + LATERAL ( + SELECT + COALESCE(array_agg(pp._id), ARRAY[]::int[]) as applicable_policies + FROM policy.policies as pp + WHERE + pp.territory_id = any(cp.start_territory_id || ats || ate || cp.end_territory_id) + AND pp.start_date <= cp.datetime + AND pp.end_date >= cp.datetime + AND pp.status = 'active' + ) as ap, + -- Find all already processed policies + LATERAL ( + SELECT + COALESCE(array_agg(pi.policy_id), ARRAY[]::int[]) as processed_policies + FROM policy.incentives as pi + WHERE + pi.carpool_id::int = cp._id + ) as pp, + -- Find identity relative data + LATERAL ( + SELECT + (CASE WHEN ci.travel_pass_user_id IS NOT NULL THEN true ELSE false END) as has_travel_pass, + (CASE WHEN ci.over_18 IS NOT NULL THEN ci.over_18 ELSE null END) as is_over_18, + ci.uuid as identity_uuid + FROM carpool.identities as ci + WHERE + cp.identity_id = ci._id + ) as id + WHERE cp.datetime >= (NOW() - interval '140 days') AND cp.datetime < (NOW() - interval '5 days') +); + +CREATE UNIQUE INDEX IF NOT EXISTS trips_carpool_id_idx ON policy.trips (carpool_id); +CREATE INDEX IF NOT EXISTS trips_datetime_idx ON policy.trips (datetime); +CREATE INDEX IF NOT EXISTS trips_trip_id_idx ON policy.trips (trip_id); +CREATE INDEX IF NOT EXISTS trips_applicable_policies_idx ON policy.trips (applicable_policies); +CREATE INDEX IF NOT EXISTS trips_processable_policies_idx ON policy.trips (processable_policies); diff --git a/api/db/migrations/policy/20210901000000_update_policy_trip_view.up.sql b/api/db/migrations/policy/20210901000000_update_policy_trip_view.up.sql new file mode 100644 index 0000000000..1616aadf6f --- /dev/null +++ b/api/db/migrations/policy/20210901000000_update_policy_trip_view.up.sql @@ -0,0 +1,28 @@ +CREATE EXTENSION IF NOT EXISTS intarray; +DROP MATERIALIZED VIEW IF EXISTS policy.trips; + +CREATE VIEW policy.trips AS ( + SELECT + cp._id as carpool_id, + cp.status as carpool_status, + cp.trip_id as trip_id, + cp.acquisition_id as acquisition_id, + cp.operator_id::int as operator_id, + cp.operator_class as operator_class, + cp.datetime as datetime, + cp.seats as seats, + cp.cost as cost, + cp.is_driver as is_driver, + (CASE WHEN cp.distance IS NOT NULL THEN cp.distance ELSE (cp.meta::json->>'calc_distance')::int END) as distance, + (CASE WHEN cp.duration IS NOT NULL THEN cp.duration ELSE (cp.meta::json->>'calc_duration')::int END) as duration, + (CASE WHEN ci.travel_pass_user_id IS NOT NULL THEN true ELSE false END) as has_travel_pass, + (CASE WHEN ci.over_18 IS NOT NULL THEN ci.over_18 ELSE null END) as is_over_18, + ci.uuid as identity_uuid, + ats || cp.start_territory_id as start_territory_id, + ate || cp.end_territory_id as end_territory_id + FROM carpool.carpools as cp + LEFT JOIN territory.get_ancestors(ARRAY[cp.start_territory_id]) as ats ON TRUE + LEFT JOIN territory.get_ancestors(ARRAY[cp.end_territory_id]) as ate ON TRUE + LEFT JOIN carpool.identities as ci + ON cp.identity_id = ci._id +); diff --git a/api/db/migrations/policy/20210930135857_create_policy_trips_view.down.sql b/api/db/migrations/policy/20210930135857_create_policy_trips_view.down.sql new file mode 100644 index 0000000000..b7c535e567 --- /dev/null +++ b/api/db/migrations/policy/20210930135857_create_policy_trips_view.down.sql @@ -0,0 +1,28 @@ +CREATE EXTENSION IF NOT EXISTS intarray; +DROP VIEW IF EXISTS policy.trips; + +CREATE VIEW policy.trips AS ( + SELECT + cp._id as carpool_id, + cp.status as carpool_status, + cp.trip_id as trip_id, + cp.acquisition_id as acquisition_id, + cp.operator_id::int as operator_id, + cp.operator_class as operator_class, + cp.datetime as datetime, + cp.seats as seats, + cp.cost as cost, + cp.is_driver as is_driver, + (CASE WHEN cp.distance IS NOT NULL THEN cp.distance ELSE (cp.meta::json->>'calc_distance')::int END) as distance, + (CASE WHEN cp.duration IS NOT NULL THEN cp.duration ELSE (cp.meta::json->>'calc_duration')::int END) as duration, + (CASE WHEN ci.travel_pass_user_id IS NOT NULL THEN true ELSE false END) as has_travel_pass, + (CASE WHEN ci.over_18 IS NOT NULL THEN ci.over_18 ELSE null END) as is_over_18, + ci.uuid as identity_uuid, + ats || cp.start_territory_id as start_territory_id, + ate || cp.end_territory_id as end_territory_id + FROM carpool.carpools as cp + LEFT JOIN territory.get_ancestors(ARRAY[cp.start_territory_id]) as ats ON TRUE + LEFT JOIN territory.get_ancestors(ARRAY[cp.end_territory_id]) as ate ON TRUE + LEFT JOIN carpool.identities as ci + ON cp.identity_id = ci._id +); diff --git a/api/db/migrations/policy/20210930135857_create_policy_trips_view.up.sql b/api/db/migrations/policy/20210930135857_create_policy_trips_view.up.sql new file mode 100644 index 0000000000..26cb90d77a --- /dev/null +++ b/api/db/migrations/policy/20210930135857_create_policy_trips_view.up.sql @@ -0,0 +1,29 @@ +CREATE EXTENSION IF NOT EXISTS intarray; +DROP VIEW IF EXISTS policy.trips; + +CREATE VIEW policy.trips AS ( + SELECT + cp._id as carpool_id, + cp.status as carpool_status, + cp.trip_id as trip_id, + cp.acquisition_id as acquisition_id, + cp.operator_id::int as operator_id, + cp.operator_class as operator_class, + cp.datetime as datetime, + cp.start_territory_id as start_territory_id, + cp.end_territory_id as end_territory_id, + cp.seats as seats, + cp.cost as cost, + cp.is_driver as is_driver, + (CASE WHEN cp.distance IS NOT NULL THEN cp.distance ELSE (cp.meta::json->>'calc_distance')::int END) as distance, + (CASE WHEN cp.duration IS NOT NULL THEN cp.duration ELSE (cp.meta::json->>'calc_duration')::int END) as duration, + (CASE WHEN ci.travel_pass_user_id IS NOT NULL THEN true ELSE false END) as has_travel_pass, + (CASE WHEN ci.over_18 IS NOT NULL THEN ci.over_18 ELSE null END) as is_over_18, + ci.uuid as identity_uuid, + tcs.value AS start_insee, + tce.value AS end_insee + FROM carpool.carpools as cp + LEFT JOIN carpool.identities as ci ON cp.identity_id = ci._id + LEFT JOIN territory.territory_codes tcs ON cp.start_territory_id = tcs.territory_id AND tcs.type::text = 'insee'::text + LEFT JOIN territory.territory_codes tce ON cp.end_territory_id = tce.territory_id AND tce.type::text = 'insee'::text +); diff --git a/api/db/migrations/trip/202003250000000_update_export_view.down.sql b/api/db/migrations/trip/20200325000000_update_export_view.down.sql similarity index 100% rename from api/db/migrations/trip/202003250000000_update_export_view.down.sql rename to api/db/migrations/trip/20200325000000_update_export_view.down.sql diff --git a/api/db/migrations/trip/202003250000000_update_export_view.up.sql b/api/db/migrations/trip/20200325000000_update_export_view.up.sql similarity index 100% rename from api/db/migrations/trip/202003250000000_update_export_view.up.sql rename to api/db/migrations/trip/20200325000000_update_export_view.up.sql diff --git a/api/db/migrations/trip/202003250000000_update_trip_view.down.sql b/api/db/migrations/trip/20200325000000_update_trip_view.down.sql similarity index 100% rename from api/db/migrations/trip/202003250000000_update_trip_view.down.sql rename to api/db/migrations/trip/20200325000000_update_trip_view.down.sql diff --git a/api/db/migrations/trip/202003250000000_update_trip_view.up.sql b/api/db/migrations/trip/20200325000000_update_trip_view.up.sql similarity index 100% rename from api/db/migrations/trip/202003250000000_update_trip_view.up.sql rename to api/db/migrations/trip/20200325000000_update_trip_view.up.sql diff --git a/api/db/migrations/trip/20210901000000_add_territory_index.down.sql b/api/db/migrations/trip/20210901000000_add_territory_index.down.sql new file mode 100644 index 0000000000..2cf81584c9 --- /dev/null +++ b/api/db/migrations/trip/20210901000000_add_territory_index.down.sql @@ -0,0 +1,2 @@ +DROP INDEX IF EXISTS trip.start_territory_id_idx; +DROP INDEX IF EXISTS trip.end_territory_id_idx; diff --git a/api/db/migrations/trip/20210901000000_add_territory_index.up.sql b/api/db/migrations/trip/20210901000000_add_territory_index.up.sql new file mode 100644 index 0000000000..0d0cde1471 --- /dev/null +++ b/api/db/migrations/trip/20210901000000_add_territory_index.up.sql @@ -0,0 +1,2 @@ +CREATE INDEX start_territory_id_idx ON carpool.carpools (start_territory_id); +CREATE INDEX end_territory_id_idx ON carpool.carpools (end_territory_id); diff --git a/api/db/migrations/trip/20211005110112-fix_hydrate_from_policy.down.sql b/api/db/migrations/trip/20211005110112-fix_hydrate_from_policy.down.sql new file mode 100644 index 0000000000..f79139174c --- /dev/null +++ b/api/db/migrations/trip/20211005110112-fix_hydrate_from_policy.down.sql @@ -0,0 +1,228 @@ +CREATE OR REPLACE FUNCTION hydrate_trip_from_policy() RETURNS TRIGGER AS $$ +BEGIN + INSERT INTO trip.list ( + operator_id, + start_territory_id, + end_territory_id, + applied_policies, + journey_id, + trip_id, + journey_start_datetime, + journey_start_weekday, + journey_start_dayhour, + journey_start_lon, + journey_start_lat, + journey_start_insee, + journey_start_postalcode, + journey_start_department, + journey_start_town, + journey_start_towngroup, + journey_start_country, + journey_end_datetime, + journey_end_lon, + journey_end_lat, + journey_end_insee, + journey_end_postalcode, + journey_end_department, + journey_end_town, + journey_end_towngroup, + journey_end_country, + journey_distance, + journey_distance_anounced, + journey_distance_calculated, + journey_duration, + journey_duration_anounced, + journey_duration_calculated, + operator, + operator_class, + operator_journey_id, + operator_passenger_id, + operator_driver_id, + passenger_id, + passenger_card, + passenger_over_18, + passenger_seats, + passenger_contribution, + passenger_incentive_raw, + passenger_incentive_rpc_raw, + passenger_incentive_rpc_sum, + passenger_incentive_rpc_financial_sum, + driver_id, + driver_card, + driver_revenue, + driver_incentive_raw, + driver_incentive_rpc_raw, + driver_incentive_rpc_sum, + driver_incentive_rpc_financial_sum, + status + ) SELECT + tv.operator_id, + tv.start_territory_id, + tv.end_territory_id, + tv.applied_policies, + tv.journey_id, + tv.trip_id, + tv.journey_start_datetime, + tv.journey_start_weekday, + tv.journey_start_dayhour, + tv.journey_start_lon, + tv.journey_start_lat, + tv.journey_start_insee, + tv.journey_start_postalcode, + tv.journey_start_department, + tv.journey_start_town, + tv.journey_start_towngroup, + tv.journey_start_country, + tv.journey_end_datetime, + tv.journey_end_lon, + tv.journey_end_lat, + tv.journey_end_insee, + tv.journey_end_postalcode, + tv.journey_end_department, + tv.journey_end_town, + tv.journey_end_towngroup, + tv.journey_end_country, + tv.journey_distance, + tv.journey_distance_anounced, + tv.journey_distance_calculated, + tv.journey_duration, + tv.journey_duration_anounced, + tv.journey_duration_calculated, + tv.operator, + tv.operator_class, + tv.operator_journey_id, + tv.operator_passenger_id, + tv.operator_driver_id, + tv.passenger_id, + tv.passenger_card, + tv.passenger_over_18, + tv.passenger_seats, + tv.passenger_contribution, + tv.passenger_incentive_raw, + tv.passenger_incentive_rpc_raw, + tv.passenger_incentive_rpc_sum, + tv.passenger_incentive_rpc_financial_sum, + tv.driver_id, + tv.driver_card, + tv.driver_revenue, + tv.driver_incentive_raw, + tv.driver_incentive_rpc_raw, + tv.driver_incentive_rpc_sum, + tv.driver_incentive_rpc_financial_sum, + tv.status + FROM carpool.carpools AS cc + LEFT JOIN trip.list_view AS tv ON tv.journey_id = cc.acquisition_id + WHERE cc._id = NEW.carpool_id + ON CONFLICT (journey_id) + DO UPDATE SET ( + operator_id, + start_territory_id, + end_territory_id, + applied_policies, + trip_id, + journey_start_datetime, + journey_start_weekday, + journey_start_dayhour, + journey_start_lon, + journey_start_lat, + journey_start_insee, + journey_start_postalcode, + journey_start_department, + journey_start_town, + journey_start_towngroup, + journey_start_country, + journey_end_datetime, + journey_end_lon, + journey_end_lat, + journey_end_insee, + journey_end_postalcode, + journey_end_department, + journey_end_town, + journey_end_towngroup, + journey_end_country, + journey_distance, + journey_distance_anounced, + journey_distance_calculated, + journey_duration, + journey_duration_anounced, + journey_duration_calculated, + operator, + operator_class, + operator_journey_id, + operator_passenger_id, + operator_driver_id, + passenger_id, + passenger_card, + passenger_over_18, + passenger_seats, + passenger_contribution, + passenger_incentive_raw, + passenger_incentive_rpc_raw, + passenger_incentive_rpc_sum, + passenger_incentive_rpc_financial_sum, + driver_id, + driver_card, + driver_revenue, + driver_incentive_raw, + driver_incentive_rpc_raw, + driver_incentive_rpc_sum, + driver_incentive_rpc_financial_sum, + status + ) = ( + excluded.operator_id, + excluded.start_territory_id, + excluded.end_territory_id, + excluded.applied_policies, + excluded.trip_id, + excluded.journey_start_datetime, + excluded.journey_start_weekday, + excluded.journey_start_dayhour, + excluded.journey_start_lon, + excluded.journey_start_lat, + excluded.journey_start_insee, + excluded.journey_start_postalcode, + excluded.journey_start_department, + excluded.journey_start_town, + excluded.journey_start_towngroup, + excluded.journey_start_country, + excluded.journey_end_datetime, + excluded.journey_end_lon, + excluded.journey_end_lat, + excluded.journey_end_insee, + excluded.journey_end_postalcode, + excluded.journey_end_department, + excluded.journey_end_town, + excluded.journey_end_towngroup, + excluded.journey_end_country, + excluded.journey_distance, + excluded.journey_distance_anounced, + excluded.journey_distance_calculated, + excluded.journey_duration, + excluded.journey_duration_anounced, + excluded.journey_duration_calculated, + excluded.operator, + excluded.operator_class, + excluded.operator_journey_id, + excluded.operator_passenger_id, + excluded.operator_driver_id, + excluded.passenger_id, + excluded.passenger_card, + excluded.passenger_over_18, + excluded.passenger_seats, + excluded.passenger_contribution, + excluded.passenger_incentive_raw, + excluded.passenger_incentive_rpc_raw, + excluded.passenger_incentive_rpc_sum, + excluded.passenger_incentive_rpc_financial_sum, + excluded.driver_id, + excluded.driver_card, + excluded.driver_revenue, + excluded.driver_incentive_raw, + excluded.driver_incentive_rpc_raw, + excluded.driver_incentive_rpc_sum, + excluded.driver_incentive_rpc_financial_sum, + excluded.status + ); + RETURN NULL; +END; +$$ language plpgsql; diff --git a/api/db/migrations/trip/20211005110112-fix_hydrate_from_policy.up.sql b/api/db/migrations/trip/20211005110112-fix_hydrate_from_policy.up.sql new file mode 100644 index 0000000000..f79139174c --- /dev/null +++ b/api/db/migrations/trip/20211005110112-fix_hydrate_from_policy.up.sql @@ -0,0 +1,228 @@ +CREATE OR REPLACE FUNCTION hydrate_trip_from_policy() RETURNS TRIGGER AS $$ +BEGIN + INSERT INTO trip.list ( + operator_id, + start_territory_id, + end_territory_id, + applied_policies, + journey_id, + trip_id, + journey_start_datetime, + journey_start_weekday, + journey_start_dayhour, + journey_start_lon, + journey_start_lat, + journey_start_insee, + journey_start_postalcode, + journey_start_department, + journey_start_town, + journey_start_towngroup, + journey_start_country, + journey_end_datetime, + journey_end_lon, + journey_end_lat, + journey_end_insee, + journey_end_postalcode, + journey_end_department, + journey_end_town, + journey_end_towngroup, + journey_end_country, + journey_distance, + journey_distance_anounced, + journey_distance_calculated, + journey_duration, + journey_duration_anounced, + journey_duration_calculated, + operator, + operator_class, + operator_journey_id, + operator_passenger_id, + operator_driver_id, + passenger_id, + passenger_card, + passenger_over_18, + passenger_seats, + passenger_contribution, + passenger_incentive_raw, + passenger_incentive_rpc_raw, + passenger_incentive_rpc_sum, + passenger_incentive_rpc_financial_sum, + driver_id, + driver_card, + driver_revenue, + driver_incentive_raw, + driver_incentive_rpc_raw, + driver_incentive_rpc_sum, + driver_incentive_rpc_financial_sum, + status + ) SELECT + tv.operator_id, + tv.start_territory_id, + tv.end_territory_id, + tv.applied_policies, + tv.journey_id, + tv.trip_id, + tv.journey_start_datetime, + tv.journey_start_weekday, + tv.journey_start_dayhour, + tv.journey_start_lon, + tv.journey_start_lat, + tv.journey_start_insee, + tv.journey_start_postalcode, + tv.journey_start_department, + tv.journey_start_town, + tv.journey_start_towngroup, + tv.journey_start_country, + tv.journey_end_datetime, + tv.journey_end_lon, + tv.journey_end_lat, + tv.journey_end_insee, + tv.journey_end_postalcode, + tv.journey_end_department, + tv.journey_end_town, + tv.journey_end_towngroup, + tv.journey_end_country, + tv.journey_distance, + tv.journey_distance_anounced, + tv.journey_distance_calculated, + tv.journey_duration, + tv.journey_duration_anounced, + tv.journey_duration_calculated, + tv.operator, + tv.operator_class, + tv.operator_journey_id, + tv.operator_passenger_id, + tv.operator_driver_id, + tv.passenger_id, + tv.passenger_card, + tv.passenger_over_18, + tv.passenger_seats, + tv.passenger_contribution, + tv.passenger_incentive_raw, + tv.passenger_incentive_rpc_raw, + tv.passenger_incentive_rpc_sum, + tv.passenger_incentive_rpc_financial_sum, + tv.driver_id, + tv.driver_card, + tv.driver_revenue, + tv.driver_incentive_raw, + tv.driver_incentive_rpc_raw, + tv.driver_incentive_rpc_sum, + tv.driver_incentive_rpc_financial_sum, + tv.status + FROM carpool.carpools AS cc + LEFT JOIN trip.list_view AS tv ON tv.journey_id = cc.acquisition_id + WHERE cc._id = NEW.carpool_id + ON CONFLICT (journey_id) + DO UPDATE SET ( + operator_id, + start_territory_id, + end_territory_id, + applied_policies, + trip_id, + journey_start_datetime, + journey_start_weekday, + journey_start_dayhour, + journey_start_lon, + journey_start_lat, + journey_start_insee, + journey_start_postalcode, + journey_start_department, + journey_start_town, + journey_start_towngroup, + journey_start_country, + journey_end_datetime, + journey_end_lon, + journey_end_lat, + journey_end_insee, + journey_end_postalcode, + journey_end_department, + journey_end_town, + journey_end_towngroup, + journey_end_country, + journey_distance, + journey_distance_anounced, + journey_distance_calculated, + journey_duration, + journey_duration_anounced, + journey_duration_calculated, + operator, + operator_class, + operator_journey_id, + operator_passenger_id, + operator_driver_id, + passenger_id, + passenger_card, + passenger_over_18, + passenger_seats, + passenger_contribution, + passenger_incentive_raw, + passenger_incentive_rpc_raw, + passenger_incentive_rpc_sum, + passenger_incentive_rpc_financial_sum, + driver_id, + driver_card, + driver_revenue, + driver_incentive_raw, + driver_incentive_rpc_raw, + driver_incentive_rpc_sum, + driver_incentive_rpc_financial_sum, + status + ) = ( + excluded.operator_id, + excluded.start_territory_id, + excluded.end_territory_id, + excluded.applied_policies, + excluded.trip_id, + excluded.journey_start_datetime, + excluded.journey_start_weekday, + excluded.journey_start_dayhour, + excluded.journey_start_lon, + excluded.journey_start_lat, + excluded.journey_start_insee, + excluded.journey_start_postalcode, + excluded.journey_start_department, + excluded.journey_start_town, + excluded.journey_start_towngroup, + excluded.journey_start_country, + excluded.journey_end_datetime, + excluded.journey_end_lon, + excluded.journey_end_lat, + excluded.journey_end_insee, + excluded.journey_end_postalcode, + excluded.journey_end_department, + excluded.journey_end_town, + excluded.journey_end_towngroup, + excluded.journey_end_country, + excluded.journey_distance, + excluded.journey_distance_anounced, + excluded.journey_distance_calculated, + excluded.journey_duration, + excluded.journey_duration_anounced, + excluded.journey_duration_calculated, + excluded.operator, + excluded.operator_class, + excluded.operator_journey_id, + excluded.operator_passenger_id, + excluded.operator_driver_id, + excluded.passenger_id, + excluded.passenger_card, + excluded.passenger_over_18, + excluded.passenger_seats, + excluded.passenger_contribution, + excluded.passenger_incentive_raw, + excluded.passenger_incentive_rpc_raw, + excluded.passenger_incentive_rpc_sum, + excluded.passenger_incentive_rpc_financial_sum, + excluded.driver_id, + excluded.driver_card, + excluded.driver_revenue, + excluded.driver_incentive_raw, + excluded.driver_incentive_rpc_raw, + excluded.driver_incentive_rpc_sum, + excluded.driver_incentive_rpc_financial_sum, + excluded.status + ); + RETURN NULL; +END; +$$ language plpgsql; diff --git a/api/ilos/core/src/foundation/Kernel.ts b/api/ilos/core/src/foundation/Kernel.ts index da60a4c1ce..6ba6ecbb4e 100644 --- a/api/ilos/core/src/foundation/Kernel.ts +++ b/api/ilos/core/src/foundation/Kernel.ts @@ -108,7 +108,7 @@ export abstract class Kernel extends ServiceProvider implements KernelInterface return handler(call); } - return promiseTimeout(timeout, handler(call)); + return promiseTimeout(timeout, handler(call), config.signature); } /** diff --git a/api/ilos/core/src/helpers/promiseTimeout.ts b/api/ilos/core/src/helpers/promiseTimeout.ts index f7f3de823a..2ec72ff40c 100644 --- a/api/ilos/core/src/helpers/promiseTimeout.ts +++ b/api/ilos/core/src/helpers/promiseTimeout.ts @@ -1,12 +1,26 @@ import { TimeoutException } from '@ilos/common'; -export function promiseTimeout(ms: number, promise: Promise): Promise { +export function promiseTimeout(ms: number, promise: Promise, signature?: string): Promise { + const s = new Date(); + let id = null; const timeout = new Promise((_resolve, reject) => { - const id = setTimeout(() => { + if (id) clearTimeout(id); + id = setTimeout(() => { clearTimeout(id); + console.debug(`[kernel] ${signature || ''} timeout expired (${ms}ms)`); reject(new TimeoutException(`Timeout Exception (${ms}ms)`)); }, ms); }); - return Promise.race([promise, timeout]) as Promise; + return Promise.race([promise, timeout]) + .then((res) => { + clearTimeout(id); + console.debug(`[kernel] ${signature || ''} succeeded in ${(new Date().getTime() - s.getTime()) / 1000}s`); + return res; + }) + .catch((err) => { + clearTimeout(id); + console.error(`[kernel] ${signature || ''} failed`, { message: err.message }); + throw err; + }) as Promise; } diff --git a/api/ilos/handler-redis/src/QueueHandler.integration.spec.ts b/api/ilos/handler-redis/src/QueueHandler.integration.spec.ts index a0c1978e70..a78cf66ec3 100644 --- a/api/ilos/handler-redis/src/QueueHandler.integration.spec.ts +++ b/api/ilos/handler-redis/src/QueueHandler.integration.spec.ts @@ -18,15 +18,9 @@ test.beforeEach(async (t) => { }, }; - class FakeRedis extends RedisConnection { - getClient() { - return null; - } - } + const connection = new RedisConnection({ connectionString: process.env.APP_REDIS_URL ?? 'redis://127.0.0.1:6379' }); - const fakeConnection = new FakeRedis({}); - - t.context.handler = new (queueHandlerFactory('basic', '0.0.1'))(fakeConnection); + t.context.handler = new (queueHandlerFactory('basic', '0.0.1'))(connection); await t.context.handler.init(); }); diff --git a/api/package.json b/api/package.json index a519891e36..e9a6568d0f 100644 --- a/api/package.json +++ b/api/package.json @@ -69,8 +69,8 @@ "@types/uuid": "^8.3.1", "ava": "^3.15.0", "axios": "^0.21.4", - "concurrently": "^6.2.1", - "faker": "^5.4.0", + "concurrently": "^6.0.0", + "faker": "^5.5.0", "lerna": "^4.0.0", "lodash": "^4.17.21", "npm-run-all": "^4.1.5", @@ -81,4 +81,4 @@ "tsconfig-paths": "^3.11.0", "typescript": "^4.4.3" } -} +} \ No newline at end of file diff --git a/api/providers/geo/src/providers/PhotonProvider.ts b/api/providers/geo/src/providers/PhotonProvider.ts index f9e0da8e54..2d94ac07ec 100644 --- a/api/providers/geo/src/providers/PhotonProvider.ts +++ b/api/providers/geo/src/providers/PhotonProvider.ts @@ -23,7 +23,7 @@ interface PhotonResponse { @provider() export class PhotonProvider implements GeoCoderInterface { - protected domain = 'https://photon.komoot.de/api'; + protected domain = 'https://photon.komoot.io/api'; async literalToPosition(literal: string): Promise { const res: PhotonResponse = await axios.get(`${this.domain}/?q=${encodeURIComponent(literal)}&limit=1`); diff --git a/api/providers/test/package.json b/api/providers/test/package.json index 33d9593911..9ae82e58a7 100644 --- a/api/providers/test/package.json +++ b/api/providers/test/package.json @@ -19,7 +19,7 @@ "@ilos/connection-postgres": "~0", "@ilos/framework": "~0", "@pdc/helper-seed": "~0", - "faker": "^5.4.0", + "faker": "^5.5.0", "supertest": "^6.1.6", "uuid": "^8.3.2" }, diff --git a/api/providers/test/src/fixtures/generators/TripGenerator.ts b/api/providers/test/src/fixtures/generators/TripGenerator.ts index fa851af926..f024b3caec 100644 --- a/api/providers/test/src/fixtures/generators/TripGenerator.ts +++ b/api/providers/test/src/fixtures/generators/TripGenerator.ts @@ -212,8 +212,8 @@ export class TripGenerator extends Generator { // calculate the number of meters for 1° // metersAtLat = meterAtEquator * cos(lat) const metersAtLat = 111200 * Math.cos(origin.lat * (Math.PI / 180)); - const radius = faker.random.number(origin.radius) / metersAtLat; - const angle = (faker.random.number(360 * 10000) * (Math.PI / 180)) / 10000; + const radius = faker.datatype.number(origin.radius) / metersAtLat; + const angle = (faker.datatype.number(360 * 10000) * (Math.PI / 180)) / 10000; return { lon: origin.lon + radius * Math.cos(angle), @@ -230,11 +230,11 @@ export class TripGenerator extends Generator { } private getPayload(payload: Partial = {}): AcquisitionPayload { - const distance = faker.random.number(100000); - const passengerStart = faker.date.recent(faker.random.number(90)); - const passengerDuration = faker.random.number(7200); - const driverStart = new Date(passengerStart.getTime() - faker.random.number(1800)); - const driverDuration = passengerDuration + faker.random.number(3600); + const distance = faker.datatype.number(100000); + const passengerStart = faker.date.recent(faker.datatype.number(90)); + const passengerDuration = faker.datatype.number(7200); + const driverStart = new Date(passengerStart.getTime() - faker.datatype.number(1800)); + const driverDuration = passengerDuration + faker.datatype.number(3600); const base = { operator_class: faker.random.arrayElement(['A', 'B', 'C']), @@ -244,8 +244,8 @@ export class TripGenerator extends Generator { distance, duration: passengerDuration, incentives: [], - contribution: faker.random.number(2000), - seats: 1 + faker.random.number(7), + contribution: faker.datatype.number(2000), + seats: 1 + faker.datatype.number(7), start: { datetime: passengerStart, lat: 48.77826, @@ -258,10 +258,10 @@ export class TripGenerator extends Generator { }, }, driver: { - distance: distance + faker.random.number(10000), + distance: distance + faker.datatype.number(10000), duration: driverDuration, incentives: [], - revenue: faker.random.number(2000), + revenue: faker.datatype.number(2000), start: { datetime: driverStart, lat: 48.77826, diff --git a/api/proxy/src/middlewares/errorHandlerMiddleware.ts b/api/proxy/src/middlewares/errorHandlerMiddleware.ts index c4dc810041..8cdb25ee7f 100644 --- a/api/proxy/src/middlewares/errorHandlerMiddleware.ts +++ b/api/proxy/src/middlewares/errorHandlerMiddleware.ts @@ -55,9 +55,10 @@ export function errorHandlerMiddleware( try { const { id, method } = Array.isArray(_req.body) ? _req.body.pop() : _req.body; - console.error(`[proxy] ${err.name} ${code} ${err.message}`, { id, method }); + console.error(`[errorHandler] ${err.name} ${code} ${err.message}\n${err.stack}`, { id, method }); } catch (e) {} + if (res.headersSent) return; res.status(code).json({ id: 1, jsonrpc: '2.0', diff --git a/api/proxy/src/tests/journey-status.integration.spec.ts b/api/proxy/src/tests/journey-status.integration.spec.ts index 70d1f9c2c5..199f57289e 100644 --- a/api/proxy/src/tests/journey-status.integration.spec.ts +++ b/api/proxy/src/tests/journey-status.integration.spec.ts @@ -85,7 +85,7 @@ test.beforeEach(async (t) => { t.context.cookies = await cookieLoginHelper(t.context.request, 'maxicovoit.admin@example.com', 'admin1234'); }); -test("Status: check 'pending' journey", async (t) => { +test.skip("Status: check 'pending' journey", async (t) => { const journey_id = uuid(); // manually create a journey in database @@ -122,9 +122,7 @@ test("Status: check 'pending' journey", async (t) => { }); }); -test('Status: check wrong permissions', async (t) => { - t.pass(); // FIXME - return; +test.skip('Status: check wrong permissions', async (t) => { const journey_id = uuid(); // check the status diff --git a/api/services/policy/package.json b/api/services/policy/package.json index e9c371911c..99d03558ae 100644 --- a/api/services/policy/package.json +++ b/api/services/policy/package.json @@ -31,8 +31,12 @@ "@pdc/provider-validator": "~0", "csv-stringify": "^5.6.5", "date-fns-tz": "^1.1.6", - "faker": "^5.4.0", + "faker": "^5.5.0", "lodash": "^4.17.21", - "moment": "^2.29.1" + "moment": "^2.29.1", + "uuid": "^8.3.2" + }, + "devDependencies": { + "@types/uuid": "^8.3.1" } -} +} \ No newline at end of file diff --git a/api/services/policy/src/ServiceProvider.ts b/api/services/policy/src/ServiceProvider.ts index 1e5af411bf..d7e44f3ca8 100644 --- a/api/services/policy/src/ServiceProvider.ts +++ b/api/services/policy/src/ServiceProvider.ts @@ -31,7 +31,7 @@ import { SimulateOnFutureAction } from './actions/SimulateOnFutureAction'; import { CampaignPgRepositoryProvider } from './providers/CampaignPgRepositoryProvider'; import { PolicyEngine } from './engine/PolicyEngine'; -import { MetadataProvider } from './engine/meta/MetadataProvider'; +import { MetadataRepositoryProvider } from './providers/MetadataRepositoryProvider'; import { IncentiveRepositoryProvider } from './providers/IncentiveRepositoryProvider'; import { TripRepositoryProvider } from './providers/TripRepositoryProvider'; import { TerritoryRepositoryProvider } from './providers/TerritoryRepositoryProvider'; @@ -46,7 +46,7 @@ import { SeedCommand } from './commands/SeedCommand'; commands: [PolicyProcessCommand, SeedCommand], providers: [ CampaignPgRepositoryProvider, - MetadataProvider, + MetadataRepositoryProvider, TripRepositoryProvider, PolicyEngine, IncentiveRepositoryProvider, diff --git a/api/services/policy/src/actions/ApplyAction.ts b/api/services/policy/src/actions/ApplyAction.ts index de4725e183..7221801c08 100644 --- a/api/services/policy/src/actions/ApplyAction.ts +++ b/api/services/policy/src/actions/ApplyAction.ts @@ -60,39 +60,67 @@ export class ApplyAction extends AbstractAction implements InitHookInterface { public async handle(params: ParamsInterface): Promise { if (!('campaign_id' in params)) { - await this.refreshAndDispatch(); + await this.dispatch(); return; } - await this.processCampaign(params.campaign_id); + await this.processCampaign(params.campaign_id, params.override_from); } - protected async refreshAndDispatch(): Promise { - await this.tripRepository.refresh(); + protected async dispatch(): Promise { const campaignIds = await this.tripRepository.listApplicablePoliciesId(); for (const campaign_id of campaignIds) { this.kernel.notify(handlerSignature, { campaign_id }, this.context); } } - protected async processCampaign(campaign_id: number): Promise { + protected async processCampaign(campaign_id: number, override_from?: Date): Promise { + console.debug('PROCESS CAMPAIGN', { campaign_id, override_from }); + // 1. Find campaign and start engine const campaign = this.engine.buildCampaign(await this.campaignRepository.find(campaign_id)); + // benchmark + const totalStart = new Date(); + let total = 0; + let counter = 0; + // 2. Start a cursor to find trips - const cursor = await this.tripRepository.findTripByPolicy(campaign); + const batchSize = 50; + const cursor = this.tripRepository.findTripByPolicy(campaign, batchSize, override_from); let done = false; + do { + const start = new Date(); const incentives: IncentiveInterface[] = []; const results = await cursor.next(); done = results.done; if (results.value) { for (const trip of results.value) { + // skip trip if campaign is finished + if (campaign.end_date < trip.datetime) continue; + // 3. For each trip, process + counter++; incentives.push(...(await this.engine.processStateless(campaign, trip))); } } + // 4. Save incentives + console.debug(`STORE ${incentives.length} incentives`); await this.incentiveRepository.createOrUpdateMany(incentives); + + // benchmark + const ms = new Date().getTime() - start.getTime(); + console.debug( + `[campaign ${campaign.policy_id}] ${counter} (${total}) trips done in ${ms}ms (${( + (counter / ms) * + 1000 + ).toFixed(3)}/s)`, + ); + total += counter; + counter = 0; } while (!done); + + console.debug(`TOTAL ${total} in ${new Date().getTime() - totalStart.getTime()}ms`); } } diff --git a/api/services/policy/src/actions/CreateCampaignAction.integration.spec.ts b/api/services/policy/src/actions/CreateCampaignAction.integration.spec.ts index 92bb55d0b4..6705ee0921 100644 --- a/api/services/policy/src/actions/CreateCampaignAction.integration.spec.ts +++ b/api/services/policy/src/actions/CreateCampaignAction.integration.spec.ts @@ -52,7 +52,7 @@ interface TestContext extends KernelTestInterface { const myTest = anyTest as TestInterface; -myTest.after.always.skip(async (t) => { +myTest.after.always(async (t) => { if (t.context.policy_id) { t.context.kernel .get(ServiceProvider) @@ -71,16 +71,16 @@ const { test, success, error } = handlerMacro) => { diff --git a/api/services/policy/src/actions/DeleteCampaignAction.integration.spec.ts b/api/services/policy/src/actions/DeleteCampaignAction.integration.spec.ts index a2ae3c33e3..203641dfcc 100644 --- a/api/services/policy/src/actions/DeleteCampaignAction.integration.spec.ts +++ b/api/services/policy/src/actions/DeleteCampaignAction.integration.spec.ts @@ -73,7 +73,7 @@ const { test, success } = handlerMacro { +test.before(async (t) => { const policy = await t.context.kernel .get(ServiceProvider) .get(CampaignRepositoryProviderInterfaceResolver) @@ -82,7 +82,7 @@ test.before.skip(async (t) => { t.context.policy_id = policy._id; }); -test.skip( +test( success, (t: ExecutionContext) => (({ diff --git a/api/services/policy/src/actions/FinalizeAction.ts b/api/services/policy/src/actions/FinalizeAction.ts index 1ae24cccd6..d55c6a05b7 100644 --- a/api/services/policy/src/actions/FinalizeAction.ts +++ b/api/services/policy/src/actions/FinalizeAction.ts @@ -14,7 +14,7 @@ import { internalOnlyMiddlewares } from '@pdc/provider-middleware'; import { IncentiveRepositoryProviderInterfaceResolver, CampaignRepositoryProviderInterfaceResolver, - TripRepositoryProviderInterfaceResolver, + IncentiveStatusEnum, } from '../interfaces'; @handler({ ...handlerConfig, middlewares: [...internalOnlyMiddlewares(handlerConfig.service)] }) @@ -22,7 +22,6 @@ export class FinalizeAction extends AbstractAction implements InitHookInterface constructor( private campaignRepository: CampaignRepositoryProviderInterfaceResolver, private incentiveRepository: IncentiveRepositoryProviderInterfaceResolver, - private tripRepository: TripRepositoryProviderInterfaceResolver, private engine: PolicyEngine, private kernel: KernelInterfaceResolver, ) { @@ -48,12 +47,11 @@ export class FinalizeAction extends AbstractAction implements InitHookInterface public async handle(params: ParamsInterface): Promise { // Get last day of previous month - const before = new Date(); - before.setDate(1); - before.setHours(0, 0, 0, -1); + const defaultTo = new Date(); + defaultTo.setDate(1); + defaultTo.setHours(0, 0, 0, -1); - // Refresh table - await this.tripRepository.refresh(); + const to = params.to ?? defaultTo; // Update incentive on cancelled carpool await this.incentiveRepository.disableOnCanceledTrip(); @@ -61,20 +59,35 @@ export class FinalizeAction extends AbstractAction implements InitHookInterface const policyMap: Map = new Map(); // Apply internal restriction of policies - await this.processStatefulCampaigns(policyMap, before); + console.debug(`START processing stateful campaigns`); + await this.processStatefulCampaigns(policyMap, to, params.from); + console.debug(`DONE processing stateful campaigns`); // TODO: Apply external restriction (order) of policies // Lock all - await this.incentiveRepository.lockAll(before); + console.debug(`LOCK_ALL incentives to: ${to}`); + await this.incentiveRepository.lockAll(to); + console.debug('DONE locking'); } - protected async processStatefulCampaigns(policyMap: Map, before: Date): Promise { + protected async processStatefulCampaigns( + policyMap: Map, + to: Date, + from?: Date, + ): Promise { // 1. Start a cursor to find incentives - const cursor = await this.incentiveRepository.findDraftIncentive(before); + const cursor = this.incentiveRepository.findDraftIncentive(to, 100, from); let done = false; do { - const updatedIncentives: { carpool_id: number; policy_id: number; amount: number }[] = []; + const start = new Date().getTime(); + + const updatedIncentives: { + carpool_id: number; + policy_id: number; + amount: number; + status: IncentiveStatusEnum; + }[] = []; const results = await cursor.next(); done = results.done; if (results.value) { @@ -93,7 +106,15 @@ export class FinalizeAction extends AbstractAction implements InitHookInterface } } // 4. Update incentives - await this.incentiveRepository.updateManyAmount(updatedIncentives); + await this.incentiveRepository.updateManyAmount(updatedIncentives, IncentiveStatusEnum.Valitated); + + const duration = new Date().getTime() - start; + console.debug( + `Finalized ${updatedIncentives.length} incentives in ${duration}ms (${( + (updatedIncentives.length / duration) * + 1000 + ).toFixed(3)}/s)`, + ); } while (!done); } } diff --git a/api/services/policy/src/actions/LaunchCampaignAction.integration.spec.ts b/api/services/policy/src/actions/LaunchCampaignAction.integration.spec.ts index 0eb3dff323..af872e3e1e 100644 --- a/api/services/policy/src/actions/LaunchCampaignAction.integration.spec.ts +++ b/api/services/policy/src/actions/LaunchCampaignAction.integration.spec.ts @@ -73,7 +73,7 @@ const { test, success, error } = handlerMacro { +test.before(async (t) => { const policy = await t.context.kernel .get(ServiceProvider) .get(CampaignRepositoryProviderInterfaceResolver) @@ -82,7 +82,7 @@ test.before.skip(async (t) => { t.context.policy_id = policy._id; }); -test.serial.skip( +test.serial( success, (t: ExecutionContext) => (({ @@ -94,7 +94,7 @@ test.serial.skip( mockContext(['incentive-campaign.create', 'territory.policy.launch']), ); -test.serial.skip( +test.serial( error, (t: ExecutionContext) => (({ diff --git a/api/services/policy/src/actions/ListCampaignAction.integration.spec.ts b/api/services/policy/src/actions/ListCampaignAction.integration.spec.ts index 4b811ce40f..67dcf74ef6 100644 --- a/api/services/policy/src/actions/ListCampaignAction.integration.spec.ts +++ b/api/services/policy/src/actions/ListCampaignAction.integration.spec.ts @@ -73,7 +73,7 @@ const { test, success } = handlerMacro { +test.before(async (t) => { const policy = await t.context.kernel .get(ServiceProvider) .get(CampaignRepositoryProviderInterfaceResolver) @@ -82,7 +82,7 @@ test.before.skip(async (t) => { t.context.policy_id = policy._id; }); -test.skip( +test( success, { territory_id: territory }, (response: ResultInterface, t: ExecutionContext) => { diff --git a/api/services/policy/src/actions/PatchCampaignAction.integration.spec.ts b/api/services/policy/src/actions/PatchCampaignAction.integration.spec.ts index 8c581b19ca..0fd250422d 100644 --- a/api/services/policy/src/actions/PatchCampaignAction.integration.spec.ts +++ b/api/services/policy/src/actions/PatchCampaignAction.integration.spec.ts @@ -54,7 +54,7 @@ interface TestContext extends KernelTestInterface { const myTest = anyTest as TestInterface; -myTest.after.always.skip(async (t) => { +myTest.after.always(async (t) => { if (t.context.policy_id) { t.context.kernel .get(ServiceProvider) @@ -73,7 +73,7 @@ const { test, success } = handlerMacro { +test.before(async (t) => { const policy = await t.context.kernel .get(ServiceProvider) .get(CampaignRepositoryProviderInterfaceResolver) @@ -82,7 +82,7 @@ test.before.skip(async (t) => { t.context.policy_id = policy._id; }); -test.skip( +test( success, (t: ExecutionContext) => (({ diff --git a/api/services/policy/src/actions/TemplatesCampaignAction.integration.spec.ts b/api/services/policy/src/actions/TemplatesCampaignAction.integration.spec.ts index 88bf9f65da..54bee16fa3 100644 --- a/api/services/policy/src/actions/TemplatesCampaignAction.integration.spec.ts +++ b/api/services/policy/src/actions/TemplatesCampaignAction.integration.spec.ts @@ -73,7 +73,7 @@ const { test, success } = handlerMacro { +test.before(async (t) => { const policy = await t.context.kernel .get(ServiceProvider) .get(CampaignRepositoryProviderInterfaceResolver) @@ -82,7 +82,7 @@ test.before.skip(async (t) => { t.context.policy_id = policy._id; }); -test.skip( +test( success, {}, (response: ResultInterface, t: ExecutionContext) => { diff --git a/api/services/policy/src/engine/PolicyEngine.spec.ts b/api/services/policy/src/engine/PolicyEngine.spec.ts index ea9bdc20dc..7c90080afb 100644 --- a/api/services/policy/src/engine/PolicyEngine.spec.ts +++ b/api/services/policy/src/engine/PolicyEngine.spec.ts @@ -2,9 +2,9 @@ import test from 'ava'; import { faker } from './helpers/faker'; import { CampaignInterface } from '../interfaces'; -import { MetadataProviderInterfaceResolver, MetaInterface } from './interfaces'; +import { MetadataRepositoryProviderInterfaceResolver, MetadataWrapperInterface } from '../interfaces'; import { PolicyEngine } from './PolicyEngine'; -import { MetadataWrapper } from './meta/MetadataWrapper'; +import { MetadataWrapper } from '../providers/MetadataWrapper'; import { RuleInterface } from '../shared/common/interfaces/RuleInterface'; function setup(rules: RuleInterface[] = []): { engine: PolicyEngine; start: Date; fakeCampaign: CampaignInterface } { @@ -59,11 +59,11 @@ function setup(rules: RuleInterface[] = []): { engine: PolicyEngine; start: Date ], }; - class CampaignMetadataRepositoryProvider extends MetadataProviderInterfaceResolver { - async get(id: number, keys = ['default']): Promise { + class CampaignMetadataRepositoryProvider extends MetadataRepositoryProviderInterfaceResolver { + async get(id: number, keys = ['default']): Promise { return meta; } - async set(id: number, meta: MetaInterface): Promise { + async set(id: number, meta: MetadataWrapperInterface): Promise { return; } } @@ -86,42 +86,6 @@ test('should boot', async (t) => { t.is(result[0].amount, ((trip[0].distance / 1000) * fakeCampaign.rules[0][1].parameters) as number); }); -test('should work and distribute incentive', async (t) => { - const { engine, fakeCampaign } = setup(); - const trip = faker.trip([ - { - carpool_id: 2, - is_driver: true, - distance: 1000, - }, - { - carpool_id: 1, - is_driver: false, - distance: 1000, - }, - { - carpool_id: 3, - is_driver: false, - distance: 3000, - }, - { - carpool_id: 4, - is_driver: true, - distance: 4000, - }, - ]); - const campaign = engine.buildCampaign(fakeCampaign); - const result = await engine.process(campaign, trip); - - t.log(result); - t.true(Array.isArray(result)); - t.is(result.length, 4); - t.is(result.find((p) => p.carpool_id === 1).amount, (1000 / 1000) * 10); - t.is(result.find((p) => p.carpool_id === 2).amount, ((4000 / 1000) * 20) / 2); - t.is(result.find((p) => p.carpool_id === 3).amount, (3000 / 1000) * 10); - t.is(result.find((p) => p.carpool_id === 4).amount, ((4000 / 1000) * 20) / 2); -}); - test('should work with amount restriction', async (t) => { const { engine, fakeCampaign } = setup([ { @@ -134,43 +98,50 @@ test('should work with amount restriction', async (t) => { }, }, ]); - const trip = faker.trip([ - { - carpool_id: 1, - is_driver: false, - distance: 1000, - identity_uuid: 'passenger_1', - }, - { - carpool_id: 2, - is_driver: true, - distance: 2000, - identity_uuid: 'driver', - }, - { - carpool_id: 3, - is_driver: false, - distance: 3000, - identity_uuid: 'passenger_2', - }, - { - carpool_id: 4, - is_driver: true, - distance: 4000, - identity_uuid: 'driver', - }, - ]); const campaign = engine.buildCampaign(fakeCampaign); - const result = await engine.process(campaign, trip); - + const result = await engine.process( + campaign, + faker.trip([ + { + carpool_id: 1, + is_driver: false, + distance: 1000, + identity_uuid: 'passenger_1', + }, + { + carpool_id: 2, + is_driver: true, + distance: 2000, + identity_uuid: 'driver', + }, + ]), + ); + const result2 = await engine.process( + campaign, + faker.trip([ + { + carpool_id: 3, + is_driver: false, + distance: 3000, + identity_uuid: 'passenger_2', + }, + { + carpool_id: 4, + is_driver: true, + distance: 4000, + identity_uuid: 'driver', + }, + ]), + ); t.log(result); t.true(Array.isArray(result)); - t.is(result.length, 4); + t.is(result.length, 2); + t.is(result2.length, 2); t.is(result.find((p) => p.carpool_id === 1).amount, (1000 / 1000) * 10); t.is(result.find((p) => p.carpool_id === 2).amount, ((4000 / 1000) * 20) / 2); - t.is(result.find((p) => p.carpool_id === 3).amount, (3000 / 1000) * 10); - t.is(result.find((p) => p.carpool_id === 4).amount, 10); + t.is(result2.find((p) => p.carpool_id === 3).amount, (3000 / 1000) * 10); + t.is(result2.find((p) => p.carpool_id === 4).amount, 10); }); test('should work with trip restriction', async (t) => { @@ -185,40 +156,49 @@ test('should work with trip restriction', async (t) => { }, }, ]); - const trip = faker.trip([ - { - carpool_id: 1, - is_driver: false, - distance: 1000, - identity_uuid: 'passenger_1', - }, - { - carpool_id: 2, - is_driver: true, - distance: 2000, - identity_uuid: 'driver', - }, - { - carpool_id: 3, - is_driver: false, - distance: 3000, - identity_uuid: 'passenger_2', - }, - { - carpool_id: 4, - is_driver: true, - distance: 4000, - identity_uuid: 'driver', - }, - ]); + const campaign = engine.buildCampaign(fakeCampaign); - const result = await engine.process(campaign, trip); + const result = await engine.process( + campaign, + faker.trip([ + { + carpool_id: 1, + is_driver: false, + distance: 1000, + identity_uuid: 'passenger_1', + }, + { + carpool_id: 2, + is_driver: true, + distance: 2000, + identity_uuid: 'driver', + }, + ]), + ); + const result2 = await engine.process( + campaign, + faker.trip([ + { + carpool_id: 3, + is_driver: false, + distance: 3000, + identity_uuid: 'passenger_2', + }, + { + carpool_id: 4, + is_driver: true, + distance: 4000, + identity_uuid: 'driver', + }, + ]), + ); t.log(result); t.true(Array.isArray(result)); - t.is(result.length, 4); + t.is(result.length, 2); + t.is(result2.length, 2); t.is(result.find((p) => p.carpool_id === 1).amount, (1000 / 1000) * 10); - t.is(result.find((p) => p.carpool_id === 2).amount, ((4000 / 1000) * 20) / 2); - t.is(result.find((p) => p.carpool_id === 3).amount, (3000 / 1000) * 10); - t.is(result.find((p) => p.carpool_id === 4).amount, ((4000 / 1000) * 20) / 2); + t.is(result.find((p) => p.carpool_id === 2).amount, (2000 / 1000) * 20); + t.is(result2.find((p) => p.carpool_id === 3).amount, (3000 / 1000) * 10); + t.is(result2.find((p) => p.carpool_id === 4).amount, (4000 / 1000) * 20 * 0); }); diff --git a/api/services/policy/src/engine/PolicyEngine.ts b/api/services/policy/src/engine/PolicyEngine.ts index 34cbbc0692..c87f498594 100644 --- a/api/services/policy/src/engine/PolicyEngine.ts +++ b/api/services/policy/src/engine/PolicyEngine.ts @@ -1,15 +1,18 @@ import { provider } from '@ilos/common'; - -import { CampaignInterface, IncentiveInterface, TripInterface } from '../interfaces'; -import { MetadataProviderInterfaceResolver } from './interfaces'; - -import { TripIncentives } from './TripIncentives'; +import { + CampaignInterface, + IncentiveInterface, + IncentiveStatusEnum, + MetadataRepositoryProviderInterfaceResolver, + TripInterface, +} from '../interfaces'; +import { MetadataWrapper } from '../providers/MetadataWrapper'; import { ProcessableCampaign } from './ProcessableCampaign'; -import { MetadataWrapper } from './meta/MetadataWrapper'; +import { TripIncentives } from './TripIncentives'; @provider() export class PolicyEngine { - constructor(protected metaRepository: MetadataProviderInterfaceResolver) {} + constructor(protected metaRepository: MetadataRepositoryProviderInterfaceResolver) {} public buildCampaign(campaign: CampaignInterface): ProcessableCampaign { return new ProcessableCampaign(campaign); @@ -23,17 +26,17 @@ export class PolicyEngine { const incentive = pc.apply(ctx, meta); tripIncentives.addIncentive(incentive); } - return tripIncentives.distributeDriverIncentives().getIncentives(); + return tripIncentives.getIncentives(); } public async processStateful( pc: ProcessableCampaign, incentive: IncentiveInterface, - ): Promise<{ carpool_id: number; policy_id: number; amount: number }> { + ): Promise<{ carpool_id: number; policy_id: number; amount: number; status: IncentiveStatusEnum }> { const keys = pc.getMetaKeys(incentive); - const meta = await this.metaRepository.get(pc.policy_id, keys); + const meta = await this.metaRepository.get(pc.policy_id, keys, incentive.datetime); const result = pc.applyStateful(incentive, meta); - await this.metaRepository.set(pc.policy_id, meta); + await this.metaRepository.set(pc.policy_id, meta, incentive.datetime); return result; } diff --git a/api/services/policy/src/engine/ProcessableCampaign.ts b/api/services/policy/src/engine/ProcessableCampaign.ts index fe2781d8bb..1912a5687e 100644 --- a/api/services/policy/src/engine/ProcessableCampaign.ts +++ b/api/services/policy/src/engine/ProcessableCampaign.ts @@ -1,9 +1,15 @@ -import { MetaInterface, RuleHandlerContextInterface } from './interfaces'; +import { RuleHandlerContextInterface } from './interfaces'; import { NotApplicableTargetException } from './exceptions/NotApplicableTargetException'; import { RuleSet } from './RuleSet'; import { StatefulRuleSet } from './set/StatefulRuleSet'; -import { IncentiveStatusEnum, IncentiveStateEnum, IncentiveInterface, CampaignInterface } from '../interfaces'; -import { MetadataWrapper } from './meta/MetadataWrapper'; +import { + IncentiveStatusEnum, + IncentiveStateEnum, + IncentiveInterface, + CampaignInterface, + MetadataWrapperInterface, +} from '../interfaces'; +import { MetadataWrapper } from '../providers/MetadataWrapper'; export class ProcessableCampaign { public readonly policy_id: number; @@ -39,7 +45,7 @@ export class ProcessableCampaign { return [this.globalSet, ...this.ruleSets].map((s) => s.hasStatefulRule).reduce((r, s) => r || s, false); } - applyStateful(incentive: IncentiveInterface, meta: MetaInterface): IncentiveInterface { + applyStateful(incentive: IncentiveInterface, meta: MetadataWrapperInterface): IncentiveInterface { let amount = this.globalSet.applyStateful(incentive, meta); for (const ruleSet of this.ruleSets) { amount = ruleSet.applyStateful( @@ -56,7 +62,10 @@ export class ProcessableCampaign { }; } - apply(context: RuleHandlerContextInterface, meta: MetaInterface = new MetadataWrapper()): IncentiveInterface { + apply( + context: RuleHandlerContextInterface, + meta: MetadataWrapperInterface = new MetadataWrapper(), + ): IncentiveInterface { let result = 0; let incentiveState: Map = new Map(); diff --git a/api/services/policy/src/engine/RuleSet.ts b/api/services/policy/src/engine/RuleSet.ts index b5c30a88c9..acd0d16a7b 100644 --- a/api/services/policy/src/engine/RuleSet.ts +++ b/api/services/policy/src/engine/RuleSet.ts @@ -10,11 +10,10 @@ import { RuleHandlerParamsInterface, AppliableRuleInterface, StatefulRuleSetInterface, - MetaInterface, StaticRuleInterface, MetaRuleInterface, } from './interfaces'; -import { IncentiveInterface } from '../interfaces'; +import { IncentiveInterface, MetadataWrapperInterface } from '../interfaces'; import { UnprocessableRuleSetException } from './exceptions/UnprocessableRuleSetException'; import { META } from './helpers/type'; @@ -64,7 +63,7 @@ export class RuleSet { }, []); } - apply(context: RuleHandlerParamsInterface, meta: MetaInterface): Map { + apply(context: RuleHandlerParamsInterface, meta: MetadataWrapperInterface): Map { let { result, ...ctx } = context; this.filterSet.filter(ctx); const initialState = this.statefulSet.buildInitialState(ctx, meta); @@ -81,7 +80,7 @@ export class RuleSet { return this.statefulSet.listStateKeys(incentive); } - applyStateful(incentive: IncentiveInterface, meta: MetaInterface): number { + applyStateful(incentive: IncentiveInterface, meta: MetadataWrapperInterface): number { return this.statefulSet.apply(incentive, meta); } } diff --git a/api/services/policy/src/engine/TripIncentives.ts b/api/services/policy/src/engine/TripIncentives.ts index 76dbb2e276..f7681e179f 100644 --- a/api/services/policy/src/engine/TripIncentives.ts +++ b/api/services/policy/src/engine/TripIncentives.ts @@ -19,38 +19,4 @@ export class TripIncentives { public getIncentives(): IncentiveInterface[] { return this.incentives; } - - protected findDriverIncentives(): IncentiveInterface[] { - const driverCarpoolIds = this.trip.filter((p) => p.is_driver).map((p) => p.carpool_id); - return this.incentives.filter((i) => driverCarpoolIds.indexOf(i.carpool_id) > -1); - } - - protected pickDriverIncentive(): IncentiveInterface | null { - return this.findDriverIncentives().sort((a, b) => (a.amount > b.amount ? -1 : a.amount < b.amount ? 1 : 0))[0]; - } - - public distributeDriverIncentives(): TripIncentives { - const driverIncentive = this.pickDriverIncentive(); - if (!driverIncentive) { - return this; - } - const { amount: amountToDistribute, result: resultToDistribute } = driverIncentive; - const toReplaceIncentives = this.findDriverIncentives(); - - const amountToDistributeEach = Math.round(amountToDistribute / toReplaceIncentives.length); - const resultToDistributeEach = Math.round(resultToDistribute / toReplaceIncentives.length); - - for (const incentive of toReplaceIncentives) { - const index = this.incentives.findIndex((i) => i.carpool_id === incentive.carpool_id); - if (index > -1) { - this.incentives[index] = { - ...incentive, - amount: amountToDistributeEach, - result: resultToDistributeEach, - }; - } - } - - return this; - } } diff --git a/api/services/policy/src/engine/faker/FakerEngine.ts b/api/services/policy/src/engine/faker/FakerEngine.ts index 8a55b358ed..77c2fddee2 100644 --- a/api/services/policy/src/engine/faker/FakerEngine.ts +++ b/api/services/policy/src/engine/faker/FakerEngine.ts @@ -25,8 +25,6 @@ export class FakerEngine { has_travel_pass: false, operator_id: 1, operator_class: 'C', - start_insee: '91377', - end_insee: '91377', seats: 1, duration: 600, distance: 5000, diff --git a/api/services/policy/src/engine/faker/InMemoryMetadataProvider.ts b/api/services/policy/src/engine/faker/InMemoryMetadataProvider.ts index 3517f1dba7..1823858ca9 100644 --- a/api/services/policy/src/engine/faker/InMemoryMetadataProvider.ts +++ b/api/services/policy/src/engine/faker/InMemoryMetadataProvider.ts @@ -1,17 +1,17 @@ -import { MetadataProviderInterfaceResolver, MetaInterface } from '../interfaces'; -import { MetadataWrapper } from '../meta/MetadataWrapper'; +import { MetadataRepositoryProviderInterfaceResolver, MetadataWrapperInterface } from '../../interfaces'; +import { MetadataWrapper } from '../../providers/MetadataWrapper'; -export class InMemoryMetadataProvider extends MetadataProviderInterfaceResolver { - protected metamap: Map = new Map(); +export class InMemoryMetadataProvider extends MetadataRepositoryProviderInterfaceResolver { + protected metamap: Map = new Map(); - async get(id: number, keys = ['default']): Promise { + async get(id: number, keys = ['default']): Promise { if (!this.metamap.has(id)) { await this.set(id, new MetadataWrapper(id, [])); } return this.metamap.get(id); } - async set(id: number, meta: MetaInterface): Promise { + async set(id: number, meta: MetadataWrapperInterface): Promise { this.metamap.set(id, meta); } } diff --git a/api/services/policy/src/engine/helpers/faker.ts b/api/services/policy/src/engine/helpers/faker.ts index 2d6956927d..a5aa09fd95 100644 --- a/api/services/policy/src/engine/helpers/faker.ts +++ b/api/services/policy/src/engine/helpers/faker.ts @@ -11,8 +11,6 @@ const basePerson: PersonInterface = { operator_id: 1, operator_class: 'C', is_driver: false, - start_insee: '91377', - end_insee: '91377', seats: 1, duration: 600, distance: 5000, diff --git a/api/services/policy/src/engine/interfaces/MetadataProviderInterface.ts b/api/services/policy/src/engine/interfaces/MetadataProviderInterface.ts deleted file mode 100644 index 0135af0b76..0000000000 --- a/api/services/policy/src/engine/interfaces/MetadataProviderInterface.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { MetaInterface } from './MetaInterface'; - -export interface MetadataProviderInterface { - get(id: number, keys?: string[]): Promise; - set(id: number, metadata: MetaInterface): Promise; -} - -export abstract class MetadataProviderInterfaceResolver implements MetadataProviderInterface { - async get(id: number, keys?: string[]): Promise { - throw new Error(); - } - - async set(id: number, metadata: MetaInterface): Promise { - throw new Error(); - } -} diff --git a/api/services/policy/src/engine/interfaces/RuleInterface.ts b/api/services/policy/src/engine/interfaces/RuleInterface.ts index 297c74fcb0..300667478f 100644 --- a/api/services/policy/src/engine/interfaces/RuleInterface.ts +++ b/api/services/policy/src/engine/interfaces/RuleInterface.ts @@ -1,5 +1,5 @@ import { TripInterface, PersonInterface, IncentiveInterface } from '../../interfaces'; -import { MetaInterface } from './MetaInterface'; +import { MetadataWrapperInterface } from '../../interfaces'; import { priority } from '../helpers/priority'; import { type } from '../helpers/type'; @@ -37,16 +37,16 @@ export interface AppliableRuleInterface { export interface StatefulRuleInterface { readonly uuid: string; - getStateKey(context: RuleHandlerContextInterface, metaGetter: MetaInterface): string | undefined; + getStateKey(context: RuleHandlerContextInterface, metaGetter: MetadataWrapperInterface): string | undefined; apply(result: number, state: number): number; setState(result: number, state: number): number; } export interface StatefulRuleSetInterface { length: number; - buildInitialState(context: RuleHandlerContextInterface, meta: MetaInterface): Map; + buildInitialState(context: RuleHandlerContextInterface, meta: MetadataWrapperInterface): Map; listStateKeys(incentive: IncentiveInterface): string[]; - apply(incentive: IncentiveInterface, meta: MetaInterface): number; + apply(incentive: IncentiveInterface, meta: MetadataWrapperInterface): number; } export interface MetaRuleInterface { diff --git a/api/services/policy/src/engine/interfaces/index.ts b/api/services/policy/src/engine/interfaces/index.ts index 83e747e8b9..7d9b73c1d4 100644 --- a/api/services/policy/src/engine/interfaces/index.ts +++ b/api/services/policy/src/engine/interfaces/index.ts @@ -1,3 +1 @@ export * from './RuleInterface'; -export * from './MetaInterface'; -export * from './MetadataProviderInterface'; diff --git a/api/services/policy/src/engine/meta/MetadataProvider.integration.spec.ts b/api/services/policy/src/engine/meta/MetadataProvider.integration.spec.ts deleted file mode 100644 index 0fb4ad5ea0..0000000000 --- a/api/services/policy/src/engine/meta/MetadataProvider.integration.spec.ts +++ /dev/null @@ -1,104 +0,0 @@ -import anyTest, { TestInterface } from 'ava'; -import { PostgresConnection } from '@ilos/connection-postgres'; - -import { MetadataProvider } from './MetadataProvider'; -import { MetadataWrapper } from './MetadataWrapper'; - -interface TestContext { - repository: MetadataProvider; - connection: PostgresConnection; - policyId: number; -} -const test = anyTest as TestInterface; - -test.before.skip(async (t) => { - t.context.policyId = 0; - t.context.connection = new PostgresConnection({ - connectionString: - 'APP_POSTGRES_URL' in process.env - ? process.env.APP_POSTGRES_URL - : 'postgresql://postgres:postgres@localhost:5432/local', - }); - - await t.context.connection.up(); - t.context.repository = new MetadataProvider(t.context.connection); -}); - -test.beforeEach.skip(async (t) => { - await t.context.connection.getClient().query({ - text: `DELETE from ${t.context.repository.table} WHERE policy_id = $1`, - values: [t.context.policyId], - }); -}); - -test.after.always.skip(async (t) => { - // clean db - await t.context.connection.getClient().query({ - text: `DELETE from ${t.context.repository.table} WHERE policy_id = $1`, - values: [t.context.policyId], - }); - - // shutdown connection - await t.context.connection.down(); -}); - -test.serial.skip('should always return a metadata wrapper', async (t) => { - const meta = await t.context.repository.get(t.context.policyId); - t.true(meta instanceof MetadataWrapper); - t.is(meta.keys().length, 0); -}); - -test.serial.skip('should create metadata wrapper on database', async (t) => { - const meta = await t.context.repository.get(t.context.policyId); - t.true(meta instanceof MetadataWrapper); - t.is(meta.keys().length, 0); - meta.set('toto', 0); - - await t.context.repository.set(t.context.policyId, meta); - - const dbResult = await t.context.connection.getClient().query({ - text: `SELECT value from ${t.context.repository.table} WHERE policy_id = $1 AND key = $2`, - values: [t.context.policyId, 'toto'], - }); - - t.log(dbResult.rows); - t.is(dbResult.rowCount, 1); - t.deepEqual(dbResult.rows, [ - { - value: 0, - }, - ]); -}); - -test.serial.skip('should update metadata wrapper on database', async (t) => { - const meta = await t.context.repository.get(t.context.policyId); - t.true(meta instanceof MetadataWrapper); - t.is(meta.keys().length, 0); - meta.set('toto', 0); - await t.context.repository.set(t.context.policyId, meta); - - const meta2 = await t.context.repository.get(t.context.policyId); - t.is(meta2.keys().length, 1); - t.is(meta2.get('toto'), 0); - meta2.set('toto', 1); - meta2.set('tata', 100); - - await t.context.repository.set(t.context.policyId, meta2); - - const dbResult = await t.context.connection.getClient().query({ - text: `SELECT key, value from ${t.context.repository.table} WHERE policy_id = $1 ORDER BY key`, - values: [t.context.policyId], - }); - - t.is(dbResult.rowCount, 2); - t.deepEqual(dbResult.rows, [ - { - key: 'tata', - value: 100, - }, - { - key: 'toto', - value: 1, - }, - ]); -}); diff --git a/api/services/policy/src/engine/meta/MetadataProvider.ts b/api/services/policy/src/engine/meta/MetadataProvider.ts deleted file mode 100644 index 89f55fe8ab..0000000000 --- a/api/services/policy/src/engine/meta/MetadataProvider.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { PostgresConnection } from '@ilos/connection-postgres'; -import { provider } from '@ilos/common'; - -import { MetadataProviderInterface, MetadataProviderInterfaceResolver } from '../interfaces/MetadataProviderInterface'; -import { MetadataWrapper } from './MetadataWrapper'; -import { MetaInterface } from '../interfaces'; - -@provider({ - identifier: MetadataProviderInterfaceResolver, -}) -export class MetadataProvider implements MetadataProviderInterface { - public readonly table = 'policy.policy_metas'; - - constructor(protected connection: PostgresConnection) {} - - async get(id: number, keys: string[] = []): Promise { - const query: { - rowMode: string; - text: string; - values: any[]; - } = { - rowMode: 'array', - text: ` - SELECT - key, - value - FROM ${this.table} - WHERE - policy_id = $1 - `, - values: [id], - }; - - if (keys.length > 0) { - query.text += ' AND key = ANY($2::varchar[])'; - query.values.push(keys); - } - - const result = await this.connection.getClient().query(query); - - return new MetadataWrapper(id, result.rows); - } - - async set(policyId: number, metadata: MetaInterface): Promise { - const keys = metadata.keys(); - const values = metadata.values(); - const policyIds = new Array(keys.length).fill(policyId); - const query = { - text: ` - INSERT INTO ${this.table} (policy_id, key, value) - SELECT * FROM UNNEST($1::int[], $2::varchar[], $3::json[]) - ON CONFLICT (policy_id, key) - DO UPDATE SET - value = excluded.value - `, - values: [policyIds, keys, values], - }; - - await this.connection.getClient().query(query); - return; - } -} diff --git a/api/services/policy/src/engine/rules/AbstractStatefulRule.ts b/api/services/policy/src/engine/rules/AbstractStatefulRule.ts index 0cf42c5cd1..b730455ad0 100644 --- a/api/services/policy/src/engine/rules/AbstractStatefulRule.ts +++ b/api/services/policy/src/engine/rules/AbstractStatefulRule.ts @@ -1,7 +1,7 @@ import { LOWEST, priority } from '../helpers/priority'; import { type, STATEFUL } from '../helpers/type'; import { StatefulRuleInterface, RuleHandlerContextInterface } from '../interfaces'; -import { MetaInterface } from '../interfaces'; +import { MetadataWrapperInterface } from '../../interfaces'; interface StatefulParametersDefaultInterface { uuid: string; @@ -22,7 +22,7 @@ export abstract class AbstractStatefulRule

{ - const { trip } = setup(); - const rule = new InseeBlacklistFilter([ - { - start: ['A'], - end: ['A'], - }, - ]); - const err = await t.throwsAsync(async () => - rule.filter({ - trip, - stack: [], - person: trip[0], - }), - ); - t.is(err.message, InseeBlacklistFilter.description); -}); - -test('should do nothing if start and end is in blacklist with AND operator', async (t) => { - const { trip } = setup(); - const rule = new InseeBlacklistFilter([ - { - start: ['A'], - end: ['A'], - }, - ]); - await t.notThrowsAsync(async () => - rule.filter({ - trip, - stack: [], - person: trip[1], - }), - ); -}); - -test('should throw error if start or end is in blacklist with OR operator', async (t) => { - const { trip } = setup(); - const rule = new InseeBlacklistFilter([ - { start: ['A'], end: [] }, - { start: [], end: ['A'] }, - ]); - - const err = await t.throwsAsync(async () => - rule.filter({ - trip, - stack: [], - person: trip[1], - }), - ); - t.is(err.message, InseeBlacklistFilter.description); -}); - -test('should throw error if start and end is in whitelist with AND operator', async (t) => { - const { trip } = setup(); - const rule = new InseeWhitelistFilter([ - { - start: ['A'], - end: ['A'], - }, - ]); - - const err = await t.throwsAsync(async () => - rule.filter({ - trip, - stack: [], - person: trip[1], - }), - ); - t.is(err.message, InseeWhitelistFilter.description); -}); - -test('should do nothing if start and end is in whitelist with AND operator', async (t) => { - const { trip } = setup(); - const rule = new InseeWhitelistFilter([ - { - start: ['A'], - end: ['A'], - }, - ]); - await t.notThrowsAsync(async () => - rule.filter({ - trip, - stack: [], - person: trip[0], - }), - ); -}); - -test('should do nothin if start or end is in whitelist with OR operator', async (t) => { - const { trip } = setup(); - const rule = new InseeWhitelistFilter([ - { start: ['A'], end: [] }, - { start: [], end: ['A'] }, - ]); - await t.notThrowsAsync(async () => - rule.filter({ - trip, - stack: [], - person: trip[1], - }), - ); -}); diff --git a/api/services/policy/src/engine/rules/filters/InseeFilter.ts b/api/services/policy/src/engine/rules/filters/InseeFilter.ts index 8e121d436e..5b75dea4c8 100644 --- a/api/services/policy/src/engine/rules/filters/InseeFilter.ts +++ b/api/services/policy/src/engine/rules/filters/InseeFilter.ts @@ -72,7 +72,9 @@ export class InseeBlacklistFilter extends InseeFilter { blacklisted = true; } } + if (blacklisted) { + console.debug(`blacklisted: ${ctx.person.start_insee} to ${ctx.person.end_insee}`); throw new NotApplicableTargetException(InseeBlacklistFilter.description); } } diff --git a/api/services/policy/src/engine/rules/modifiers/PerSeatModifier.spec.ts b/api/services/policy/src/engine/rules/modifiers/PerSeatModifier.spec.ts deleted file mode 100644 index a3e44bf021..0000000000 --- a/api/services/policy/src/engine/rules/modifiers/PerSeatModifier.spec.ts +++ /dev/null @@ -1,35 +0,0 @@ -import test from 'ava'; - -import { faker } from '../../helpers/faker'; -import { PerSeatModifier } from './PerSeatModifier'; -import { TripInterface } from '../../../interfaces'; - -function setup(): { rule: PerSeatModifier; trip: TripInterface } { - const rule = new PerSeatModifier(); - - const trip = faker.trip([ - { seats: 0, is_driver: true }, - { seats: 5, is_driver: false }, - ]); - return { rule, trip }; -} -test('should multiply result by number of seat', async (t) => { - const { rule, trip } = setup(); - const context = { - trip, - stack: [], - result: 10, - person: trip[0], - }; - await rule.apply(context); - t.is(context.result, 10); - - const context2 = { - trip, - stack: [], - result: 10, - person: trip[1], - }; - await rule.apply(context2); - t.is(context2.result, 50); -}); diff --git a/api/services/policy/src/engine/rules/modifiers/PerSeatModifier.ts b/api/services/policy/src/engine/rules/modifiers/PerSeatModifier.ts index 47ee93d792..8e302d054b 100644 --- a/api/services/policy/src/engine/rules/modifiers/PerSeatModifier.ts +++ b/api/services/policy/src/engine/rules/modifiers/PerSeatModifier.ts @@ -1,11 +1,12 @@ import { RuleHandlerContextInterface } from '../../interfaces'; import { ModifierRule } from '../ModifierRule'; +// This rule is deprecated, please migrate to per_passenger_modifier export class PerSeatModifier extends ModifierRule { static readonly slug: string = 'per_seat_modifier'; static readonly description: string = 'Le montant est multiplié par le nombre de sièges'; modify(ctx: RuleHandlerContextInterface, result: number): number { - return result * (ctx.person.seats || 1); + return result; } } diff --git a/api/services/policy/src/engine/rules/native/IdfmRegular.spec.ts b/api/services/policy/src/engine/rules/native/IdfmRegular.spec.ts index 12d24fa0cb..cae2cb07eb 100644 --- a/api/services/policy/src/engine/rules/native/IdfmRegular.spec.ts +++ b/api/services/policy/src/engine/rules/native/IdfmRegular.spec.ts @@ -42,36 +42,7 @@ function setup(): { }; return { policy, defaultTripParams }; } -test('case 0', async (t) => { - const { policy, defaultTripParams } = setup(); - const trip = faker.trip([ - { - ...defaultTripParams, - is_driver: true, - distance: 1000, - }, - { - ...defaultTripParams, - is_driver: false, - distance: 1000, - }, - { - ...defaultTripParams, - is_driver: false, - distance: 5000, - start_insee: '75115', - end_insee: '75116', - }, - ]); - const context = { - stack: [], - result: 0, - person: trip[0], - trip, - }; - await t.throwsAsync(async () => policy.apply(context)); -}); test('case 1', async (t) => { const { policy, defaultTripParams } = setup(); @@ -496,8 +467,6 @@ test('case 14', async (t) => { is_driver: true, distance: 18948, cost: 0, - start_insee: '75056', // would make it fail. No Paris to Paris - end_insee: '75056', }, { ...defaultTripParams, @@ -505,8 +474,6 @@ test('case 14', async (t) => { distance: 6, // makes it fail. passenger dist must be > 2000 seats: 1, cost: 0, - start_insee: '94021', - end_insee: '75056', }, ]); @@ -528,8 +495,6 @@ test('case 15', async (t) => { is_driver: true, distance: 15463, cost: 0, - start_insee: '75056', - end_insee: '91027', }, { ...defaultTripParams, @@ -537,8 +502,6 @@ test('case 15', async (t) => { distance: 15, // makes it fail. passenger dist must be > 2000 seats: 1, cost: 0, - start_insee: '75056', - end_insee: '91027', }, ]); diff --git a/api/services/policy/src/engine/rules/native/IdfmRegular.ts b/api/services/policy/src/engine/rules/native/IdfmRegular.ts index cb11d1b156..2e96976d85 100644 --- a/api/services/policy/src/engine/rules/native/IdfmRegular.ts +++ b/api/services/policy/src/engine/rules/native/IdfmRegular.ts @@ -44,12 +44,7 @@ export class IdfmRegular extends AbstractRule { p.is_over_18 !== false && // accept TRUE and NULL @issue #848 p.start_territory_id.indexOf(this.parameters.territory_id) >= 0 && // au départ p.end_territory_id.indexOf(this.parameters.territory_id) >= 0 && // et à l'arrivée de l'ile de france - p.distance >= 2000 && // trajet supérieur à 2km seulement - !( - this.parameters.paris_insee_code.indexOf(p.start_insee) >= 0 && - this.parameters.paris_insee_code.indexOf(p.end_insee) >= 0 - ), - // mais pas Paris-Paris + p.distance >= 2000, // trajet supérieur à 2km seulement ) .map((p) => ({ distance: p.distance, seats: p.seats })) .sort((p1, p2) => (p1.distance > p2.distance ? 1 : p1.distance < p2.distance ? -1 : 0)); diff --git a/api/services/policy/src/engine/rules/native/IdfmStrikeJan2020.ts b/api/services/policy/src/engine/rules/native/IdfmStrikeJan2020.ts deleted file mode 100644 index 010f78b59a..0000000000 --- a/api/services/policy/src/engine/rules/native/IdfmStrikeJan2020.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { NotApplicableTargetException } from '../../exceptions/NotApplicableTargetException'; -import { AbstractRule } from '../AbstractRule'; -import { RuleHandlerParamsInterface } from '../../interfaces'; - -interface IdfmParametersInterface { - territory_id: number; - paris_insee_code: string[]; -} - -export class IdfmStrikeJan2020 extends AbstractRule { - static readonly slug: string = 'idfm_strike_jan2020'; - static readonly description: string = 'Politique de grève IDFM janvier 2020'; - static readonly schema: { [k: string]: any } = { - type: 'object', - required: ['territory_id', 'paris_insee_code'], - properties: { - territory_id: { - type: 'number', - }, - paris_insee_code: { - type: 'array', - items: { - type: 'string', - }, - }, - }, - }; - apply(ctx: RuleHandlerParamsInterface): void { - if (!ctx.person.is_driver) { - throw new NotApplicableTargetException(IdfmStrikeJan2020.slug); - } - - const eligibleJourneys = ctx.trip - .filter( - (p) => - !p.is_driver && - p.is_over_18 !== false && // accept TRUE and NULL @issue #848 - p.start_territory_id.indexOf(this.parameters.territory_id) >= 0 && // au départ - p.end_territory_id.indexOf(this.parameters.territory_id) >= 0 && // et à l'arrivée de l'ile de france - p.distance >= 2000 && // trajet supérieur à 2km seulement - !( - this.parameters.paris_insee_code.indexOf(p.start_insee) >= 0 && - this.parameters.paris_insee_code.indexOf(p.end_insee) >= 0 - ), - // mais pas Paris-Paris - ) - .map((p) => ({ distance: p.distance, seats: p.seats })) - .sort((p1, p2) => (p1.distance > p2.distance ? 1 : p1.distance < p2.distance ? -1 : 0)); - - if (eligibleJourneys.length === 0) { - throw new NotApplicableTargetException(`Campaign "${IdfmStrikeJan2020.slug}" not Application on target`); - } - - // give 4€ for any distance - ctx.result = 400; - } -} diff --git a/api/services/policy/src/engine/rules/native/index.ts b/api/services/policy/src/engine/rules/native/index.ts index c1131c28ca..8c8985906d 100644 --- a/api/services/policy/src/engine/rules/native/index.ts +++ b/api/services/policy/src/engine/rules/native/index.ts @@ -1,5 +1,5 @@ import { IdfmRegular } from './IdfmRegular'; -import { IdfmStrikeJan2020 } from './IdfmStrikeJan2020'; +// import { IdfmStrikeJan2020 } from './IdfmStrikeJan2020'; import { StaticRuleInterface } from '../../interfaces'; -export const native: StaticRuleInterface[] = [IdfmRegular, IdfmStrikeJan2020]; +export const native: StaticRuleInterface[] = [IdfmRegular]; diff --git a/api/services/policy/src/engine/rules/stateful/AbstractStatefulRestriction.ts b/api/services/policy/src/engine/rules/stateful/AbstractStatefulRestriction.ts index d3a216b7a9..89b8bbf137 100644 --- a/api/services/policy/src/engine/rules/stateful/AbstractStatefulRestriction.ts +++ b/api/services/policy/src/engine/rules/stateful/AbstractStatefulRestriction.ts @@ -1,7 +1,8 @@ import { AbstractStatefulRule } from '../AbstractStatefulRule'; import { NotApplicableTargetException } from '../../exceptions/NotApplicableTargetException'; import { getMetaKey } from '../../helpers/getMetaKey'; -import { MetaInterface, RuleHandlerContextInterface } from '../../interfaces'; +import { RuleHandlerContextInterface } from '../../interfaces'; +import { MetadataWrapperInterface } from '../../../interfaces'; export interface StatefulRestrictionParameters { target?: 'driver' | 'passenger'; @@ -36,7 +37,7 @@ export abstract class AbstractStatefulRestriction extends AbstractStatefulRule = {}): { rule: MaxAmountRestriction; trip: TripInterface } { diff --git a/api/services/policy/src/engine/rules/stateful/MaxTripRestriction.spec.ts b/api/services/policy/src/engine/rules/stateful/MaxTripRestriction.spec.ts index 36311813e6..de5fd4b0cc 100644 --- a/api/services/policy/src/engine/rules/stateful/MaxTripRestriction.spec.ts +++ b/api/services/policy/src/engine/rules/stateful/MaxTripRestriction.spec.ts @@ -4,7 +4,7 @@ import { MaxTripRestriction } from './MaxTripRestriction'; import { faker } from '../../helpers/faker'; import { StatefulRestrictionParameters } from './AbstractStatefulRestriction'; import { NotApplicableTargetException } from '../../exceptions/NotApplicableTargetException'; -import { MetadataWrapper } from '../../meta/MetadataWrapper'; +import { MetadataWrapper } from '../../../providers/MetadataWrapper'; import { TripInterface } from '../../../interfaces'; function setup(cfg: Partial = {}): { rule: MaxTripRestriction; trip: TripInterface } { diff --git a/api/services/policy/src/engine/rules/stateful/MaxTripRestriction.ts b/api/services/policy/src/engine/rules/stateful/MaxTripRestriction.ts index af8393c1a8..d8e4f0921c 100644 --- a/api/services/policy/src/engine/rules/stateful/MaxTripRestriction.ts +++ b/api/services/policy/src/engine/rules/stateful/MaxTripRestriction.ts @@ -1,5 +1,6 @@ import { AbstractStatefulRestriction } from './AbstractStatefulRestriction'; -import { MetaInterface, RuleHandlerContextInterface } from '../../interfaces'; +import { RuleHandlerContextInterface } from '../../interfaces'; +import { MetadataWrapperInterface } from '../../../interfaces'; export class MaxTripRestriction extends AbstractStatefulRestriction { static readonly slug: string = 'max_trip_restriction'; @@ -17,7 +18,7 @@ export class MaxTripRestriction extends AbstractStatefulRestriction { return state + 1; } - getStateKey(ctx: RuleHandlerContextInterface, meta: MetaInterface): string | undefined { + getStateKey(ctx: RuleHandlerContextInterface, meta: MetadataWrapperInterface): string | undefined { const result = super.getStateKey(ctx, meta); if (this.parameters.target !== 'driver' || !ctx.person.is_driver) { return result; diff --git a/api/services/policy/src/engine/set/StatefulRuleSet.spec.ts b/api/services/policy/src/engine/set/StatefulRuleSet.spec.ts index 445190f76c..3ae40477d2 100644 --- a/api/services/policy/src/engine/set/StatefulRuleSet.spec.ts +++ b/api/services/policy/src/engine/set/StatefulRuleSet.spec.ts @@ -1,6 +1,6 @@ import test from 'ava'; import { faker } from '../helpers/faker'; -import { MetadataWrapper } from '../meta/MetadataWrapper'; +import { MetadataWrapper } from '../../providers/MetadataWrapper'; import { StatefulRuleSet } from './StatefulRuleSet'; import { getMetaKey } from '../helpers/getMetaKey'; import { IncentiveStateEnum, IncentiveStatusEnum } from '../../interfaces'; diff --git a/api/services/policy/src/engine/set/StatefulRuleSet.ts b/api/services/policy/src/engine/set/StatefulRuleSet.ts index 5234b62e37..63caa6d77a 100644 --- a/api/services/policy/src/engine/set/StatefulRuleSet.ts +++ b/api/services/policy/src/engine/set/StatefulRuleSet.ts @@ -1,8 +1,7 @@ -import { IncentiveInterface } from '../../interfaces'; +import { IncentiveInterface, MetadataWrapperInterface } from '../../interfaces'; import { RuleHandlerContextInterface, StatefulRuleInterface, - MetaInterface, StatefulRuleSetInterface, StaticRuleInterface, } from '../interfaces'; @@ -22,7 +21,7 @@ export class StatefulRuleSet extends AbstractRuleSet impl return this.ruleSet.length; } - buildInitialState(context: RuleHandlerContextInterface, meta: MetaInterface): Map { + buildInitialState(context: RuleHandlerContextInterface, meta: MetadataWrapperInterface): Map { const incentiveState: Map = new Map(); for (const statefulRule of this.ruleSet) { @@ -40,7 +39,7 @@ export class StatefulRuleSet extends AbstractRuleSet impl return [...keys]; } - apply(incentive: IncentiveInterface, meta: MetaInterface): number { + apply(incentive: IncentiveInterface, meta: MetadataWrapperInterface): number { let result = incentive.result; for (const statefulRule of this.ruleSet) { if (statefulRule.uuid in incentive.meta) { diff --git a/api/services/policy/src/engine/templates/eventTrafficLimitPolicy.integration.spec.ts b/api/services/policy/src/engine/templates/eventTrafficLimitPolicy.integration.spec.ts index 721329b479..2f8bddf229 100644 --- a/api/services/policy/src/engine/templates/eventTrafficLimitPolicy.integration.spec.ts +++ b/api/services/policy/src/engine/templates/eventTrafficLimitPolicy.integration.spec.ts @@ -9,7 +9,7 @@ const { test, results } = macro({ end_date: new Date('2019-01-15T23:59:59.999Z'), }); -test.skip(results, [ +test(results, [ { carpool_id: 10, amount: 0 }, { carpool_id: 11, amount: 0 }, { carpool_id: 34, amount: 100 }, diff --git a/api/services/policy/src/engine/templates/financialIncentivePolicy.integration.spec.ts b/api/services/policy/src/engine/templates/financialIncentivePolicy.integration.spec.ts index ae8deabaf6..91b3894b23 100644 --- a/api/services/policy/src/engine/templates/financialIncentivePolicy.integration.spec.ts +++ b/api/services/policy/src/engine/templates/financialIncentivePolicy.integration.spec.ts @@ -9,7 +9,7 @@ const { test, results } = macro({ end_date: new Date('2019-02-01'), }); -test.skip(results, [ +test(results, [ { carpool_id: 10, amount: 0 }, { carpool_id: 11, amount: 0 }, { carpool_id: 34, amount: 100 }, diff --git a/api/services/policy/src/engine/templates/freeTravelForPassengerPolicy.integration.spec.ts b/api/services/policy/src/engine/templates/freeTravelForPassengerPolicy.integration.spec.ts index b7a2ebeeb4..34f276dd21 100644 --- a/api/services/policy/src/engine/templates/freeTravelForPassengerPolicy.integration.spec.ts +++ b/api/services/policy/src/engine/templates/freeTravelForPassengerPolicy.integration.spec.ts @@ -9,7 +9,7 @@ const { test, results } = macro({ end_date: new Date('2019-02-01'), }); -test.skip(results, [ +test(results, [ { carpool_id: 10, amount: 0 }, { carpool_id: 11, amount: 0 }, { carpool_id: 34, amount: 0 }, diff --git a/api/services/policy/src/engine/templates/macro.ts b/api/services/policy/src/engine/templates/macro.ts index c6257f97e9..3b59f054cc 100644 --- a/api/services/policy/src/engine/templates/macro.ts +++ b/api/services/policy/src/engine/templates/macro.ts @@ -8,7 +8,7 @@ import { CampaignPgRepositoryProvider } from '../../providers/CampaignPgReposito import { ServiceProvider } from '../../ServiceProvider'; import { CampaignInterface } from '../../interfaces'; import { PolicyEngine } from '../PolicyEngine'; -import { MetadataProvider } from '../meta/MetadataProvider'; +import { MetadataRepositoryProvider } from '../../providers/MetadataRepositoryProvider'; import { trips as defaultTrips } from './trips'; interface TestContext { @@ -47,14 +47,14 @@ export function macro( if (t.context.policyId) { const connection = t.context.kernel.get(ServiceProvider).get(PostgresConnection).getClient(); const campaignRepository = t.context.kernel.get(ServiceProvider).get(CampaignPgRepositoryProvider); - const metaRepository = t.context.kernel.get(ServiceProvider).get(MetadataProvider); + const metaRepository = t.context.kernel.get(ServiceProvider).get(MetadataRepositoryProvider); await connection.query({ - text: `DELETE FROM ${campaignRepository.table} WHERE _id = $1`, + text: `DELETE FROM ${campaignRepository.table} WHERE _id = $1::int`, values: [t.context.policyId], }); await connection.query({ - text: `DELETE FROM ${metaRepository.table} WHERE policy_id = $1`, + text: `DELETE FROM ${metaRepository.table} WHERE policy_id = $1::int`, values: [t.context.policyId], }); } diff --git a/api/services/policy/src/engine/templates/nonFinancialIncentivePolicy.integration.spec.ts b/api/services/policy/src/engine/templates/nonFinancialIncentivePolicy.integration.spec.ts index e5724b4454..6981ea7b8b 100644 --- a/api/services/policy/src/engine/templates/nonFinancialIncentivePolicy.integration.spec.ts +++ b/api/services/policy/src/engine/templates/nonFinancialIncentivePolicy.integration.spec.ts @@ -9,7 +9,7 @@ const { test, results } = macro({ end_date: new Date('2019-02-01'), }); -test.skip(results, [ +test(results, [ { carpool_id: 10, amount: 0 }, { carpool_id: 11, amount: 0 }, { carpool_id: 34, amount: 10 }, diff --git a/api/services/policy/src/engine/templates/pollutionLimitPolicy.integration.spec.ts b/api/services/policy/src/engine/templates/pollutionLimitPolicy.integration.spec.ts index 31dc6e9015..948acd7461 100644 --- a/api/services/policy/src/engine/templates/pollutionLimitPolicy.integration.spec.ts +++ b/api/services/policy/src/engine/templates/pollutionLimitPolicy.integration.spec.ts @@ -9,7 +9,7 @@ const { test, results } = macro({ end_date: new Date('2019-01-15T23:59:59.999Z'), }); -test.skip(results, [ +test(results, [ { carpool_id: 10, amount: 0, meta: {} }, { carpool_id: 11, amount: 0, meta: {} }, { diff --git a/api/services/policy/src/engine/templates/weekdayTrafficLimitPolicy.integration.spec.ts b/api/services/policy/src/engine/templates/weekdayTrafficLimitPolicy.integration.spec.ts index 3fd93096c4..30bc2c094d 100644 --- a/api/services/policy/src/engine/templates/weekdayTrafficLimitPolicy.integration.spec.ts +++ b/api/services/policy/src/engine/templates/weekdayTrafficLimitPolicy.integration.spec.ts @@ -9,7 +9,7 @@ const { test, results } = macro({ end_date: new Date('2019-02-01'), }); -test.skip(results, [ +test(results, [ { carpool_id: 10, amount: 0, meta: {} }, { carpool_id: 11, amount: 0, meta: {} }, { carpool_id: 34, amount: 0, meta: {} }, diff --git a/api/services/policy/src/engine/templates/weekendTrafficLimitPolicy.integration.spec.ts b/api/services/policy/src/engine/templates/weekendTrafficLimitPolicy.integration.spec.ts index da61a7d3e0..fb9197b26a 100644 --- a/api/services/policy/src/engine/templates/weekendTrafficLimitPolicy.integration.spec.ts +++ b/api/services/policy/src/engine/templates/weekendTrafficLimitPolicy.integration.spec.ts @@ -9,7 +9,7 @@ const { test, results } = macro({ end_date: new Date('2019-02-01'), }); -test.skip(results, [ +test(results, [ { carpool_id: 10, amount: 0 }, { carpool_id: 11, amount: 0 }, { carpool_id: 34, amount: 0 }, diff --git a/api/services/policy/src/interfaces/IncentiveRepositoryProviderInterface.ts b/api/services/policy/src/interfaces/IncentiveRepositoryProviderInterface.ts index cbad1c42d1..c21c4721f1 100644 --- a/api/services/policy/src/interfaces/IncentiveRepositoryProviderInterface.ts +++ b/api/services/policy/src/interfaces/IncentiveRepositoryProviderInterface.ts @@ -1,24 +1,34 @@ import { PoolClient } from '@ilos/connection-postgres'; -import { IncentiveInterface } from '.'; +import { IncentiveInterface, IncentiveStatusEnum } from '.'; import { CampaignStateInterface } from './CampaignInterface'; export type IncentiveCreateOptionsType = { connection?: PoolClient | null; release?: boolean }; export interface IncentiveRepositoryProviderInterface { - updateManyAmount(data: { carpool_id: number; policy_id: number; amount: number }[]): Promise; + updateManyAmount( + data: { carpool_id: number; policy_id: number; amount: number; status: IncentiveStatusEnum }[], + status?: IncentiveStatusEnum, + ): Promise; createOrUpdateMany(data: IncentiveInterface[]): Promise; disableOnCanceledTrip(): Promise; lockAll(before: Date): Promise; - findDraftIncentive(before: Date, batchSize?: number): AsyncGenerator; + findDraftIncentive(to: Date, batchSize?: number, from?: Date): AsyncGenerator; getCampaignState(policy_id: number): Promise; } export abstract class IncentiveRepositoryProviderInterfaceResolver implements IncentiveRepositoryProviderInterface { - abstract updateManyAmount(data: { carpool_id: number; policy_id: number; amount: number }[]): Promise; + abstract updateManyAmount( + data: { carpool_id: number; policy_id: number; amount: number; status: IncentiveStatusEnum }[], + status?: IncentiveStatusEnum, + ): Promise; abstract createOrUpdateMany(data: IncentiveInterface[]): Promise; abstract disableOnCanceledTrip(): Promise; abstract lockAll(before: Date): Promise; - abstract findDraftIncentive(before: Date, batchSize?: number): AsyncGenerator; + abstract findDraftIncentive( + to: Date, + batchSize?: number, + from?: Date, + ): AsyncGenerator; abstract getCampaignState(policy_id: number): Promise; } diff --git a/api/services/policy/src/interfaces/MetadataRepositoryProviderInterface.ts b/api/services/policy/src/interfaces/MetadataRepositoryProviderInterface.ts new file mode 100644 index 0000000000..9ffcb24337 --- /dev/null +++ b/api/services/policy/src/interfaces/MetadataRepositoryProviderInterface.ts @@ -0,0 +1,21 @@ +import { MetadataWrapperInterface } from './MetadataWrapperInterface'; + +export interface MetadataRepositoryProviderInterface { + get(id: number, keys?: string[], datetime?: Date): Promise; + set(id: number, metadata: MetadataWrapperInterface, date: Date): Promise; + delete(policyId: number, from?: Date): Promise; +} + +export abstract class MetadataRepositoryProviderInterfaceResolver implements MetadataRepositoryProviderInterface { + async get(id: number, keys?: string[], datetime?: Date): Promise { + throw new Error(); + } + + async set(id: number, metadata: MetadataWrapperInterface, date: Date): Promise { + throw new Error(); + } + + async delete(policyId: number, from?: Date): Promise { + throw new Error(); + } +} diff --git a/api/services/policy/src/engine/interfaces/MetaInterface.ts b/api/services/policy/src/interfaces/MetadataWrapperInterface.ts similarity index 81% rename from api/services/policy/src/engine/interfaces/MetaInterface.ts rename to api/services/policy/src/interfaces/MetadataWrapperInterface.ts index d850f62090..fe6a997cc4 100644 --- a/api/services/policy/src/engine/interfaces/MetaInterface.ts +++ b/api/services/policy/src/interfaces/MetadataWrapperInterface.ts @@ -1,4 +1,4 @@ -export interface MetaInterface { +export interface MetadataWrapperInterface { has(key: string): boolean; get(key: string, fallback?: number): number; set(key: string, data: number): void; diff --git a/api/services/policy/src/interfaces/TripRepositoryProviderInterface.ts b/api/services/policy/src/interfaces/TripRepositoryProviderInterface.ts index 512361d154..052c6e8c13 100644 --- a/api/services/policy/src/interfaces/TripRepositoryProviderInterface.ts +++ b/api/services/policy/src/interfaces/TripRepositoryProviderInterface.ts @@ -2,16 +2,19 @@ import { TripInterface } from '.'; import { ProcessableCampaign } from '../engine/ProcessableCampaign'; export interface TripRepositoryProviderInterface { - findTripByPolicy(policy: ProcessableCampaign, batchSize?: number): AsyncGenerator; - refresh(): Promise; + findTripByPolicy( + policy: ProcessableCampaign, + batchSize?: number, + override_from?: Date, + ): AsyncGenerator; listApplicablePoliciesId(): Promise; } export abstract class TripRepositoryProviderInterfaceResolver implements TripRepositoryProviderInterface { - abstract refresh(): Promise; abstract listApplicablePoliciesId(): Promise; abstract findTripByPolicy( policy: ProcessableCampaign, batchSize?: number, + override_from?: Date, ): AsyncGenerator; } diff --git a/api/services/policy/src/interfaces/index.ts b/api/services/policy/src/interfaces/index.ts index 5c4debc1aa..5daea0f670 100644 --- a/api/services/policy/src/interfaces/index.ts +++ b/api/services/policy/src/interfaces/index.ts @@ -23,3 +23,8 @@ export { TerritoryRepositoryProviderInterface, TerritoryRepositoryProviderInterfaceResolver, } from './TerritoryRepositoryProviderInterface'; +export { + MetadataRepositoryProviderInterface, + MetadataRepositoryProviderInterfaceResolver, +} from './MetadataRepositoryProviderInterface'; +export { MetadataWrapperInterface } from './MetadataWrapperInterface'; diff --git a/api/services/policy/src/providers/CampaignPgRepositoryProvider.ts b/api/services/policy/src/providers/CampaignPgRepositoryProvider.ts index 3341156f69..f92b764044 100644 --- a/api/services/policy/src/providers/CampaignPgRepositoryProvider.ts +++ b/api/services/policy/src/providers/CampaignPgRepositoryProvider.ts @@ -219,8 +219,8 @@ export class CampaignPgRepositoryProvider implements CampaignRepositoryProviderI const query = { text: ` SELECT * FROM ${this.table} - WHERE _id = $1 - AND territory_id = $2 + WHERE _id = $1::int + AND territory_id = $2::int AND deleted_at IS NULL LIMIT 1 `, diff --git a/api/services/policy/src/providers/CampaignRepositoryProvider.integration.spec.ts b/api/services/policy/src/providers/CampaignRepositoryProvider.integration.spec.ts index 6eb8b5d6fc..21f32d7e1b 100644 --- a/api/services/policy/src/providers/CampaignRepositoryProvider.integration.spec.ts +++ b/api/services/policy/src/providers/CampaignRepositoryProvider.integration.spec.ts @@ -33,7 +33,7 @@ function makeCampaign(data: Partial = {}): CampaignInterface }; } -test.before.skip(async (t) => { +test.before(async (t) => { t.context.territory_id = 0; t.context.connection = new PostgresConnection({ connectionString: @@ -46,7 +46,7 @@ test.before.skip(async (t) => { t.context.campaign = await t.context.repository.create(makeCampaign()); }); -test.after.always.skip(async (t) => { +test.after.always(async (t) => { await t.context.connection.getClient().query({ text: `DELETE FROM ${t.context.repository.table} WHERE territory_id = $1`, values: [t.context.territory_id], @@ -54,7 +54,7 @@ test.after.always.skip(async (t) => { await t.context.connection.down(); }); -test.serial.skip('Should create campaign', async (t) => { +test.serial('Should create campaign', async (t) => { const campaignData = makeCampaign(); const campaign = await t.context.repository.create(campaignData); @@ -68,25 +68,25 @@ test.serial.skip('Should create campaign', async (t) => { t.is(result.rows[0].status, 'draft'); }); -test.serial.skip('Should find campaign', async (t) => { +test.serial('Should find campaign', async (t) => { t.log(t.context.campaign); const campaign = await t.context.repository.find(t.context.campaign._id); t.is(campaign.name, t.context.campaign.name); t.is(campaign.status, t.context.campaign.status); }); -test.serial.skip('Should find campaign by territory', async (t) => { +test.serial('Should find campaign by territory', async (t) => { const campaign = await t.context.repository.findOneWhereTerritory(t.context.campaign._id, t.context.territory_id); t.is(campaign.name, t.context.campaign.name); t.is(campaign.status, t.context.campaign.status); }); -test.serial.skip('Should not find campaign by territory', async (t) => { +test.serial('Should not find campaign by territory', async (t) => { const campaign = await t.context.repository.findOneWhereTerritory(t.context.campaign._id, 1); t.is(campaign, null); }); -test.serial.skip('Should patch campaign', async (t) => { +test.serial('Should patch campaign', async (t) => { const name = 'Awesome campaign'; const campaign = await t.context.repository.patch(t.context.campaign._id, { name }); @@ -102,7 +102,7 @@ test.serial.skip('Should patch campaign', async (t) => { t.context.campaign.name = name; }); -test.serial.skip('Should patch campaign by territory id', async (t) => { +test.serial('Should patch campaign by territory id', async (t) => { const name = 'Awesome campaign 2'; const campaign = await t.context.repository.patchWhereTerritory(t.context.campaign._id, t.context.territory_id, { name, @@ -120,7 +120,7 @@ test.serial.skip('Should patch campaign by territory id', async (t) => { t.context.campaign.name = name; }); -test.serial.skip('Should not patch campaign by territory id', async (t) => { +test.serial('Should not patch campaign by territory id', async (t) => { const name = 'Not updating!'; const err = await t.throwsAsync(async () => t.context.repository.patchWhereTerritory(t.context.campaign._id, 1, { name }), @@ -135,7 +135,7 @@ test.serial.skip('Should not patch campaign by territory id', async (t) => { t.not(result.rows[0].name, name); }); -// test.serial.skip('Should not patch campaign if active', async (t) => { +// test.serial('Should not patch campaign if active', async (t) => { // const name = 'Nope!'; // const err = await t.throwsAsync(async () => t.context.repository.patch(t.context.campaign._id, { name })); @@ -149,7 +149,7 @@ test.serial.skip('Should not patch campaign by territory id', async (t) => { // t.not(result.rows[0].name, name); // }); -test.serial.skip('Should not delete campaign if active', async (t) => { +test.serial('Should not delete campaign if active', async (t) => { await t.context.connection.getClient().query({ text: `UPDATE ${t.context.repository.table} SET status = 'active'::policy.policy_status_enum WHERE _id = $1`, values: [t.context.campaign._id], diff --git a/api/services/policy/src/providers/IncentiveRepositoryProvider.integration.spec.ts b/api/services/policy/src/providers/IncentiveRepositoryProvider.integration.spec.ts index 421ab7f529..2c53f637e2 100644 --- a/api/services/policy/src/providers/IncentiveRepositoryProvider.integration.spec.ts +++ b/api/services/policy/src/providers/IncentiveRepositoryProvider.integration.spec.ts @@ -133,11 +133,13 @@ test.serial.skip('Should update many incentives amount', async (t) => { policy_id: 0, carpool_id: 3, amount: 0, + status: IncentiveStatusEnum.Draft, }, { policy_id: 0, carpool_id: 2, amount: 0, + status: IncentiveStatusEnum.Draft, }, ]; diff --git a/api/services/policy/src/providers/IncentiveRepositoryProvider.ts b/api/services/policy/src/providers/IncentiveRepositoryProvider.ts index afca82397d..4a2d1d035a 100644 --- a/api/services/policy/src/providers/IncentiveRepositoryProvider.ts +++ b/api/services/policy/src/providers/IncentiveRepositoryProvider.ts @@ -7,6 +7,7 @@ import { IncentiveRepositoryProviderInterface, IncentiveRepositoryProviderInterfaceResolver, IncentiveStateEnum, + IncentiveStatusEnum, CampaignStateInterface, } from '../interfaces'; @@ -20,6 +21,7 @@ export class IncentiveRepositoryProvider implements IncentiveRepositoryProviderI constructor(protected connection: PostgresConnection) {} async disableOnCanceledTrip(): Promise { + console.debug(`DISABLE_ON_CANCELED_TRIPS`); const query = { text: ` UPDATE ${this.table} AS pi @@ -51,7 +53,10 @@ export class IncentiveRepositoryProvider implements IncentiveRepositoryProviderI await this.connection.getClient().query(query); } - async updateManyAmount(data: { carpool_id: number; policy_id: number; amount: number }[]): Promise { + async updateManyAmount( + data: { carpool_id: number; policy_id: number; amount: number; status: IncentiveStatusEnum }[], + status?: IncentiveStatusEnum, + ): Promise { const idSet: Set = new Set(); const filteredData = data.reverse().filter((d) => { const key = `${d.policy_id}/${d.carpool_id}`; @@ -62,7 +67,10 @@ export class IncentiveRepositoryProvider implements IncentiveRepositoryProviderI return true; }); - const keys = ['policy_id', 'carpool_id', 'amount'].map((k) => filteredData.map((d) => d[k])); + // pick values for the given keys. Override status if defined + const values = ['policy_id', 'carpool_id', 'amount', 'status'].map((k) => + filteredData.map((d) => (status && k === 'status' ? status : d[k])), + ); const query = { text: ` @@ -70,34 +78,52 @@ export class IncentiveRepositoryProvider implements IncentiveRepositoryProviderI SELECT * FROM UNNEST ( $1::int[], $2::int[], - $3::int[] + $3::int[], + $4::policy.incentive_status_enum[] ) as t( policy_id, carpool_id, - amount + amount, + status ) ) UPDATE ${this.table} as pt SET ( amount, - state + state, + status ) = ( data.amount, - CASE WHEN data.amount = 0 THEN 'null'::policy.incentive_state_enum ELSE state END + CASE WHEN data.amount = 0 THEN 'null'::policy.incentive_state_enum ELSE state END, + data.status ) FROM data WHERE data.carpool_id = pt.carpool_id AND data.policy_id = pt.policy_id `, - values: [...keys], + values: [...values], }; await this.connection.getClient().query(query); - return; } - async *findDraftIncentive(before: Date, batchSize = 100): AsyncGenerator { + async *findDraftIncentive(to: Date, batchSize = 100, from?: Date): AsyncGenerator { + const resCount = await this.connection.getClient().query({ + text: ` + SELECT + count(*) + FROM ${this.table} + WHERE + status = $1::policy.incentive_status_enum + ${from ? 'AND datetime >= $3::timestamp' : ''} + AND datetime <= $2::timestamp + `, + values: ['draft', to, ...(from ? [from] : [])], + }); + + console.debug(`FOUND ${resCount.rows[0].count} incentives to process`); + const query = { text: ` SELECT @@ -111,11 +137,12 @@ export class IncentiveRepositoryProvider implements IncentiveRepositoryProviderI meta FROM ${this.table} WHERE - status = $1::policy.incentive_status_enum AND - datetime <= $2::timestamp + status = $1::policy.incentive_status_enum + ${from ? 'AND datetime >= $3::timestamp' : ''} + AND datetime <= $2::timestamp ORDER BY datetime ASC; `, - values: ['draft', before], + values: ['draft', to, ...(from ? [from] : [])], }; const client = await this.connection.getClient().connect(); diff --git a/api/services/policy/src/providers/MetadataRepositoryProvider.integration.spec.ts b/api/services/policy/src/providers/MetadataRepositoryProvider.integration.spec.ts new file mode 100644 index 0000000000..d53c582b11 --- /dev/null +++ b/api/services/policy/src/providers/MetadataRepositoryProvider.integration.spec.ts @@ -0,0 +1,159 @@ +import anyTest, { TestInterface } from 'ava'; +import { PostgresConnection } from '@ilos/connection-postgres'; + +import { MetadataRepositoryProvider } from './MetadataRepositoryProvider'; +import { MetadataWrapper } from './MetadataWrapper'; + +interface TestContext { + repository: MetadataRepositoryProvider; + connection: PostgresConnection; + policyId: number; +} +const test = anyTest as TestInterface; + +test.before(async (t) => { + t.context.policyId = 0; + t.context.connection = new PostgresConnection({ + connectionString: + 'APP_POSTGRES_URL' in process.env + ? process.env.APP_POSTGRES_URL + : 'postgresql://postgres:postgres@localhost:5432/local', + }); + + await t.context.connection.up(); + t.context.repository = new MetadataRepositoryProvider(t.context.connection); +}); + +test.after.always(async (t) => { + // clean db + await t.context.connection.getClient().query({ + text: `DELETE from ${t.context.repository.table} WHERE policy_id = $1`, + values: [t.context.policyId], + }); + + // shutdown connection + await t.context.connection.down(); +}); + +test.serial('should always return a metadata wrapper', async (t) => { + const meta = await t.context.repository.get(t.context.policyId); + t.true(meta instanceof MetadataWrapper); + t.is(meta.keys().length, 0); +}); + +test.serial('should create metadata wrapper on database', async (t) => { + const meta = await t.context.repository.get(t.context.policyId); + t.true(meta instanceof MetadataWrapper); + t.is(meta.keys().length, 0); + meta.set('toto', 0); + + await t.context.repository.set(t.context.policyId, meta, new Date('2021-01-01')); + + const dbResult = await t.context.connection.getClient().query({ + text: `SELECT value from ${t.context.repository.table} WHERE policy_id = $1 AND key = $2`, + values: [t.context.policyId, 'toto'], + }); + + t.log(dbResult.rows); + t.is(dbResult.rowCount, 1); + t.deepEqual(dbResult.rows, [ + { + value: 0, + }, + ]); +}); + +test.serial('should return metadata from database', async (t) => { + const meta = await t.context.repository.get(t.context.policyId); + t.true(meta instanceof MetadataWrapper); + t.is(meta.keys().length, 1); + const value = meta.get('toto'); + t.is(value, 0); +}); + +test.serial('should create another metadata wrapper on database', async (t) => { + const meta = await t.context.repository.get(t.context.policyId); + t.true(meta instanceof MetadataWrapper); + t.is(meta.keys().length, 1); + meta.set('toto', 1); + await t.context.repository.set(t.context.policyId, meta, new Date('2021-02-01')); + + const meta2 = await t.context.repository.get(t.context.policyId); + t.is(meta2.keys().length, 1); + t.is(meta2.get('toto'), 1); + meta2.set('toto', 2); + meta2.set('tata', 100); + + await t.context.repository.set(t.context.policyId, meta2, new Date('2021-03-01')); + + const dbResult = await t.context.connection.getClient().query({ + text: `SELECT key, value from ${t.context.repository.table} WHERE policy_id = $1 ORDER BY key, datetime`, + values: [t.context.policyId], + }); + + t.is(dbResult.rowCount, 4); + t.deepEqual(dbResult.rows, [ + { + key: 'tata', + value: 100, + }, + { + key: 'toto', + value: 0, + }, + { + key: 'toto', + value: 1, + }, + { + key: 'toto', + value: 2, + }, + ]); + + const meta3 = await t.context.repository.get(t.context.policyId); + t.is(meta3.keys().length, 2); + t.is(meta3.get('toto'), 2); + t.is(meta3.get('tata'), 100); +}); + +test.serial('should get old meta if asked at datetime', async (t) => { + const meta = await t.context.repository.get(t.context.policyId, ['toto'], new Date('2021-02-01')); + t.true(meta instanceof MetadataWrapper); + t.is(meta.keys().length, 1); + const value = meta.get('toto'); + t.is(value, 1); +}); + +test.serial('should delete from datetime', async (t) => { + await t.context.repository.delete(t.context.policyId, new Date('2021-03-01')); + + const dbResult = await t.context.connection.getClient().query({ + text: `SELECT key, value from ${t.context.repository.table} WHERE policy_id = $1 ORDER BY key, datetime`, + values: [t.context.policyId], + }); + + t.is(dbResult.rowCount, 2); + + t.deepEqual(dbResult.rows, [ + { + key: 'toto', + value: 0, + }, + { + key: 'toto', + value: 1, + }, + ]); +}); + +test.serial('should delete', async (t) => { + await t.context.repository.delete(t.context.policyId); + + const dbResult = await t.context.connection.getClient().query({ + text: `SELECT key, value from ${t.context.repository.table} WHERE policy_id = $1 ORDER BY key, datetime`, + values: [t.context.policyId], + }); + + t.is(dbResult.rowCount, 0); +}); diff --git a/api/services/policy/src/providers/MetadataRepositoryProvider.ts b/api/services/policy/src/providers/MetadataRepositoryProvider.ts new file mode 100644 index 0000000000..79fc6af062 --- /dev/null +++ b/api/services/policy/src/providers/MetadataRepositoryProvider.ts @@ -0,0 +1,97 @@ +import { PostgresConnection } from '@ilos/connection-postgres'; +import { provider } from '@ilos/common'; + +import { + MetadataWrapperInterface, + MetadataRepositoryProviderInterface, + MetadataRepositoryProviderInterfaceResolver, +} from '../interfaces'; +import { MetadataWrapper } from './MetadataWrapper'; + +@provider({ + identifier: MetadataRepositoryProviderInterfaceResolver, +}) +export class MetadataRepositoryProvider implements MetadataRepositoryProviderInterface { + public readonly table = 'policy.policy_metas'; + + constructor(protected connection: PostgresConnection) {} + + async get(id: number, keys: string[] = [], datetime?: Date): Promise { + const whereClauses: { + text: string; + value: any; + }[] = [ + { + text: 'policy_id = $1', + value: id, + }, + ]; + + if (keys.length > 0) { + whereClauses.push({ + text: 'key = ANY($2::varchar[])', + value: keys, + }); + } + + if (datetime) { + whereClauses.push({ + text: `datetime <= ${keys.length ? '$3' : '$2'}::timestamp`, + value: datetime, + }); + } + + // get the latest value for a key + const query: { + rowMode: string; + text: string; + values: any[]; + } = { + rowMode: 'array', + text: ` + SELECT + key, + (max(array[extract('epoch' from datetime), value::int]))[2] as value + FROM ${this.table} + WHERE ${whereClauses.map((w) => w.text).join(' AND ')} + GROUP BY key + `, + values: [...whereClauses.map((w) => w.value)], + }; + + const result = await this.connection.getClient().query(query); + + return new MetadataWrapper(id, result.rows); + } + + async set(policyId: number, metadata: MetadataWrapperInterface, date: Date): Promise { + const keys = metadata.keys(); + const values = metadata.values(); + const policyIds = new Array(keys.length).fill(policyId); + const dates = new Array(keys.length).fill(date); + const query = { + text: ` + INSERT INTO ${this.table} (policy_id, key, value, datetime) + SELECT * FROM UNNEST($1::int[], $2::varchar[], $3::int[], $4::timestamp[]) + `, + values: [policyIds, keys, values, dates], + }; + + await this.connection.getClient().query(query); + return; + } + + async delete(policyId: number, from?: Date): Promise { + const query = { + text: ` + DELETE FROM ${this.table} + WHERE policy_id = $1::int + ${from ? 'AND datetime >= $2::timestamp' : ''} + `, + values: [policyId, ...(from ? [from] : [])], + }; + + await this.connection.getClient().query(query); + return; + } +} diff --git a/api/services/policy/src/engine/meta/MetadataWrapper.ts b/api/services/policy/src/providers/MetadataWrapper.ts similarity index 84% rename from api/services/policy/src/engine/meta/MetadataWrapper.ts rename to api/services/policy/src/providers/MetadataWrapper.ts index c83016c18c..5b37f812fc 100644 --- a/api/services/policy/src/engine/meta/MetadataWrapper.ts +++ b/api/services/policy/src/providers/MetadataWrapper.ts @@ -1,6 +1,6 @@ -import { MetaInterface } from '../interfaces/MetaInterface'; +import { MetadataWrapperInterface } from '../interfaces'; -export class MetadataWrapper implements MetaInterface { +export class MetadataWrapper implements MetadataWrapperInterface { protected data: Map; constructor(public readonly policy_id: number = 0, initialData?: [string, number][]) { diff --git a/api/services/policy/src/providers/TripRepositoryProvider.ts b/api/services/policy/src/providers/TripRepositoryProvider.ts index ebf06da962..82c3763e40 100644 --- a/api/services/policy/src/providers/TripRepositoryProvider.ts +++ b/api/services/policy/src/providers/TripRepositoryProvider.ts @@ -1,6 +1,7 @@ import { promisify } from 'util'; import { provider } from '@ilos/common'; import { PostgresConnection, Cursor } from '@ilos/connection-postgres'; +import { v4 } from 'uuid'; import { TripRepositoryProviderInterface, TripRepositoryProviderInterfaceResolver, TripInterface } from '../interfaces'; import { ProcessableCampaign } from '../engine/ProcessableCampaign'; @@ -13,64 +14,148 @@ export class TripRepositoryProvider implements TripRepositoryProviderInterface { constructor(protected connection: PostgresConnection) {} - async refresh(): Promise { - await this.connection.getClient().query(`REFRESH MATERIALIZED VIEW ${this.table}`); - return; + async listApplicablePoliciesId(): Promise { + const results = await this.connection.getClient().query("SELECT _id FROM policy.policies WHERE status = 'active'"); + return results.rows.map((r) => r._id); } - async listApplicablePoliciesId(): Promise { - const query = { + /** + * Find trips by policy + * + * This AsyncGenerator works in 2 ways + * 1. find all trips matching a policy where no incentive has been calculated + * 2. find all trips matching a policy from a given date (overrides results) + * + * Criterions to match a policy : + * 1. trip's datetime must be within range + * 2. start_territory OR end_territory must be within the policy's territory + * + * The queries are split in different stages and written on an unlogged table + * for performance. Cleanup is done manually at the end of the call. + * + * Query stages + * 1. find all children territories of the AOM (policy.territory_id) as + * carpool.start_territory_id and carpool.end_territory_id are at town level. + * The int array is used on every line to filter carpools. + * 2. Create an unlogged table with all the matching carpools (datetime, territory_id). + * When overrideFrom exists, it is used as start_date and ALL trips are returned. + * When it is missing, only carpools without calculated incentives are returned. + * 3. Add indexes on the unlogged table for faster joins + * 4. Create 2 unlogged tables for start and end territories. We use the list of + * territory ancestors in the output and need to calculate it for each row. + * These queries do the lookup once for all start and end territory IDs of all + * carpools from the created unlogged table. + * 5. Create indexes on these tables + * 6. Format the results as [driver_json, passenger_json] rows with the joined + * start and end territory ancestors' lists. + * + * The AsyncGenerator yields calculations within a loop iterating over the Cursor. + */ + async *findTripByPolicy( + policy: ProcessableCampaign, + batchSize = 100, + overrideFrom?: Date, + ): AsyncGenerator { + // generate unique name for temporary table + const tableName = `trips_${policy.policy_id}_${v4().replace(/-/g, '')}`; + console.debug(`TABLE NAME: ${tableName}`); + + // fetch descendants first to improve performance + const descendantsRes = await this.connection.getClient().query({ + text: `SELECT unnest(territory.get_descendants(array[$1::int])) _id`, + values: [policy.territory_id], + }); + + const descendants = descendantsRes.rowCount ? descendantsRes.rows.map((r) => r._id) : []; + + const s = new Date(); + + await this.connection.getClient().query({ text: ` - SELECT - distinct pp - FROM ${this.table} as pt, - UNNEST(pt.processable_policies) as pp + CREATE UNLOGGED TABLE ${tableName} AS ( + SELECT pt.* + FROM ${this.table} pt + LEFT JOIN policy.incentives pi + ON pt.carpool_id = pi.carpool_id + AND pi.policy_id = $4::int + WHERE + pt.datetime >= $1::timestamp AND pt.datetime < $2::timestamp + AND pt.carpool_status = 'ok'::carpool.carpool_status_enum + ${overrideFrom ? '' : 'AND pi.carpool_id IS NULL'} + AND ( + pt.start_territory_id = ANY($3::int[]) + OR + pt.end_territory_id = ANY($3::int[]) + ) + ) `, - values: [], - }; - const results = await this.connection.getClient().query(query); - return results.rows.map((r) => r.pp); - } + values: [overrideFrom || policy.start_date, policy.end_date, descendants, policy.policy_id], + }); + + console.debug(`CREATE TABLE in ${new Date().getTime() - s.getTime()}ms`); + + const s2 = new Date(); + await this.connection.getClient().query(`CREATE INDEX ON ${tableName} (start_territory_id)`); + await this.connection.getClient().query(`CREATE INDEX ON ${tableName} (end_territory_id)`); + console.debug(`CREATE INDEXES in ${new Date().getTime() - s2.getTime()}ms`); + + const s3 = new Date(); + await this.connection.getClient().query(` + CREATE UNLOGGED TABLE ${tableName}_start AS ( + SELECT + start_territory_id AS territory_id, + start_territory_id || territory.get_ancestors(ARRAY[cc.start_territory_id]) ancestors + FROM (SELECT distinct start_territory_id FROM ${tableName}) cc + ) + `); + + await this.connection.getClient().query(` + CREATE UNLOGGED TABLE ${tableName}_end AS ( + SELECT + end_territory_id AS territory_id, + end_territory_id || territory.get_ancestors(ARRAY[cc.end_territory_id]) ancestors + FROM (SELECT distinct end_territory_id FROM ${tableName}) cc + ) + `); + console.debug(`CREATE START/END TABLES in ${new Date().getTime() - s3.getTime()}ms`); + + const s4 = new Date(); + await this.connection.getClient().query(`CREATE INDEX ON ${tableName}_start (territory_id)`); + await this.connection.getClient().query(`CREATE INDEX ON ${tableName}_end (territory_id)`); + console.debug(`CREATE INDEXES in ${new Date().getTime() - s4.getTime()}ms`); - async *findTripByPolicy(policy: ProcessableCampaign, batchSize = 100): AsyncGenerator { const query = { text: ` - SELECT - json_agg( - json_build_object( - 'identity_uuid', pt.identity_uuid, - 'carpool_id', pt.carpool_id, - 'operator_id', pt.operator_id, - 'operator_class', pt.operator_class, - 'is_over_18', pt.is_over_18, - 'is_driver', pt.is_driver, - 'has_travel_pass', pt.has_travel_pass, - 'datetime', pt.datetime, - 'start_insee', pt.start_insee, - 'end_insee', pt.end_insee, - 'seats', pt.seats, - 'duration', pt.duration, - 'distance', pt.distance, - 'cost', pt.cost, - 'start_territory_id', pt.start_territory_id, - 'end_territory_id', pt.end_territory_id - ) - ) as people - FROM ${this.table} as pt - LEFT JOIN policy.incentives as pi - ON pi.carpool_id = pt.carpool_id - AND pi.policy_id = $4::int - WHERE pt.datetime >= $1::timestamp AND pt.datetime <= $2::timestamp - AND pt.carpool_status = 'ok'::carpool.carpool_status_enum - AND ( - $3::int = ANY(pt.start_territory_id) - OR $3::int = ANY(pt.end_territory_id) - ) - AND pi.carpool_id IS NULL - GROUP BY pt.trip_id - ORDER BY min(pt.datetime) ASC + SELECT + json_agg( + json_build_object( + 'identity_uuid', t.identity_uuid, + 'carpool_id', t.carpool_id, + 'operator_id', t.operator_id, + 'operator_class', t.operator_class, + 'is_over_18', t.is_over_18, + 'is_driver', t.is_driver, + 'has_travel_pass', t.has_travel_pass, + 'datetime', t.datetime, + 'seats', t.seats, + 'duration', t.duration, + 'distance', t.distance, + 'cost', t.cost, + 'start_insee', t.start_insee, + 'end_insee', t.end_insee, + 'start_territory_id', t_start.ancestors, + 'end_territory_id', t_end.ancestors + ) + ) AS people + FROM ${tableName} t + LEFT JOIN ${tableName}_start t_start + ON t.start_territory_id = t_start.territory_id + LEFT JOIN ${tableName}_end t_end + ON t.end_territory_id = t_end.territory_id + GROUP BY t.acquisition_id + ORDER BY min(t.datetime) ASC `, - values: [policy.start_date, policy.end_date, policy.territory_id, policy.policy_id], + values: [], }; const client = await this.connection.getClient().connect(); @@ -90,10 +175,22 @@ export class TripRepositoryProvider implements TripRepositoryProviderInterface { ]; } } catch (e) { + await this.dropTmpTables(tableName); cursor.close(() => client.release()); throw e; } } while (count > 0); + + // done + await this.dropTmpTables(tableName); cursor.close(() => client.release()); + console.debug(`CURSOR RELEASED: ${tableName}`); + } + + private async dropTmpTables(tableName: string): Promise { + await this.connection.getClient().query(`DROP TABLE IF EXISTS ${tableName}`); + await this.connection.getClient().query(`DROP TABLE IF EXISTS ${tableName}_start`); + await this.connection.getClient().query(`DROP TABLE IF EXISTS ${tableName}_end`); + console.debug(`DROP TABLES ${tableName}/_start/_end`); } } diff --git a/api/yarn.lock b/api/yarn.lock index a6b334b474..4e27a3ed0f 100644 --- a/api/yarn.lock +++ b/api/yarn.lock @@ -1416,14 +1416,14 @@ integrity sha512-kpXL41O0a30D5lch4Z82f1n/7ZIx2IFxaMBbcQjf3ul5H7rXA8P6NlraL4iAt8J2EPjJi3931ydshr7y89MjJA== "@types/node@*", "@types/node@^16.9.4": - version "16.11.1" - resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.1.tgz#2e50a649a50fc403433a14f829eface1a3443e97" - integrity sha512-PYGcJHL9mwl1Ek3PLiYgyEKtwTMmkMw4vbiyz/ps3pfdRYLVv+SN7qHVAImrjdAXxgluDEw6Ph4lyv+m9UpRmA== + version "16.11.6" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.6.tgz#6bef7a2a0ad684cf6e90fcfe31cecabd9ce0a3ae" + integrity sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w== "@types/node@^14.0.1": - version "14.17.27" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.17.27.tgz#5054610d37bb5f6e21342d0e6d24c494231f3b85" - integrity sha512-94+Ahf9IcaDuJTle/2b+wzvjmutxXAEXU6O81JHblYXUg2BDG+dnBy7VxIPHKAyEEDHzCMQydTJuWvrE+Aanzw== + version "14.17.32" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.17.32.tgz#2ca61c9ef8c77f6fa1733be9e623ceb0d372ad96" + integrity sha512-JcII3D5/OapPGx+eJ+Ik1SQGyt6WvuqdRfh9jUwL6/iHGjmyOriBDciBUu7lEIBTL2ijxwrR70WUnw5AEDmFvQ== "@types/nodemailer@^6.4.4": version "6.4.4" @@ -1470,9 +1470,9 @@ "@types/node" "*" "@types/sinon@^10.0.3": - version "10.0.4" - resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-10.0.4.tgz#9332527665692b9f6826afe017f342a3ac6120f4" - integrity sha512-fOYjrxQv8zJsqOY6V6ecP4eZhQBxtY80X0er1VVnUIAIZo74jHm8e1vguG5Yt4Iv8W2Wr7TgibB8MfRe32k9pA== + version "10.0.6" + resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-10.0.6.tgz#bc3faff5154e6ecb69b797d311b7cf0c1b523a1d" + integrity sha512-6EF+wzMWvBNeGrfP3Nx60hhx+FfwSg1JJBLAAP/IdIUq0EYkqCYf70VT3PhuhPX9eLD+Dp+lNdpb/ZeHG8Yezg== dependencies: "@sinonjs/fake-timers" "^7.1.0" @@ -1785,9 +1785,9 @@ astral-regex@^2.0.0: integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== async@^3.2.0: - version "3.2.1" - resolved "https://registry.yarnpkg.com/async/-/async-3.2.1.tgz#d3274ec66d107a47476a4c49136aacdb00665fc8" - integrity sha512-XdD5lRO/87udXCMC9meWdYiR+Nq6ZjUfXidViUZGu2F1MO4T3XwZ1et0hb2++BgLfhyJwy44BGB/yx80ABx8hg== + version "3.2.2" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.2.tgz#2eb7671034bb2194d45d30e31e24ec7e7f9670cd" + integrity sha512-H0E+qZaDEfx/FY4t7iLRv1W2fFI6+pyCeTw1uN20AQPiwqwM6ojPxHxdLv4z8hi2DtnW9BOckSspLucW7pIE5g== async@~0.9.0: version "0.9.2" @@ -1877,9 +1877,9 @@ ava@^3.15.0: yargs "^16.2.0" aws-sdk@^2.991.0: - version "2.1011.0" - resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.1011.0.tgz#8de560a1203fbbb1908dc9f34814613874a248c2" - integrity sha512-U5CDmLUHYAur89+Rx1r3X5gRzc8ib90G9aaKnyGqLsIail24r6UINejJB8pyOzFQ+u22/kFKKclfXcTj/Hflbg== + version "2.1017.0" + resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.1017.0.tgz#cc8981d269ab6c1de9f3d6bf8e9cb6d85f3dcbc7" + integrity sha512-oS3KmMhzxK0HEy1AfHV8EjfvNlm8PpLSswbh+SF6MBP1Hy8vyQ/1dFhhWpEogpyIzV6MUdKnq++bTijXGIRqdA== dependencies: buffer "4.9.2" events "1.1.1" @@ -2033,14 +2033,14 @@ braces@^3.0.1, braces@~3.0.2: fill-range "^7.0.1" browserslist@^4.16.6: - version "4.17.4" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.17.4.tgz#72e2508af2a403aec0a49847ef31bd823c57ead4" - integrity sha512-Zg7RpbZpIJRW3am9Lyckue7PLytvVxxhJj1CaJVlCWENsGEAOlnlt8X0ZxGRPp7Bt9o8tIRM5SEXy4BCPMJjLQ== + version "4.17.5" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.17.5.tgz#c827bbe172a4c22b123f5e337533ceebadfdd559" + integrity sha512-I3ekeB92mmpctWBoLXe0d5wPS2cBuRvvW0JyyJHMrk9/HmP2ZjrTboNAZ8iuGqaEIlKguljbQY32OkOJIRrgoA== dependencies: - caniuse-lite "^1.0.30001265" - electron-to-chromium "^1.3.867" + caniuse-lite "^1.0.30001271" + electron-to-chromium "^1.3.878" escalade "^3.1.1" - node-releases "^2.0.0" + node-releases "^2.0.1" picocolors "^1.0.0" buffer-crc32@^0.2.1, buffer-crc32@^0.2.13: @@ -2096,9 +2096,9 @@ builtins@^1.0.3: integrity sha1-y5T662HIaWRR2zZTThQi+U8K7og= bullmq@^1.47.1: - version "1.50.4" - resolved "https://registry.yarnpkg.com/bullmq/-/bullmq-1.50.4.tgz#235b3147cc631b197ab7c4b4e92a34a108657034" - integrity sha512-626b869aTmB8D9S2cK8r1V/PhaciALPW/QhWTXgBnIttrOLrLRpbyf3N2tMvJnKsIfOXePT4KV6P98q/M4fHdg== + version "1.51.1" + resolved "https://registry.yarnpkg.com/bullmq/-/bullmq-1.51.1.tgz#97cc505044f4abc0ca9425b333fc74449f3e9ed5" + integrity sha512-TPnf4PTDIR56GW3fO01TN6b6Gv88CLKDPwJ4ueRESaLHbZTCDMkNsi4xP4dxhn8gWgwid1DJo/ssKjsQFAvxnA== dependencies: cron-parser "^2.18.0" get-port "^5.1.1" @@ -2211,10 +2211,10 @@ camelcase@^6.2.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.2.0.tgz#924af881c9d525ac9d87f40d964e5cea982a1809" integrity sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg== -caniuse-lite@^1.0.30001265: - version "1.0.30001270" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001270.tgz#cc9c37a4ec5c1a8d616fc7bace902bb053b0cdea" - integrity sha512-TcIC7AyNWXhcOmv2KftOl1ShFAaHQYcB/EPL/hEyMrcS7ZX0/DvV1aoy6BzV0+16wTpoAyTMGDNAJfSqS/rz7A== +caniuse-lite@^1.0.30001271: + version "1.0.30001272" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001272.tgz#8e9790ff995e9eb6e1f4c45cd07ddaa87cddbb14" + integrity sha512-DV1j9Oot5dydyH1v28g25KoVm7l8MTxazwuiH3utWiAS6iL/9Nh//TGwqFEeqqN8nnWYQ8HHhUq+o4QPt9kvYw== caseless@~0.12.0: version "0.12.0" @@ -2315,9 +2315,9 @@ ci-parallel-vars@^1.0.1: integrity sha512-uvzpYrpmidaoxvIQHM+rKSrigjOe9feHYbw4uOI2gdfe1C3xIlxO+kVXq83WQWNniTf8bAxVpy+cQeFQsMERKg== clean-css@^4.2.1: - version "4.2.3" - resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.3.tgz#507b5de7d97b48ee53d84adb0160ff6216380f78" - integrity sha512-VcMWDN54ZN/DS+g58HYL5/n4Zrqe8vHJpGA8KdgUXFU4fuP/aHNw8eld9SyEIyabIMJX/0RaY/fplOo5hYLSFA== + version "4.2.4" + resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.4.tgz#733bf46eba4e607c6891ea57c24a989356831178" + integrity sha512-EJUDT7nDVFDvaQgAo2G/PJvxmp1o/c6iXLbswsBbUFXi1Nr+AjA2cKmfbKDMjMvzEe75g3P6JkaDDAKk96A85A== dependencies: source-map "~0.6.0" @@ -2484,9 +2484,9 @@ commander@^5.1.0: integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg== commander@^8.2.0: - version "8.2.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-8.2.0.tgz#37fe2bde301d87d47a53adeff8b5915db1381ca8" - integrity sha512-LLKxDvHeL91/8MIyTAD5BFMNtoIwztGPMiM/7Bl8rIPmHCZXRxmSWr91h57dpOpnQ6jIUqEWdXE/uBYMfiVZDA== + version "8.3.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" + integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== common-path-prefix@^3.0.0: version "3.0.0" @@ -2550,7 +2550,7 @@ concordance@^5.0.1: semver "^7.3.2" well-known-symbols "^2.0.0" -concurrently@^6.2.1: +concurrently@^6.0.0, concurrently@^6.2.1: version "6.3.0" resolved "https://registry.yarnpkg.com/concurrently/-/concurrently-6.3.0.tgz#63128cb4a6ed54d3c0ed8528728590a5fe54582a" integrity sha512-k4k1jQGHHKsfbqzkUszVf29qECBrkvBKkcPJEUDTyVR7tZd1G/JOfnst4g1sYbFvJ4UjHZisj1aWQR8yLKpGPw== @@ -2663,9 +2663,9 @@ conventional-commits-filter@^2.0.7: modify-values "^1.0.0" conventional-commits-parser@^3.2.0: - version "3.2.2" - resolved "https://registry.yarnpkg.com/conventional-commits-parser/-/conventional-commits-parser-3.2.2.tgz#190fb9900c6e02be0c0bca9b03d57e24982639fd" - integrity sha512-Jr9KAKgqAkwXMRHjxDwO/zOCDKod1XdAESHAGuJX38iZ7ZzVti/tvVoysO0suMsdAObp9NQ2rHSsSbnAqZ5f5g== + version "3.2.3" + resolved "https://registry.yarnpkg.com/conventional-commits-parser/-/conventional-commits-parser-3.2.3.tgz#fc43704698239451e3ef35fd1d8ed644f46bd86e" + integrity sha512-YyRDR7On9H07ICFpRm/igcdjIqebXbvf4Cff+Pf0BrBys1i1EOzx9iFXNlAbdrLAR8jf7bkUYkDAr8pEy0q4Pw== dependencies: JSONStream "^1.0.4" is-text-path "^1.0.1" @@ -3207,10 +3207,10 @@ ee-first@1.1.1: resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= -electron-to-chromium@^1.3.867: - version "1.3.873" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.873.tgz#c238c9199e4951952fe815a65c1beab5db4826b8" - integrity sha512-TiHlCgl2uP26Z0c67u442c0a2MZCWZNCRnPTQDPhVJ4h9G6z2zU0lApD9H0K9R5yFL5SfdaiVsVD2izOY24xBQ== +electron-to-chromium@^1.3.878: + version "1.3.884" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.884.tgz#0cd8c3a80271fd84a81f284c60fb3c9ecb33c166" + integrity sha512-kOaCAa+biA98PwH5BpCkeUeTL6mCeg8p3Q3OhqzPyqhu/5QUnWAN2wr/3IK8xMQxIV76kfoQpP+Bn/wij/jXrg== emittery@^0.8.0: version "0.8.1" @@ -3497,7 +3497,7 @@ eyes@0.1.x: resolved "https://registry.yarnpkg.com/eyes/-/eyes-0.1.8.tgz#62cf120234c683785d902348a800ef3e0cc20bc0" integrity sha1-Ys8SAjTGg3hdkCNIqADvPgzCC8A= -faker@^5.4.0: +faker@^5.5.0: version "5.5.3" resolved "https://registry.yarnpkg.com/faker/-/faker-5.5.3.tgz#c57974ee484431b25205c2c8dc09fda861e51e0e" integrity sha512-wLTv2a28wjUyWkbnX7u/ABZBkUkIF2fCd73V6P2oFqEGEktDfzWx4UxrSqtPRw0xPRAcjeAOIiJWqZm3pP4u3g== @@ -4881,9 +4881,9 @@ latest-version@^5.1.0: package-json "^6.3.0" lazystream@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/lazystream/-/lazystream-1.0.0.tgz#f6995fe0f820392f61396be89462407bb77168e4" - integrity sha1-9plf4PggOS9hOWvolGJAe7dxaOQ= + version "1.0.1" + resolved "https://registry.yarnpkg.com/lazystream/-/lazystream-1.0.1.tgz#494c831062f1f9408251ec44db1cba29242a2638" + integrity sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw== dependencies: readable-stream "^2.0.5" @@ -5884,9 +5884,9 @@ ms@^2.0.0, ms@^2.1.1, ms@^2.1.3: integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== msgpackr-extract@^1.0.14: - version "1.0.14" - resolved "https://registry.yarnpkg.com/msgpackr-extract/-/msgpackr-extract-1.0.14.tgz#87d3fe825d226e7f3d9fe136375091137f958561" - integrity sha512-t8neMf53jNZRF+f0H9VvEUVvtjGZ21odSBRmFfjZiyxr9lKYY0mpY3kSWZAIc7YWXtCZGOvDQVx2oqcgGiRBrw== + version "1.0.15" + resolved "https://registry.yarnpkg.com/msgpackr-extract/-/msgpackr-extract-1.0.15.tgz#3010a3ff0b033782d525116071b6c32864a79db2" + integrity sha512-vgJgzFva0/4/mt84wXf3CRCDPHKqiqk5t7/kVSjk/V2IvwSjoStHhxyq/b2+VrWcch3sxiNQOJEWXgI86Fm7AQ== dependencies: nan "^2.14.2" node-gyp-build "^4.2.3" @@ -6019,10 +6019,10 @@ node-preload@^0.2.1: dependencies: process-on-spawn "^1.0.0" -node-releases@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.0.tgz#67dc74903100a7deb044037b8a2e5f453bb05400" - integrity sha512-aA87l0flFYMzCHpTM3DERFSYxc6lv/BltdbRTOMZuxZ0cwZCD3mejE5n9vLhSJCN++/eOqr77G1IO5uXxlQYWA== +node-releases@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.1.tgz#3d1d395f204f1f2f29a54358b9fb678765ad2fc5" + integrity sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA== nodemailer@^6.6.3: version "6.7.0" @@ -7523,9 +7523,9 @@ shebang-regex@^3.0.0: integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== shell-quote@^1.6.1: - version "1.7.2" - resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.2.tgz#67a7d02c76c9da24f99d20808fcaded0e0e04be2" - integrity sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg== + version "1.7.3" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.3.tgz#aa40edac170445b9a431e17bb62c0b881b9c4123" + integrity sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw== side-channel@^1.0.4: version "1.0.4" @@ -8119,14 +8119,14 @@ trim-newlines@^3.0.0: integrity sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw== trim-off-newlines@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/trim-off-newlines/-/trim-off-newlines-1.0.2.tgz#65d52e4f4115992c0dac87220bdcc279fe58c422" - integrity sha512-DAnbtY4lNoOTLw05HLuvPoBFAGV4zOKQ9d1Q45JB+bcDwYIEkCr0xNgwKtygtKFBbRlFA/8ytkAM1V09QGWksg== + version "1.0.3" + resolved "https://registry.yarnpkg.com/trim-off-newlines/-/trim-off-newlines-1.0.3.tgz#8df24847fcb821b0ab27d58ab6efec9f2fe961a1" + integrity sha512-kh6Tu6GbeSNMGfrrZh6Bb/4ZEHV1QlB4xNDBeog8Y9/QwFlKTRyWvY3Fs9tRDAMZliVUwieMgEdIeL/FtqjkJg== ts-node@^10.2.1: - version "10.3.0" - resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.3.0.tgz#a797f2ed3ff50c9a5d814ce400437cb0c1c048b4" - integrity sha512-RYIy3i8IgpFH45AX4fQHExrT8BxDeKTdC83QFJkNzkvt8uFB6QJ8XMyhynYiKMLxt9a7yuXaDBZNOYS3XjDcYw== + version "10.4.0" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.4.0.tgz#680f88945885f4e6cf450e7f0d6223dd404895f7" + integrity sha512-g0FlPvvCXSIO1JDF6S232P5jPYqBkRL9qly81ZgAOSU7rwI0stphCgd2kLiCrU9DjQCrJMWEqcNSjQL02s6d8A== dependencies: "@cspotcode/source-map-support" "0.7.0" "@tsconfig/node10" "^1.0.7" diff --git a/dashboard/src/app/core/entities/campaign/api-format/campaign.formater.ts b/dashboard/src/app/core/entities/campaign/api-format/campaign.formater.ts index 1707b38a62..0b1a17e3d5 100644 --- a/dashboard/src/app/core/entities/campaign/api-format/campaign.formater.ts +++ b/dashboard/src/app/core/entities/campaign/api-format/campaign.formater.ts @@ -538,7 +538,7 @@ export class CampaignFormater { // UI_STATUS ui_status.for_passenger = !!ui_status.for_passenger; ui_status.for_driver = !!ui_status.for_driver; - ui_status.for_trip = !!ui_status.for_trip; + // ui_status.for_trip = !!ui_status.for_trip; const apiData: CampaignInterface = { _id, diff --git a/dashboard/src/app/core/entities/campaign/api-format/campaign.ts b/dashboard/src/app/core/entities/campaign/api-format/campaign.ts index fc4b0ffe1e..8e624e9cc5 100644 --- a/dashboard/src/app/core/entities/campaign/api-format/campaign.ts +++ b/dashboard/src/app/core/entities/campaign/api-format/campaign.ts @@ -50,7 +50,7 @@ export class Campaign extends BaseModel implements FormModel, Model, MapModel 0, diff --git a/dashboard/src/app/modules/campaign/components/campaign-form/step-2/filters-form.component.html b/dashboard/src/app/modules/campaign/components/campaign-form/step-2/filters-form.component.html index 767e39ad58..945b6766f4 100644 --- a/dashboard/src/app/modules/campaign/components/campaign-form/step-2/filters-form.component.html +++ b/dashboard/src/app/modules/campaign/components/campaign-form/step-2/filters-form.component.html @@ -129,13 +129,7 @@

Définissez des conditions d'éligibilités

- + Cible * @@ -158,7 +152,7 @@

Définissez des conditions d'éligibilités

> Passagers - + { - const fn = `${v[0] || v[1] ? 'en' : 'dis'}able`; - this.campaignForm.get('only_adult')[fn](); - }); - + // Activate the adult_only checkbox for passengers + this.forPassengerControl.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((v) => { + const fn = `${v ? 'en' : 'dis'}able`; + this.campaignForm.get('only_adult')[fn](); + }); this.forDriverControl.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((checked) => { if (checked) { - this.forTripControl.setValue(false); + // this.forTripControl.setValue(false); if (!this.forPassengerControl.value) { this.campaignForm.get('only_adult').setValue(null); } @@ -203,17 +199,17 @@ export class FiltersFormComponent extends DestroyObservable implements OnInit, A }); this.forPassengerControl.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((checked) => { if (checked) { - this.forTripControl.setValue(false); + // this.forTripControl.setValue(false); } else if (this.forDriverControl.value) { this.campaignForm.get('only_adult').setValue(null); } }); - this.forTripControl.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((checked) => { - if (checked) { - this.forPassengerControl.setValue(false); - this.forDriverControl.setValue(false); - } - }); + // this.forTripControl.valueChanges.pipe(takeUntil(this.destroy$)).subscribe((checked) => { + // if (checked) { + // this.forPassengerControl.setValue(false); + // this.forDriverControl.setValue(false); + // } + // }); } private initSelectedInseeFilterTabIndex(): void { diff --git a/dashboard/src/app/modules/campaign/components/campaign-form/step-3/parameters-form.component.html b/dashboard/src/app/modules/campaign/components/campaign-form/step-3/parameters-form.component.html index 8557b54d97..d4f314a02c 100644 --- a/dashboard/src/app/modules/campaign/components/campaign-form/step-3/parameters-form.component.html +++ b/dashboard/src/app/modules/campaign/components/campaign-form/step-3/parameters-form.component.html @@ -196,7 +196,7 @@

Définissez des rétributions

> - Rétribution + Rétribution *
@@ -240,13 +240,13 @@

Définissez des rétributions

*ngIf="forPassengerControl.value" > - - + --> Prendre en compte l'ensemble des places réservées par les passagers  + + Prendre en compte l'ensemble des places réservées par les passagers  info + color="accent"> + info + +
diff --git a/dashboard/src/app/modules/campaign/components/campaign-form/step-3/parameters-form.component.ts b/dashboard/src/app/modules/campaign/components/campaign-form/step-3/parameters-form.component.ts index 4c38e3cab3..0eab9c61d1 100644 --- a/dashboard/src/app/modules/campaign/components/campaign-form/step-3/parameters-form.component.ts +++ b/dashboard/src/app/modules/campaign/components/campaign-form/step-3/parameters-form.component.ts @@ -68,9 +68,9 @@ export class ParametersFormComponent extends DestroyObservable implements OnInit return this.campaignForm.get('ui_status').get('for_passenger') as FormControl; } - get forTripControl(): FormControl { - return this.campaignForm.get('ui_status').get('for_trip') as FormControl; - } + // get forTripControl(): FormControl { + // return this.campaignForm.get('ui_status').get('for_trip') as FormControl; + // } get restrictionFormArray(): FormArray { return this.campaignForm.get('restrictions') as FormArray; diff --git a/dashboard/src/app/modules/campaign/components/campaign-form/step-3/retribution-form/retribution-form.component.html b/dashboard/src/app/modules/campaign/components/campaign-form/step-3/retribution-form/retribution-form.component.html index aec52a4538..4d9f5cad9f 100644 --- a/dashboard/src/app/modules/campaign/components/campaign-form/step-3/retribution-form/retribution-form.component.html +++ b/dashboard/src/app/modules/campaign/components/campaign-form/step-3/retribution-form/retribution-form.component.html @@ -1,9 +1,11 @@ -
+ +

Pour le conducteur Pour le(s) passager(s) - Pour le trajet - + + + gratuit

@@ -16,17 +18,16 @@

Montant - + {{ incentiveUnitFr[campaignFormControls.unit.value] }} - par trajet - - par km - - par passager + + par trajet + par km +

diff --git a/dashboard/src/app/modules/campaign/components/campaign-form/step-3/retribution-form/retribution-form.component.ts b/dashboard/src/app/modules/campaign/components/campaign-form/step-3/retribution-form/retribution-form.component.ts index a113ac49c3..b83b0b822d 100644 --- a/dashboard/src/app/modules/campaign/components/campaign-form/step-3/retribution-form/retribution-form.component.ts +++ b/dashboard/src/app/modules/campaign/components/campaign-form/step-3/retribution-form/retribution-form.component.ts @@ -13,19 +13,10 @@ export class RetributionFormComponent extends DestroyObservable implements OnIni @Input() campaignForm: FormGroup; @Input() forDriver: boolean; @Input() forPassenger: boolean; - @Input() forTrip = false; + // @Input() forTrip = false; @Input() formGroup: FormGroup; incentiveUnitFr = INCENTIVE_UNITS_FR; - constructor() { - super(); - } - - ngOnInit(): void { - this.setValidators(); - this.initOnChange(); - } - get uiStatusControl(): FormControl { return this.campaignForm.get('ui_status') as FormControl; } @@ -54,6 +45,15 @@ export class RetributionFormComponent extends DestroyObservable implements OnIni return this.campaignForm.controls.unit.value === IncentiveUnitEnum.EUR; } + constructor() { + super(); + } + + ngOnInit(): void { + this.setValidators(); + this.initOnChange(); + } + private initOnChange(): void { this.uiStatusControl.valueChanges.subscribe(() => { this.setValidators(); @@ -75,27 +75,10 @@ export class RetributionFormComponent extends DestroyObservable implements OnIni const uiStatus = this.uiStatusControl.value; const free = this.freeControl.value; const validation = { - driver: false, - passenger: false, + driver: !!uiStatus.driver, + passenger: free ? false : !!uiStatus.passenger, }; - // set validators according to differents stats - if (uiStatus.for_driver && !uiStatus.for_passenger) { - validation.driver = true; - validation.passenger = false; - } - if (uiStatus.for_passenger && !uiStatus.for_driver) { - validation.passenger = true; - validation.driver = false; - } - if ((uiStatus.for_driver && uiStatus.for_passenger) || uiStatus.for_trip) { - validation.passenger = true; - validation.driver = true; - } - if (free) { - validation.passenger = false; - } - validation.driver ? this.forDriverFormGroup.get('amount').setValidators(validators) : this.forDriverFormGroup.get('amount').clearValidators(); diff --git a/dashboard/src/app/modules/campaign/components/campaign-simulation-pane/campaign-simulation-pane.component.html b/dashboard/src/app/modules/campaign/components/campaign-simulation-pane/campaign-simulation-pane.component.html index 9dd7b4da39..666dcaa261 100644 --- a/dashboard/src/app/modules/campaign/components/campaign-simulation-pane/campaign-simulation-pane.component.html +++ b/dashboard/src/app/modules/campaign/components/campaign-simulation-pane/campaign-simulation-pane.component.html @@ -21,4 +21,7 @@
Incitations distribuées

{{ state.amount }}

+
+

La simulation n'a pu être calculée.

+
diff --git a/dashboard/src/app/modules/campaign/components/campaign-simulation-pane/campaign-simulation-pane.component.ts b/dashboard/src/app/modules/campaign/components/campaign-simulation-pane/campaign-simulation-pane.component.ts index 7b8b2c0841..f2a0d35696 100644 --- a/dashboard/src/app/modules/campaign/components/campaign-simulation-pane/campaign-simulation-pane.component.ts +++ b/dashboard/src/app/modules/campaign/components/campaign-simulation-pane/campaign-simulation-pane.component.ts @@ -2,8 +2,8 @@ import * as moment from 'moment'; import { format, subDays, subMonths } from 'date-fns'; import { fr } from 'date-fns/locale'; import { omit } from 'lodash-es'; -import { BehaviorSubject, combineLatest } from 'rxjs'; -import { debounceTime, filter, map, switchMap, takeUntil, tap } from 'rxjs/operators'; +import { BehaviorSubject, combineLatest, throwError } from 'rxjs'; +import { catchError, debounceTime, filter, map, switchMap, takeUntil, tap } from 'rxjs/operators'; import { CurrencyPipe } from '@angular/common'; import { Component, Input, OnChanges, OnInit, SimpleChange } from '@angular/core'; @@ -38,6 +38,9 @@ export class CampaignSimulationPaneComponent extends DestroyObservable implement public timeState = getTimeState(1); public range$ = new BehaviorSubject(1); public simulatedCampaign$ = new BehaviorSubject(null); + public errors = { + simulation_failed: false, + }; get months(): number { return this.range$.value; @@ -55,7 +58,10 @@ export class CampaignSimulationPaneComponent extends DestroyObservable implement combineLatest([this.range$, this.simulatedCampaign$]) .pipe( debounceTime(250), - tap(() => (this.loading = true)), + tap(() => { + this.loading = true; + Object.keys(this.errors).forEach((key) => (this.errors[key] = false)); + }), filter(([, campaign]) => this.auth.user && (!!campaign.territory_id || !!this.auth.user.territory_id)), map(([r, c]: [number, CampaignUx]) => { this.timeState = getTimeState(r); @@ -67,7 +73,15 @@ export class CampaignSimulationPaneComponent extends DestroyObservable implement return c; }), map(CampaignFormater.toApi), - switchMap((c) => this.campaignApi.simulate(c)), + switchMap((c) => + this.campaignApi.simulate(c).pipe( + catchError((err) => { + this.errors.simulation_failed = true; + this.loading = false; + return throwError(err); + }), + ), + ), takeUntil(this.destroy$), ) .subscribe((state: CampaignReducedStats) => { diff --git a/dashboard/src/app/modules/campaign/modules/campaign-ui/components/campaign-rules-view/campaign-rules-view.component.ts b/dashboard/src/app/modules/campaign/modules/campaign-ui/components/campaign-rules-view/campaign-rules-view.component.ts index 53935578b0..666cefbedd 100644 --- a/dashboard/src/app/modules/campaign/modules/campaign-ui/components/campaign-rules-view/campaign-rules-view.component.ts +++ b/dashboard/src/app/modules/campaign/modules/campaign-ui/components/campaign-rules-view/campaign-rules-view.component.ts @@ -36,9 +36,10 @@ export class CampaignRulesViewComponent implements OnInit { get targets(): string { const forDriver = this.campaign.ui_status.for_driver; const forPassenger = this.campaign.ui_status.for_passenger; - const forTrip = this.campaign.ui_status.for_trip; + // const forTrip = this.campaign.ui_status.for_trip; const onlyAdult = this.campaign.only_adult; - return this._campaignUiService.targets(forDriver, forPassenger, forTrip, onlyAdult); + // return this._campaignUiService.targets(forDriver, forPassenger, forTrip, onlyAdult); + return this._campaignUiService.targets(forDriver, forPassenger, onlyAdult); } get ranks(): string { diff --git a/dashboard/src/app/modules/campaign/services/campaign-ui.service.ts b/dashboard/src/app/modules/campaign/services/campaign-ui.service.ts index c5d32047aa..b6a06311b9 100644 --- a/dashboard/src/app/modules/campaign/services/campaign-ui.service.ts +++ b/dashboard/src/app/modules/campaign/services/campaign-ui.service.ts @@ -73,9 +73,9 @@ export class CampaignUiService { text += ` ${valueForDriver} ${unit(valueForDriver, campaign.unit)} par trajet`; text += perKmForDriver ? ' par km' : ''; text += perPassenger ? ' par passager' : ''; - if (!uiStatus.for_trip) { - text += ' pour le conducteur'; - } + // if (!uiStatus.for_trip) { + // text += ' pour le conducteur'; + // } } // text += uiStatus.for_driver && uiStatus.for_passenger ? ', ' : ''; @@ -91,12 +91,12 @@ export class CampaignUiService { } // TRAJET - if (uiStatus.for_trip) { - // tslint:disable-next-line:max-line-length - text += ` ${valueForDriver} ${unit(valueForDriver, campaign.unit)} par trajet`; - text += perKmForDriver ? ' par km' : ''; - text += perPassenger ? ' par passager' : ''; - } + // if (uiStatus.for_trip) { + // // tslint:disable-next-line:max-line-length + // text += ` ${valueForDriver} ${unit(valueForDriver, campaign.unit)} par trajet`; + // text += perKmForDriver ? ' par km' : ''; + // text += perPassenger ? ' par passager' : ''; + // } text += `.`; } @@ -146,8 +146,10 @@ export class CampaignUiService { return ranks.join(', '); } - public targets(forDriver: boolean, forPassenger: boolean, forTrip: boolean, onlyAdult: boolean): string { - if (!(forDriver || forPassenger || forTrip)) { + // public targets(forDriver: boolean, forPassenger: boolean, forTrip: boolean, onlyAdult: boolean): string { + public targets(forDriver: boolean, forPassenger: boolean, onlyAdult: boolean): string { + // if (!(forDriver || forPassenger || forTrip)) { + if (!(forDriver || forPassenger)) { return ''; } let label = ''; @@ -160,12 +162,12 @@ export class CampaignUiService { label += ', majeurs uniquement'; } } - if (forTrip) { - label += 'Trajets'; - if (onlyAdult) { - label += ', passagers majeurs uniquement'; - } - } + // if (forTrip) { + // label += 'Trajets'; + // if (onlyAdult) { + // label += ', passagers majeurs uniquement'; + // } + // } return label; } diff --git a/docker-compose.yml b/docker-compose.yml index 6374da16a0..869fe784a5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -63,7 +63,6 @@ services: - redis - postgres - mailer - - worker - s3 worker: diff --git a/integration.sh b/integration.sh new file mode 100644 index 0000000000..d25785c975 --- /dev/null +++ b/integration.sh @@ -0,0 +1,65 @@ +DC="$(which docker-compose) -p pdce2e -f docker-compose.e2e.yml" +CERT_DIR="$(pwd)/docker/traefik/certs" + +generate_certs() { + echo "Generating certificates" + openssl genrsa -out $CERT_DIR/localCA.key 2048 + openssl req -x509 -new -nodes -key $CERT_DIR/localCA.key -sha256 -days 1825 -out $CERT_DIR/localCA.pem -subj "/C=FR/ST=Idf/L=Paris/O=Local PDC/CN=pdc/emailAddress=technique@covoiturage.beta.gouv.fr" + openssl genrsa -out $CERT_DIR/cert.key 2048 + openssl req -new -key $CERT_DIR/cert.key -out $CERT_DIR/cert.csr -subj "/C=FR/ST=Idf/L=Paris/O=Local PDC/CN=*.covoiturage.test/emailAddress=technique@covoiturage.beta.gouv.fr" + openssl x509 -req -in $CERT_DIR/cert.csr -CA $CERT_DIR/localCA.pem -CAkey $CERT_DIR/localCA.key -CAcreateserial -out $CERT_DIR/cert.crt -days 500 -sha256 +} + +rebuild() { + echo "Rebuilding app image" + $DC build api + $DC build dashboard +} + +start_services() { + echo "Start services" + echo "$DC up -d s3 postgres" + $DC up -d s3 postgres +} + +start_app() { + echo "Start app" + $DC up -d proxy +} + +wait_for_app() { + $DC run wait +} + +seed_data() { + echo "Seed data" + $DC run api yarn workspace @pdc/proxy ilos seed +} + +create_bucket() { + echo "Create bucket" + $DC run -e BUCKET=$1 s3-init +} + +integration() { + echo "Start integration test" + $DC run api sh -c "yarn install && yarn test:integration" +} + +stop() { + echo "Cleaning up" + $DC down -v +} + +if [ "$1" = "rebuild" ]; then + rebuild +fi + +if [ ! -f $CERT_DIR/cert.key ]; then + generate_certs +fi + +start_services && seed_data && create_bucket local-pdc-export && create_bucket local-pdc-public && start_app && wait_for_app && integration 2> /dev/null +EXIT=$? +stop +exit $EXIT diff --git a/shared/policy/apply.contract.ts b/shared/policy/apply.contract.ts index 7f29c1112d..27f5996ca0 100644 --- a/shared/policy/apply.contract.ts +++ b/shared/policy/apply.contract.ts @@ -1,5 +1,6 @@ export type ParamsInterface = { campaign_id?: number; + override_from?: Date; }; export type ResultInterface = void; diff --git a/shared/policy/finalize.contract.ts b/shared/policy/finalize.contract.ts index 1fc352d996..ef8bf6aaa0 100644 --- a/shared/policy/finalize.contract.ts +++ b/shared/policy/finalize.contract.ts @@ -1,4 +1,7 @@ -export type ParamsInterface = void; +export interface ParamsInterface { + to?: Date; + from?: Date; +} export type ResultInterface = void; diff --git a/tests/.eslintrc.js b/tests/.eslintrc.js deleted file mode 100644 index ce9269cbb3..0000000000 --- a/tests/.eslintrc.js +++ /dev/null @@ -1,37 +0,0 @@ -module.exports = { - parser: '@typescript-eslint/parser', - plugins: ['@typescript-eslint', 'cypress', 'prettier'], - settings: { - 'import/resolver': { - node: { - extensions: ['.js', '.jsx', '.ts', '.tsx'], - }, - }, - }, - extends: [ - 'plugin:@typescript-eslint/recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin - 'plugin:cypress/recommended', - 'prettier', - 'plugin:prettier/recommended', - ], - parserOptions: { - ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features - sourceType: 'module', // Allows for the use of imports - }, - rules: { - 'prettier/prettier': 'error', - 'max-len': ['warn', { code: 120 }], - '@typescript-eslint/indent': 'off', - '@typescript-eslint/no-use-before-define': 'warn', - '@typescript-eslint/camelcase': 'off', - '@typescript-eslint/explicit-function-return-type': 'off', - '@typescript-eslint/no-explicit-any': 'warn', - '@typescript-eslint/no-inferrable-types': [ - 'warn', - { - ignoreProperties: true, - ignoreParameters: true, - }, - ], - }, -}; diff --git a/tests/cypress/support/emails/mailhog.ts b/tests/cypress/support/emails/mailhog.ts index 1f406c2e2a..cbcf0822a1 100644 --- a/tests/cypress/support/emails/mailhog.ts +++ b/tests/cypress/support/emails/mailhog.ts @@ -112,7 +112,7 @@ async function mybox(kind: string, query: string, empty = false): Promise(fn: Function, ...args: any[]): Promise { - let tries = [1000, 3000, 5000, 10000, 30000]; + const tries = [1000, 3000, 5000, 10000, 30000]; let error = null; for (const time of tries) { try {