├── .dockerignore ├── .github ├── dependabot.yml ├── pull_request_template.md └── workflows │ ├── actionlint.yml │ ├── ci.yml │ ├── copy-pr-template-to-dependabot-prs.yml │ ├── deploy.yml │ ├── pact-verify.yml │ └── release.yml ├── .gitignore ├── .govuk_dependabot_merger.yml ├── .rspec ├── .rubocop.yml ├── .ruby-version ├── Dockerfile ├── Gemfile ├── Gemfile.lock ├── LICENCE ├── README.md ├── Rakefile ├── app ├── builders │ ├── bulk_migrate_confirmation_email_builder.rb │ ├── bulk_subscriber_list_email_builder.rb │ ├── bulk_subscriber_list_email_builder_with_account.rb │ ├── digest_email_builder.rb │ ├── immediate_email_builder.rb │ ├── linked_account_email_builder.rb │ ├── subscriber_auth_email_builder.rb │ ├── subscription_auth_email_builder.rb │ └── subscription_confirmation_email_builder.rb ├── controllers │ ├── application_controller.rb │ ├── content_changes_controller.rb │ ├── spam_reports_controller.rb │ ├── status_updates_controller.rb │ ├── subscriber_lists_controller.rb │ ├── subscribers_auth_token_controller.rb │ ├── subscribers_controller.rb │ ├── subscribers_govuk_account_controller.rb │ ├── subscriptions_auth_token_controller.rb │ ├── subscriptions_controller.rb │ └── unsubscribe_controller.rb ├── models │ ├── application_record.rb │ ├── content_change.rb │ ├── digest_run.rb │ ├── digest_run_subscriber.rb │ ├── email.rb │ ├── frequency.rb │ ├── matched_content_change.rb │ ├── matched_message.rb │ ├── message.rb │ ├── subscriber.rb │ ├── subscriber_list.rb │ ├── subscription.rb │ ├── subscription_content.rb │ └── user.rb ├── presenters │ ├── bulk_email_body_presenter.rb │ ├── content_change_presenter.rb │ ├── footer_presenter.rb │ └── message_presenter.rb ├── queries │ ├── digest_items_query.rb │ ├── digest_run_subscriber_query.rb │ ├── email_criteria_query.rb │ ├── find_exact_query.rb │ ├── find_latest_matching_subscription.rb │ ├── find_without_links_and_tags_and_content_id.rb │ ├── matched_for_notification.rb │ ├── subscriber_list_query.rb │ ├── subscriber_lists_by_content_item_query.rb │ ├── subscriber_lists_by_criteria_query.rb │ ├── subscriber_lists_by_path_query.rb │ └── subscriber_lists_for_finder_query.rb ├── services │ ├── auth_token_generator_service.rb │ ├── bulk_unsubscribe_list_service.rb │ ├── check_notify_email_service.rb │ ├── content_change_handler_service.rb │ ├── create_subscriber_list_service.rb │ ├── create_subscription_service.rb │ ├── digest_initiator_service.rb │ ├── immediate_email_generation_service.rb │ ├── immediate_email_generation_service │ │ └── batch.rb │ ├── matched_content_change_generation_service.rb │ ├── matched_message_generation_service.rb │ ├── merge_subscribers_service.rb │ ├── send_email_service.rb │ ├── send_email_service │ │ ├── send_notify_email.rb │ │ └── send_pseudo_email.rb │ ├── unsubscribe_all_service.rb │ └── update_last_alerted_at_subscriber_list_service.rb ├── validators │ ├── criteria_schema_validator.rb │ ├── email_address_validator.rb │ ├── links_validator.rb │ ├── root_relative_url_validator.rb │ ├── tags_validator.rb │ └── uuid_validator.rb └── workers │ ├── application_worker.rb │ ├── bulk_migrate_list_worker.rb │ ├── bulk_unsubscribe_list_worker.rb │ ├── daily_digest_initiator_worker.rb │ ├── digest_email_generation_worker.rb │ ├── digest_run_completion_marker_worker.rb │ ├── email_deletion_worker.rb │ ├── historical_data_deletion_worker.rb │ ├── metrics_collection_worker.rb │ ├── metrics_collection_worker │ ├── base_exporter.rb │ ├── content_change_exporter.rb │ ├── digest_run_exporter.rb │ └── message_exporter.rb │ ├── nullify_subscribers_worker.rb │ ├── polling_alert_check_worker.rb │ ├── process_content_change_worker.rb │ ├── process_message_worker.rb │ ├── recover_lost_jobs_worker.rb │ ├── recover_lost_jobs_worker │ ├── missing_digest_runs_check.rb │ ├── old_pending_emails_check.rb │ └── unprocessed_check.rb │ ├── send_email_worker.rb │ ├── subscriber_list_audit_worker.rb │ └── weekly_digest_initiator_worker.rb ├── bin ├── brakeman ├── bundle ├── dev ├── rails ├── rake ├── rubocop ├── setup └── update ├── config.ru ├── config ├── application.rb ├── boot.rb ├── brakeman.ignore ├── bulk_email │ └── email_addresses.txt ├── database.yml ├── environment.rb ├── environments │ ├── development.rb │ ├── production.rb │ └── test.rb ├── initializers │ ├── application_controller_renderer.rb │ ├── assets.rb │ ├── backtrace_silencers.rb │ ├── cookies_serializer.rb │ ├── filter_parameter_logging.rb │ ├── gds_sso.rb │ ├── govuk_error.rb │ ├── inflections.rb │ ├── mime_types.rb │ ├── prometheus.rb │ ├── secrets_to_credentials.rb │ ├── session_store.rb │ ├── sidekiq.rb │ ├── wrap_parameters.rb │ └── zeitwerk.rb ├── locales │ └── en.yml ├── puma.rb ├── routes.rb ├── secrets.yml ├── sidekiq.yml └── spring.rb ├── db ├── migrate │ ├── 20141025105641_create_subscriber_list.rb │ ├── 20150428072646_seed_policy_subscriptions.csv │ ├── 20150428072646_seed_policy_subscriptions.rb │ ├── 20150428072646_seed_policy_subscriptions_mapping.csv │ ├── 20150508081921_reslug_social_equality.rb │ ├── 20151001102405_add_links_to_subscriber_list.rb │ ├── 20151001154627_add_null_false_to_tags_field.rb │ ├── 20151102165117_rename_policy_links_to_parent.rb │ ├── 20151111124933_remove_policy_subscriber_lists_with_erroneous_links.rb │ ├── 20151111150405_rename_parent_links_to_policies.rb │ ├── 20151112144850_rename_policy_tags_to_policies.rb │ ├── 20160205102023_add_document_type_to_subscriber_list.rb │ ├── 20160209143102_add_foreign_travel_advice_subscriber_lists.csv │ ├── 20160209143102_add_foreign_travel_advice_subscriber_lists.rb │ ├── 20160215144137_add_content_i_ds_to_travel_advice_topics.csv │ ├── 20160215144137_add_content_i_ds_to_travel_advice_topics.rb │ ├── 20160224152054_add_all_countries_subscriber_list.rb │ ├── 20160718090427_add_json_columns_to_subscriber_list.rb │ ├── 20160905121642_remove_hstore.rb │ ├── 20170208150700_remove_temp_json_fields.rb │ ├── 20170221141514_create_notification_log.rb │ ├── 20170302162543_add_enabled_flag_to_subscriber_lists.rb │ ├── 20170302162818_add_enabled_disabled_gov_delivery_ids_to_notification_log.rb │ ├── 20170320170223_add_supertype_fields.rb │ ├── 20170327104203_add_migrated_from_gov_uk_delivery_to_subscriber_list.rb │ ├── 20170720135533_add_subscriber_count_to_subscriber_lists.rb │ ├── 20170922091721_remove_redundant_columns_after_q2_mission.rb │ ├── 20171016143522_create_subscribers.rb │ ├── 20171019072332_make_subscriber_address_not_null.rb │ ├── 20171019072924_make_subscriber_address_unique.rb │ ├── 20171020071004_create_subscriptions.rb │ ├── 20171020091213_create_notifications.rb │ ├── 20171023094334_create_emails.rb │ ├── 20171109085657_create_delivery_attempt.rb │ ├── 20171109091838_add_address_to_email.rb │ ├── 20171110163345_make_subscriber_address_nullable.rb │ ├── 20171113162352_rename_notifications_to_content_changes.rb │ ├── 20171113163731_update_emails_reference_from_notifications_to_content_changes.rb │ ├── 20171114171050_add_subscription_content.rb │ ├── 20171115162823_index_delivery_attempt_email_id_updated_at.rb │ ├── 20171123142518_add_uuid_to_subscriptions.rb │ ├── 20171124093729_add_processed_at_to_content_change.rb │ ├── 20171124105714_uuid_constraints.rb │ ├── 20171130194217_create_users.rb │ ├── 20171201082903_remove_null_gov_delivery_ids.rb │ ├── 20171201094128_make_subscriber_list_title_not_null.rb │ ├── 20171205114740_remove_duplicate_subscriber_lists_and_add_unique_constraint.rb │ ├── 20171208081924_add_priority_to_change_content.rb │ ├── 20171212130557_add_indexes_to_subscriber_list.rb │ ├── 20171212133939_allow_nullable_subscription_contents_subscription_id.rb │ ├── 20171214113834_delete_test_emails_and_delivery_attempts.rb │ ├── 20171215154302_delivery_attempt_delete_cascade.rb │ ├── 20171215161358_cascade_nullify_subscription_contents_emails.rb │ ├── 20171215163908_cascade_nullify_subscription_contents_subscriptions.rb │ ├── 20171219103753_remove_duplicate_subscriber_list.rb │ ├── 20171219104253_make_subscriber_list_title_unique.rb │ ├── 20180116163006_store_signon_user_ids.rb │ ├── 20180118085957_add_frequency_to_subscriptions.rb │ ├── 20180118134516_create_digest_runs.rb │ ├── 20180123093411_add_matched_content_change.rb │ ├── 20180126144040_create_digest_run_subscribers.rb │ ├── 20180129081557_add_digest_run_subscriber_id_to_subscription_contents.rb │ ├── 20180205165003_add_deleted_at_to_subscriptions.rb │ ├── 20180212121358_add_unique_index_on_reference.rb │ ├── 20180215144634_add_case_insensitive_index_on_emails.rb │ ├── 20180216100846_add_subscription_contents_index.rb │ ├── 20180220125651_make_delivery_attempt_reference_uuid.rb │ ├── 20180222142356_add_subscriber_count_to_digest_run.rb │ ├── 20180223095104_make_various_primary_key_uui_ds.rb │ ├── 20180226101223_add_finished_sending_at_to_emails.rb │ ├── 20180226101630_index_finished_sending_at_on_emails.rb │ ├── 20180228112734_create_email_archive.rb │ ├── 20180228122502_add_archived_at_to_emails.rb │ ├── 20180228122604_index_archived_at_on_emails.rb │ ├── 20180228132051_add_deactivated_at_to_subscriber.rb │ ├── 20180228144454_add_source_to_subscriptions.rb │ ├── 20180301130127_remove_old_uuid_fields.rb │ ├── 20180301132513_add_subscription_ended_at.rb │ ├── 20180301141036_remove_deleted_at_from_subscription.rb │ ├── 20180301141950_add_ended_reason_to_subscriptions.rb │ ├── 20180301151800_remove_unique_index_on_subscriptions.rb │ ├── 20180301153539_add_unique_index_on_active_subscriptions.rb │ ├── 20180301180432_update_subscriber_list_titles.rb │ ├── 20180302090139_remove_incorrect_foreign_keys.rb │ ├── 20180302090154_add_foreign_key_on_deletes.rb │ ├── 20180305091124_increase_subscriber_list_title_length_limit.rb │ ├── 20180305094418_add_footnote_to_content_change.rb │ ├── 20180308105331_remove_subscriber_count_from_subscriber_lists.rb │ ├── 20180309114801_add_completed_at_column_to_delivery_attempt.rb │ ├── 20180312105539_add_sent_at_column_to_delivery_attempts.rb │ ├── 20180313081514_add_slug_to_subscriber_list.rb │ ├── 20180313081630_make_subscriber_list_gov_delivery_id_not_null.rb │ ├── 20180313081909_make_subscriber_list_gov_delivery_id_longer.rb │ ├── 20180313083354_populate_all_slug_fields.rb │ ├── 20180313084148_make_subscriber_list_slug_not_null.rb │ ├── 20180313090745_make_subscriber_list_slug_unique.rb │ ├── 20180313093530_make_subscriber_list_gov_delivery_id_nullable.rb │ ├── 20180313093731_remove_gov_delivery_id_from_subscriber_lists.rb │ ├── 20180313152401_remove_notification_log.rb │ ├── 20180315000000_make_subscription_id_on_subscription_contents_not_null.rb │ ├── 20180315000001_clear_out_duplicate_subscription_contents.rb │ ├── 20180315080842_add_subscriber_id_to_emails.rb │ ├── 20180315084923_add_unique_index_on_subscription_content.rb │ ├── 20180316115057_add_status_and_failure_reason_to_emails.rb │ ├── 20180316120328_add_status_and_failure_reasons_indexes_to_emails.rb │ ├── 20180316221116_set_email_statuses.rb │ ├── 20180316223209_change_email_status_default.rb │ ├── 20180321101329_index_created_updated_uuid_tables.rb │ ├── 20180321103043_add_address_index_to_emails.rb │ ├── 20180321104249_remove_status_and_failure_reason_indexes.rb │ ├── 20180321125113_add_email_status_indexes.rb │ ├── 20180501155601_update_land_registry_titles.rb │ ├── 20180618132321_remove_title_index_from_subscriber_list.rb │ ├── 20180625135732_add_index_to_subscription_content.rb │ ├── 20180625143326_disable_subscriptions_with_inactive_subscribers.rb │ ├── 20180628071838_add_index_to_content_change_processed_at.rb │ ├── 20180628072213_add_index_to_digest_run_completed_at.rb │ ├── 20180628072318_add_index_to_digest_run_created_at.rb │ ├── 20180628134912_add_exported_at_to_email_archives.rb │ ├── 20180628135936_add_indexes_to_email_archive.rb │ ├── 20180705095148_remove_email_archive_table.rb │ ├── 20180723091112_add_marked_as_spam_to_email.rb │ ├── 20180727115914_drop_unused_indexes.rb │ ├── 20180828153412_add_missing_policies_links_to_policy_subscriber_lists.rb │ ├── 20180910095917_add_ended_email_id_to_subscriptions.rb │ ├── 20180917150259_remove_ended_email_id_foreign_key_from_subscriptions.rb │ ├── 20181119131532_update_slug_and_title_limit.rb │ ├── 20190125145045_add_content_purpose_supergroup_to_subscriber_list.rb │ ├── 20190206130316_add_reject_content_purpose_supergroup_to_subscriber_list.rb │ ├── 20190313155146_remove_reject_content_purpose_supergroup_from_subscriber_list.rb │ ├── 20190412121520_add_facet_group_link_to_eu_exit_subscriber_lists.rb │ ├── 20190503114015_remove_content_purpose_supergroup_from_subscriber_list.rb │ ├── 20190522114020_add_type_to_subscriber_list.rb │ ├── 20190618111941_remove_type_from_subscriber_list.rb │ ├── 20190717121233_add_index_to_subscription_ended_at.rb │ ├── 20190814163542_create_messages.rb │ ├── 20190815150119_create_matched_messages.rb │ ├── 20190815191552_add_message_to_subscription_contents.rb │ ├── 20190815191553_validate_message_to_subscription_contents.rb │ ├── 20190815192452_subscription_contents_accept_nil_content_changes.rb │ ├── 20190815192913_unique_index_on_subscription_contents_messages.rb │ ├── 20190823114916_add_url_to_subscriber_lists.rb │ ├── 20190829070710_add_description_to_subscriber_lists.rb │ ├── 20190902130018_add_tags_links_digest_to_subscriber_lists.rb │ ├── 20190902133711_add_tags_links_digest_to_existing_subscriber_lists.rb │ ├── 20190903101929_add_criteria_rules_to_messages.rb │ ├── 20190904182908_remove_superfluous_message_fields.rb │ ├── 20190905162914_add_indexes_to_subscriber_list_digests.rb │ ├── 20190911112938_reduce_slug_lengths.rb │ ├── 20190911115859_remove_limits_on_subscriber_lists.rb │ ├── 20190912161623_amend_brexit_result_descriptions.rb │ ├── 20190913102634_add_group_id_to_subscriber_lists.rb │ ├── 20191011142014_add_partial_index_on_subscription_contents.rb │ ├── 20191014131300_remove_partial_index_on_subscription_contents.rb │ ├── 20191016072945_add_descriptions_to_travel_advice.rb │ ├── 20200123161117_update_brexit_subscriber_list_to_transition.rb │ ├── 20200203103706_update_brexit_checker_titles.rb │ ├── 20200212134444_remove_descriptions_from_travel_advice.rb │ ├── 20200310111954_rename_brexit_to_transition.rb │ ├── 20200406094740_rename_coronavirus_topical_event_sub.rb │ ├── 20200730170816_drop_failure_reason_from_emails.rb │ ├── 20200730172117_migrate_delivery_attempt_statuses.rb │ ├── 20200804143030_remove_marked_as_spam_from_email.rb │ ├── 20200810121128_remove_subscriber_count_from_digest_run.rb │ ├── 20200817151547_add_processed_at_to_digest_runs.rb │ ├── 20200818150135_unique_index_on_digest_run_subscribers.rb │ ├── 20200818172715_rename_completed_at_digest_run_subscriber.rb │ ├── 20200903094629_add_sent_at_to_emails.rb │ ├── 20200916095316_add_unique_index_for_digest_runs.rb │ ├── 20200916163844_remove_sent_at_and_completed_at_from_delivery_attempts.rb │ ├── 20200916164443_remove_finished_sending_at_from_emails.rb │ ├── 20201016164726_add_foreign_key_indexes_for_subscriber_id.rb │ ├── 20201019110929_add_index_for_digest_run_id_on_digest_run_subscribers.rb │ ├── 20201019112513_add_index_for_message_id_on_subscription_contents.rb │ ├── 20201019154701_delete_lingering_subscription_contents.rb │ ├── 20201020153450_change_foreign_key_constraint_for_emails.rb │ ├── 20201021153802_drop_delivery_attempts_table.rb │ ├── 20201110163036_remove_deactivated_at_from_subscribers.rb │ ├── 20201130145943_remove_email_archived_at.rb │ ├── 20201217084600_remove_group_id.rb │ ├── 20201218095119_remove_description_field.rb │ ├── 20210104090701_remove_message_url.rb │ ├── 20210309090225_update_brexit_data_subscriber_list_criteria.rb │ ├── 20210608161920_add_content_id_to_subscriber_lists.rb │ ├── 20210629083707_add_govuk_account_id_to_subscribers.rb │ ├── 20220105173124_use_text_for_long_subscriber_lists_columns.rb │ ├── 20220105174025_use_text_for_long_emails_columns.rb │ ├── 20220117130900_add_omit_footer_unsubscribe_link_to_messages.rb │ ├── 20220119090527_add_override_subscription_frequency_to_immediate_to_message.rb │ ├── 20220121161623_add_description_column_to_subscriber_lists.rb │ ├── 20231102140714_add_content_id_to_email.rb │ ├── 20231102150419_update_emails_add_notify_status_and_id_index.rb │ ├── 20240411100041_add_last_audited_at_to_subscriber_lists.rb │ ├── 20240513131857_add_last_alerted_at.rb │ ├── 20240522152228_add_subscription_id_to_email.rb │ └── data │ │ ├── subscriber-list-criteria-2021-03-08.csv │ │ └── subscriber-list-titles-2018-03-05.csv ├── schema.rb └── seeds.rb ├── docs ├── adr │ ├── adr-001-notify-integration.md │ ├── adr-002-digest-mvp.md │ ├── adr-002 │ │ ├── digests.png │ │ └── digests.puml │ ├── adr-003-initial-data-retention-strategy.md │ ├── adr-004-message-concept.md │ ├── adr-005-record-architecture-decisions.md │ ├── adr-006-email-delivery-responsibilities.md │ ├── adr-007-retain-data-for-up-to-one-year.md │ ├── adr-008-monitoring-and-alerting.md │ ├── adr-009-sidekiq-lost-job-recovery.md │ └── adr-010-send-unpublish-emails-for-single-pages.md ├── alert_check_scheduled_jobs.md ├── analytics.md ├── api.md ├── bulk-email.md ├── data-cleanup-mechanisms.md ├── env-vars.md ├── load-test-email-alert-api.md ├── matching-content-to-subscriber-lists.md ├── receiving-emails-from-email-alert-api-in-integration-and-staging.md ├── sidekiq-web.md ├── subscriber-list-audit.md └── support-tasks.md ├── lib ├── callable.rb ├── collectors │ └── global_prometheus_collector.rb ├── email_alert_criteria.rb ├── hash_digest.rb ├── metrics.rb ├── notifications_from_notify.rb ├── public_urls.rb ├── reports │ ├── concerns │ │ └── notification_stats.rb │ ├── finder_statistics_report.rb │ ├── future_content_change_statistics_report.rb │ ├── historical_content_change_statistics_report.rb │ ├── matched_content_changes_report.rb │ ├── potentially_dead_lists_report.rb │ ├── single_page_notifications_report.rb │ ├── subscriber_list_subscriber_count_report.rb │ ├── subscriber_lists_report.rb │ └── subscriber_lists_report_row.rb ├── search_alert_list.rb ├── services.rb ├── subscriber_list_mover.rb ├── symbolize_json.rb ├── tasks │ ├── alert_listeners.rake │ ├── bulk_email.rake │ ├── data_migration.rake │ ├── lint.rake │ ├── report.rake │ ├── subscriber_list_audit.rake │ └── support.rake └── valid_tags.rb ├── log └── .gitkeep └── spec ├── README.md ├── builders ├── bulk_migrate_confirmation_email_builder.rb ├── bulk_subscriber_list_email_builder_spec.rb ├── bulk_subscriber_list_email_builder_with_account_spec.rb ├── digest_email_builder_spec.rb ├── immediate_email_builder_spec.rb ├── linked_account_email_builder_spec.rb ├── subscriber_auth_email_builder_spec.rb ├── subscription_auth_email_builder_spec.rb └── subscription_confirmation_email_builder_spec.rb ├── factories.rb ├── features ├── README.md ├── creating_subscriber_list_spec.rb ├── daily_digest_spec.rb ├── login_verify_email_spec.rb ├── sending_email_spec.rb ├── single_content_item_notification_spec.rb ├── status_update_spec.rb ├── subscribing_spec.rb ├── unsubscribing_spec.rb └── weekly_digest_spec.rb ├── integration ├── README.md ├── anonymise_email_addresses_spec.rb ├── browsing_subscriber_lists_spec.rb ├── bulk_unsubscribe_spec.rb ├── create_subscriber_list_spec.rb ├── create_subscription_spec.rb ├── send_content_change_spec.rb ├── show_subscriber_list_metrics_spec.rb ├── show_subscriber_list_spec.rb ├── spam_reports_spec.rb ├── status_updates_spec.rb ├── subscribers_auth_token_spec.rb ├── subscribers_govuk_account_spec.rb ├── subscribers_spec.rb ├── subscriptions_auth_token_spec.rb ├── subscriptions_spec.rb ├── unsubscribe_spec.rb ├── update_subscription_list_spec.rb └── update_subscription_spec.rb ├── lib ├── email_alert_criteria_spec.rb ├── hash_digest_spec.rb ├── metrics_spec.rb ├── notifications_from_notify_spec.rb ├── public_urls_spec.rb ├── reports │ ├── finder_statistics_report_spec.rb │ ├── matched_content_changes_report_spec.rb │ ├── potentially_dead_lists_report_spec.rb │ ├── single_page_notifications_report_spec.rb │ ├── subscriber_list_subscriber_count_report_spec.rb │ └── subscriber_lists_report_spec.rb ├── subscriber_list_mover_spec.rb └── tasks │ ├── alert_listeners_spec.rb │ ├── bulk_email_spec.rb │ ├── data_migration_spec.rb │ ├── report_spec.rb │ ├── subscriber_list_audit_spec.rb │ └── support_spec.rb ├── models ├── digest_run_spec.rb ├── digest_run_subscriber_spec.rb ├── email_spec.rb ├── matched_content_change_spec.rb ├── matched_message_spec.rb ├── message_spec.rb ├── subscriber_list_spec.rb ├── subscriber_spec.rb ├── subscription_content_spec.rb ├── subscription_spec.rb └── user_spec.rb ├── presenters ├── bulk_email_body_presenter_spec.rb ├── content_change_presenter_spec.rb ├── footer_presenter_spec.rb └── message_presenter_spec.rb ├── queries ├── digest_items_query_spec.rb ├── digest_run_subscriber_query_spec.rb ├── find_exact_query_spec.rb ├── find_latest_matching_subscription_spec.rb ├── matched_for_notification_spec.rb ├── subscriber_list_query_spec.rb ├── subscriber_lists_by_criteria_query_spec.rb └── subscriber_lists_for_finder_query_spec.rb ├── service_consumers └── pact_helper.rb ├── services ├── auth_token_generator_service_spec.rb ├── bulk_unsubscribe_list_service_spec.rb ├── content_change_handler_service_spec.rb ├── create_subscriber_list_service_spec.rb ├── create_subscription_service_spec.rb ├── digest_initiator_service_spec.rb ├── immediate_email_generation_service │ └── batch_spec.rb ├── immediate_email_generation_service_spec.rb ├── matched_content_change_generation_service_spec.rb ├── matched_message_generation_service_spec.rb ├── merge_subscribers_service_spec.rb ├── send_email_service │ ├── send_notify_email_spec.rb │ └── send_pseudo_email_spec.rb ├── send_email_service_spec.rb ├── unsubscribe_all_service_spec.rb └── update_last_alerted_at_subscriber_list_service_spec.rb ├── spec_helper.rb ├── support ├── authentication_helpers.rb ├── content_item_helpers.rb ├── notify_request_helpers.rb ├── request_helpers.rb ├── search_alert_list_helpers.rb └── token_helpers.rb ├── validators ├── criteria_schema_validator_spec.rb ├── email_address_validator_spec.rb ├── links_validator_spec.rb ├── root_relative_url_validator_spec.rb ├── tags_validator_spec.rb └── uuid_validator_spec.rb └── workers ├── bulk_migrate_list_worker_spec.rb ├── bulk_unsubscribe_list_worker_spec.rb ├── daily_digest_initiator_worker_spec.rb ├── digest_email_generation_worker_spec.rb ├── digest_run_completion_marker_worker_spec.rb ├── email_deletion_worker_spec.rb ├── historical_data_deletion_worker_spec.rb ├── metrics_collection_worker ├── content_change_exporter_spec.rb ├── digest_run_exporter_spec.rb └── message_exporter_spec.rb ├── metrics_collection_worker_spec.rb ├── nullify_subscribers_worker_spec.rb ├── polling_alert_check_worker_spec.rb ├── process_content_change_worker_spec.rb ├── process_message_worker_spec.rb ├── recover_lost_jobs_worker ├── missing_digest_runs_check_spec.rb ├── old_pending_emails_check_spec.rb └── unprocessed_check_spec.rb ├── recover_lost_jobs_worker_spec.rb ├── send_email_worker_spec.rb ├── subscriber_list_audit_worker_spec.rb └── weekly_digest_initiator_worker_spec.rb /.dockerignore: -------------------------------------------------------------------------------- 1 | .dockerignore 2 | .git 3 | .gitignore 4 | .github 5 | Dockerfile 6 | Jenkinsfile 7 | Procfile 8 | README.md 9 | coverage 10 | docs 11 | features 12 | log 13 | node_modules 14 | script 15 | spec 16 | test 17 | tmp 18 | vendor 19 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: bundler 4 | directory: / 5 | schedule: 6 | interval: daily 7 | - package-ecosystem: docker 8 | directory: / 9 | schedule: 10 | interval: weekly 11 | ignore: 12 | - dependency-name: ruby 13 | - package-ecosystem: github-actions 14 | directory: / 15 | schedule: 16 | interval: daily 17 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ⚠️ This repo is Continuously Deployed: make sure you [follow the guidance](https://docs.publishing.service.gov.uk/manual/development-pipeline.html#merge-your-own-pull-request) ⚠️ 2 | 3 | Follow [these steps](https://guides.rubyonrails.org/upgrading_ruby_on_rails.html) if you are doing a Rails upgrade. 4 | -------------------------------------------------------------------------------- /.github/workflows/actionlint.yml: -------------------------------------------------------------------------------- 1 | name: Lint GitHub Actions 2 | on: 3 | push: 4 | paths: ['.github/**'] 5 | jobs: 6 | actionlint: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | with: 11 | show-progress: false 12 | - uses: alphagov/govuk-infrastructure/.github/actions/actionlint@main 13 | -------------------------------------------------------------------------------- /.github/workflows/copy-pr-template-to-dependabot-prs.yml: -------------------------------------------------------------------------------- 1 | name: Copy PR template to Dependabot PRs 2 | 3 | on: 4 | pull_request_target: 5 | types: [opened] 6 | 7 | permissions: 8 | contents: read 9 | pull-requests: write 10 | 11 | jobs: 12 | copy_pr_template: 13 | name: Copy PR template to Dependabot PR 14 | runs-on: ubuntu-latest 15 | if: github.actor == 'dependabot[bot]' 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - name: Post PR template as a comment 20 | uses: actions/github-script@v7 21 | with: 22 | script: | 23 | const fs = require('fs') 24 | 25 | const body = [ 26 | "pull_request_template.md", 27 | ".github/pull_request_template.md", 28 | "docs/pull_request_template.md", 29 | ]. 30 | filter(path => fs.existsSync(path)). 31 | map(path => fs.readFileSync(path)). 32 | join("\n") 33 | 34 | if (body !== "") { 35 | github.rest.issues.createComment({ 36 | issue_number: context.issue.number, 37 | owner: context.repo.owner, 38 | repo: context.repo.repo, 39 | body 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | workflow_dispatch: 5 | workflow_run: 6 | workflows: [CI] 7 | types: [completed] 8 | branches: [main] 9 | 10 | jobs: 11 | release: 12 | if: github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' 13 | name: Release 14 | uses: alphagov/govuk-infrastructure/.github/workflows/release.yml@main 15 | secrets: 16 | GH_TOKEN: ${{ secrets.GOVUK_CI_GITHUB_API_TOKEN }} 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | tmp 2 | /log/* 3 | !/log/.gitkeep 4 | coverage 5 | spec/reports/pacts 6 | -------------------------------------------------------------------------------- /.govuk_dependabot_merger.yml: -------------------------------------------------------------------------------- 1 | api_version: 2 2 | defaults: 3 | auto_merge: true 4 | update_external_dependencies: true 5 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --require spec_helper 2 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_gem: 2 | rubocop-govuk: 3 | - config/default.yml 4 | - config/rails.yml 5 | 6 | inherit_mode: 7 | merge: 8 | - Exclude 9 | 10 | # ************************************************************** 11 | # TRY NOT TO ADD OVERRIDES IN THIS FILE 12 | # 13 | # This repo is configured to follow the RuboCop GOV.UK styleguide. 14 | # Any rules you override here will cause this repo to diverge from 15 | # the way we write code in all other GOV.UK repos. 16 | # 17 | # See https://github.com/alphagov/rubocop-govuk/blob/main/CONTRIBUTING.md 18 | # ************************************************************** 19 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.3.1 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG ruby_version=3.3 2 | ARG base_image=ghcr.io/alphagov/govuk-ruby-base:$ruby_version 3 | ARG builder_image=ghcr.io/alphagov/govuk-ruby-builder:$ruby_version 4 | 5 | 6 | FROM --platform=$TARGETPLATFORM $builder_image AS builder 7 | 8 | WORKDIR $APP_HOME 9 | COPY Gemfile* .ruby-version ./ 10 | RUN bundle install 11 | COPY . . 12 | RUN bootsnap precompile --gemfile . 13 | 14 | 15 | FROM --platform=$TARGETPLATFORM $base_image 16 | 17 | ENV GOVUK_APP_NAME=email-alert-api 18 | 19 | WORKDIR $APP_HOME 20 | COPY --from=builder $BUNDLE_PATH $BUNDLE_PATH 21 | COPY --from=builder $BOOTSNAP_CACHE_DIR $BOOTSNAP_CACHE_DIR 22 | COPY --from=builder $APP_HOME . 23 | 24 | USER app 25 | CMD ["puma"] 26 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | ruby "~> 3.3.1" 4 | 5 | gem "rails", "8.0.2" 6 | 7 | gem "bootsnap", require: false 8 | gem "faraday" 9 | gem "gds-api-adapters" 10 | gem "gds-sso" 11 | gem "govuk_app_config" 12 | gem "govuk_document_types" 13 | gem "govuk_personalisation" 14 | gem "govuk_sidekiq" 15 | gem "json-schema" 16 | gem "jwt" 17 | gem "nokogiri" 18 | gem "notifications-ruby-client" 19 | gem "pg" 20 | gem "plek" 21 | gem "ratelimit" 22 | gem "redcarpet" 23 | gem "sentry-sidekiq" 24 | gem "sidekiq-scheduler" 25 | gem "sitemap-parser" 26 | gem "with_advisory_lock" 27 | 28 | group :test do 29 | gem "brakeman" 30 | gem "climate_control" 31 | gem "equivalent-xml" 32 | gem "factory_bot_rails" 33 | gem "webmock" 34 | end 35 | 36 | group :development, :test do 37 | gem "database_cleaner" 38 | gem "listen" 39 | gem "pact", require: false 40 | gem "pact_broker-client" 41 | gem "pry-byebug" 42 | gem "rspec-rails" 43 | gem "rubocop-govuk", require: false 44 | gem "simplecov" 45 | end 46 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Crown Copyright (Government Digital Service) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require File.expand_path("config/application", __dir__) 5 | 6 | Rails.application.load_tasks 7 | 8 | begin 9 | require "pact/tasks" 10 | rescue LoadError 11 | # Pact isn't available in all environments 12 | end 13 | 14 | unless Rails.env.production? 15 | Rake::Task[:default].clear 16 | task default: %i[lint spec pact:verify] 17 | end 18 | -------------------------------------------------------------------------------- /app/builders/bulk_migrate_confirmation_email_builder.rb: -------------------------------------------------------------------------------- 1 | class BulkMigrateConfirmationEmailBuilder 2 | include Callable 3 | 4 | def initialize(source_id:, destination_id:, count:) 5 | @source_id = source_id 6 | @destination_id = destination_id 7 | @count = count 8 | end 9 | 10 | def call 11 | Email.create!( 12 | subject:, 13 | body:, 14 | address: ENV["BULK_MIGRATE_CONFIRMATION_EMAIL_ACCOUNT"], 15 | ) 16 | end 17 | 18 | private 19 | 20 | attr_reader :source_id, :destination_id, :count 21 | 22 | def subject 23 | "Bulk migration of #{source_subscriber_list_title} is complete" 24 | end 25 | 26 | def source_subscriber_list_title 27 | SubscriberList.find(source_id).title 28 | end 29 | 30 | def destination_subscriber_list_title 31 | SubscriberList.find(destination_id).title 32 | end 33 | 34 | def body 35 | <<~BODY 36 | #{count} subscriptions have been migrated: 37 | From "#{source_subscriber_list_title}" 38 | To "#{destination_subscriber_list_title}" 39 | 40 | No email notification has been sent to users. 41 | 42 | Thanks 43 | GOV.UK emails 44 | BODY 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /app/builders/bulk_subscriber_list_email_builder_with_account.rb: -------------------------------------------------------------------------------- 1 | class BulkSubscriberListEmailBuilderWithAccount < BulkSubscriberListEmailBuilder 2 | def filter_subscriptions(subscriptions) 3 | subscriptions.select { |sub| Services.accounts_emails.include?(sub.subscriber.address) } 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /app/builders/digest_email_builder.rb: -------------------------------------------------------------------------------- 1 | class DigestEmailBuilder 2 | include Callable 3 | 4 | def initialize(content:, subscription:) 5 | @content = content 6 | @subscription = subscription 7 | @subscriber = subscription.subscriber 8 | @subscriber_list = subscription.subscriber_list 9 | end 10 | 11 | def call 12 | Email.create!( 13 | address: subscriber.address, 14 | subject: I18n.t!( 15 | "emails.digests.#{subscription.frequency}.subject", 16 | title: subscriber_list.title, 17 | ), 18 | body:, 19 | subscriber_id: subscriber.id, 20 | subscription_id: subscription.id, 21 | ) 22 | end 23 | 24 | private 25 | 26 | attr_reader :content, :subscription, :subscriber_list, :subscriber 27 | 28 | def body 29 | <<~BODY 30 | #{I18n.t("emails.digests.#{subscription.frequency}.opening_line")} 31 | 32 | # #{subscriber_list.title} 33 | 34 | --- 35 | 36 | #{presented_results} 37 | 38 | --- 39 | 40 | #{FooterPresenter.call(subscriber, subscription)} 41 | BODY 42 | end 43 | 44 | def presented_results 45 | changes = content.map do |item| 46 | presenter = "#{item.class.name}Presenter".constantize 47 | presenter.call(item, subscription) 48 | end 49 | 50 | changes.join("\n\n---\n\n").strip 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /app/builders/subscriber_auth_email_builder.rb: -------------------------------------------------------------------------------- 1 | class SubscriberAuthEmailBuilder 2 | include Callable 3 | 4 | def initialize(subscriber:, destination:, token:) 5 | @subscriber = subscriber 6 | @destination = destination 7 | @token = token 8 | end 9 | 10 | def call 11 | Email.create!( 12 | subject:, 13 | body:, 14 | address: subscriber.address, 15 | subscriber_id: subscriber.id, 16 | ) 17 | end 18 | 19 | private 20 | 21 | attr_reader :subscriber, :destination, :token 22 | 23 | def subject 24 | "Change your GOV.UK email preferences" 25 | end 26 | 27 | def body 28 | <<~BODY 29 | # Click the link to confirm your email address 30 | 31 | # [Yes, I want to change my GOV.UK email preferences](#{link}) 32 | 33 | This link will stop working after 7 days. 34 | 35 | If you did not request this email, you can ignore it. 36 | 37 | Thanks 38 | GOV.UK emails 39 | BODY 40 | end 41 | 42 | def link 43 | PublicUrls.url_for(base_path: destination, token:) 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /app/builders/subscription_auth_email_builder.rb: -------------------------------------------------------------------------------- 1 | class SubscriptionAuthEmailBuilder 2 | include Callable 3 | 4 | def initialize(address:, token:, subscriber_list:, frequency:) 5 | @address = address 6 | @token = token 7 | @subscriber_list = subscriber_list 8 | @frequency = frequency 9 | end 10 | 11 | def call 12 | Email.create!( 13 | subject:, 14 | body:, 15 | address:, 16 | ) 17 | end 18 | 19 | private 20 | 21 | attr_reader :address, :token, :frequency, :subscriber_list 22 | 23 | def subject 24 | "Confirm that you want to get emails from GOV.UK" 25 | end 26 | 27 | def body 28 | <<~BODY 29 | # Click the link to confirm that you want to get emails from GOV.UK 30 | 31 | # [Yes, I want emails about #{subscriber_list.title}](#{link}) 32 | 33 | This link will stop working after 7 days. 34 | 35 | #{I18n.t!("emails.subscription_auth.frequency.#{frequency}")}. You can change this at any time. 36 | 37 | If you did not request this email, you can ignore it. 38 | 39 | Thanks 40 | GOV.UK emails 41 | BODY 42 | end 43 | 44 | def link 45 | PublicUrls.url_for( 46 | base_path: "/email/subscriptions/authenticate", 47 | token:, 48 | topic_id: subscriber_list.slug, 49 | frequency:, 50 | ) 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::API 2 | include GDS::SSO::ControllerMethods 3 | 4 | before_action :authorise 5 | 6 | rescue_from ActiveRecord::RecordInvalid do |exception| 7 | render_unprocessable(exception.record&.errors&.messages) 8 | end 9 | 10 | private 11 | 12 | def render_unprocessable(messages) 13 | render json: { error: "Unprocessable Entity", 14 | details: messages }, 15 | status: :unprocessable_entity 16 | end 17 | 18 | def authorise 19 | authorise_user!("internal_app") 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /app/controllers/spam_reports_controller.rb: -------------------------------------------------------------------------------- 1 | class SpamReportsController < ApplicationController 2 | wrap_parameters false 3 | 4 | def create 5 | subscriber = Subscriber.find_by_address(params[:to]) 6 | UnsubscribeAllService.call(subscriber, :marked_as_spam) if subscriber 7 | head :no_content 8 | end 9 | 10 | private 11 | 12 | def authorise 13 | authorise_user!("status_updates") 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/controllers/unsubscribe_controller.rb: -------------------------------------------------------------------------------- 1 | class UnsubscribeController < ApplicationController 2 | def unsubscribe 3 | subscription = Subscription.active.find(id) 4 | subscription.end(reason: :unsubscribed) 5 | end 6 | 7 | def unsubscribe_all 8 | subscriber = Subscriber.find(id) 9 | UnsubscribeAllService.call(subscriber, :unsubscribed) 10 | end 11 | 12 | private 13 | 14 | def id 15 | params.fetch(:id) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | self.abstract_class = true 3 | end 4 | -------------------------------------------------------------------------------- /app/models/content_change.rb: -------------------------------------------------------------------------------- 1 | class ContentChange < ApplicationRecord 2 | include SymbolizeJSON 3 | 4 | validates :content_id, 5 | :title, 6 | :base_path, 7 | :change_note, 8 | :public_updated_at, 9 | :email_document_supertype, 10 | :government_document_supertype, 11 | :govuk_request_id, 12 | :document_type, 13 | :publishing_app, 14 | presence: true 15 | 16 | has_many :matched_content_changes 17 | has_many :subscription_contents 18 | 19 | enum :priority, { normal: 0, high: 1 } 20 | 21 | def queue 22 | priority == "high" ? :send_email_immediate_high : :send_email_immediate 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /app/models/digest_run.rb: -------------------------------------------------------------------------------- 1 | class DigestRun < ApplicationRecord 2 | DIGEST_RANGE_HOUR = 8 3 | 4 | validates :starts_at, :ends_at, :date, :range, presence: true 5 | before_validation :set_range_dates, on: :create 6 | validate :ends_at_is_in_the_past 7 | validate :weekly_digest_is_on_a_saturday 8 | 9 | has_many :digest_run_subscribers, dependent: :destroy 10 | has_many :subscribers, through: :digest_run_subscribers 11 | 12 | enum :range, { daily: 0, weekly: 1 } 13 | 14 | def mark_as_completed 15 | completed_time = digest_run_subscribers.maximum(:processed_at) || Time.zone.now 16 | update!(completed_at: completed_time) 17 | end 18 | 19 | private 20 | 21 | def set_range_dates 22 | self.starts_at = Time.zone.parse("#{DIGEST_RANGE_HOUR}:00", starts_at_time) 23 | self.ends_at = Time.zone.parse("#{DIGEST_RANGE_HOUR}:00", date) 24 | end 25 | 26 | def ends_at_is_in_the_past 27 | return if ends_at < Time.zone.now 28 | 29 | errors.add(:date, "must be in the past, or today if after #{DIGEST_RANGE_HOUR}:00") 30 | end 31 | 32 | def weekly_digest_is_on_a_saturday 33 | return if daily? || date.saturday? 34 | 35 | errors.add(:date, "must be a Saturday for weekly digests") 36 | end 37 | 38 | def starts_at_time 39 | (daily? ? date - 1.day : date - 1.week) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /app/models/digest_run_subscriber.rb: -------------------------------------------------------------------------------- 1 | class DigestRunSubscriber < ApplicationRecord 2 | # Any validations added this to this model won't be applied on record 3 | # creation as this table is populated by the #insert_all bulk method 4 | 5 | belongs_to :digest_run 6 | belongs_to :subscriber 7 | 8 | def self.populate(digest_run, subscriber_ids) 9 | now = Time.zone.now 10 | records = subscriber_ids.map do |subscriber_id| 11 | { 12 | digest_run_id: digest_run.id, 13 | subscriber_id:, 14 | created_at: now, 15 | updated_at: now, 16 | } 17 | end 18 | 19 | insert_all(records).pluck("id") 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /app/models/email.rb: -------------------------------------------------------------------------------- 1 | # Any validations added this to this model won't be applied on record 2 | # creation as this table is populated by the #insert_all bulk method 3 | class Email < ApplicationRecord 4 | enum :status, { pending: 0, sent: 1, failed: 2 } 5 | 6 | def self.timed_bulk_insert(records, batch_size) 7 | return insert_all!(records) unless records.size == batch_size 8 | 9 | Metrics.email_bulk_insert(batch_size) { insert_all!(records) } 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/models/frequency.rb: -------------------------------------------------------------------------------- 1 | class Frequency 2 | IMMEDIATELY = "immediately".freeze 3 | DAILY = "daily".freeze 4 | WEEKLY = "weekly".freeze 5 | end 6 | -------------------------------------------------------------------------------- /app/models/matched_content_change.rb: -------------------------------------------------------------------------------- 1 | class MatchedContentChange < ApplicationRecord 2 | # Any validations added this to this model won't be applied on record 3 | # creation as this table is populated by the #insert_all bulk method 4 | 5 | belongs_to :content_change 6 | belongs_to :subscriber_list 7 | end 8 | -------------------------------------------------------------------------------- /app/models/matched_message.rb: -------------------------------------------------------------------------------- 1 | class MatchedMessage < ApplicationRecord 2 | # Any validations added this to this model won't be applied on record 3 | # creation as this table is populated by the #insert_all bulk method 4 | 5 | belongs_to :message 6 | belongs_to :subscriber_list 7 | end 8 | -------------------------------------------------------------------------------- /app/models/message.rb: -------------------------------------------------------------------------------- 1 | class Message < ApplicationRecord 2 | include SymbolizeJSON 3 | 4 | has_many :matched_messages 5 | has_many :subscription_contents 6 | 7 | validates :title, :body, :criteria_rules, :govuk_request_id, presence: true 8 | validates :criteria_rules, criteria_schema: true, allow_blank: true 9 | validates :sender_message_id, uuid: true, uniqueness: true, allow_nil: true 10 | 11 | enum :priority, { normal: 0, high: 1 } 12 | 13 | def queue 14 | :send_email_immediate 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/models/subscription_content.rb: -------------------------------------------------------------------------------- 1 | class SubscriptionContent < ApplicationRecord 2 | # Any validations added this to this model won't be applied on record 3 | # creation as this table is populated by the #insert_all bulk method 4 | 5 | belongs_to :subscription 6 | belongs_to :digest_run_subscriber, optional: true 7 | belongs_to :email, optional: true 8 | 9 | # A subscription content should always have one of these and not both 10 | belongs_to :content_change, optional: true 11 | belongs_to :message, optional: true 12 | 13 | scope :immediate, -> { where(digest_run_subscriber_id: nil) } 14 | scope :digest, -> { where.not(digest_run_subscriber_id: nil) } 15 | 16 | def self.populate_for_content(content, records) 17 | base = case content 18 | when ContentChange 19 | { content_change_id: content.id } 20 | when Message 21 | { message_id: content.id } 22 | else 23 | raise ArgumentError, "Expected #{content.class.name} to be a "\ 24 | "ContentChange or a Message" 25 | end 26 | 27 | now = Time.zone.now 28 | 29 | attributes = records.map do |record| 30 | base.merge(created_at: now, updated_at: now).merge(record) 31 | end 32 | 33 | SubscriptionContent.insert_all!(attributes) 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /app/models/user.rb: -------------------------------------------------------------------------------- 1 | class User < ApplicationRecord 2 | include GDS::SSO::User 3 | end 4 | -------------------------------------------------------------------------------- /app/presenters/bulk_email_body_presenter.rb: -------------------------------------------------------------------------------- 1 | class BulkEmailBodyPresenter 2 | include Callable 3 | 4 | def initialize(body, subscriber_list) 5 | @body = body 6 | @subscriber_list = subscriber_list 7 | end 8 | 9 | def call 10 | body.gsub("%LISTURL%", list_url) 11 | end 12 | 13 | private 14 | 15 | attr_reader :body, :subscriber_list 16 | 17 | def list_url 18 | return "" unless subscriber_list.url 19 | 20 | PublicUrls.url_for( 21 | base_path: subscriber_list.url, 22 | utm_source: subscriber_list.slug, 23 | utm_campaign: "govuk-notifications-bulk", 24 | ) 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /app/presenters/message_presenter.rb: -------------------------------------------------------------------------------- 1 | class MessagePresenter 2 | include Callable 3 | 4 | def initialize(message, _subscription = nil) 5 | @message = message 6 | end 7 | 8 | def call 9 | message.body 10 | end 11 | 12 | private 13 | 14 | attr_reader :message 15 | end 16 | -------------------------------------------------------------------------------- /app/queries/digest_run_subscriber_query.rb: -------------------------------------------------------------------------------- 1 | class DigestRunSubscriberQuery 2 | def self.call(digest_run:) 3 | content_changes_exist = 4 | SubscriberList 5 | .joins(matched_content_changes: :content_change) 6 | .where("subscriptions.subscriber_list_id = subscriber_lists.id") 7 | .where("content_changes.created_at >= ?", digest_run.starts_at) 8 | .where("content_changes.created_at < ?", digest_run.ends_at) 9 | .arel 10 | .exists 11 | 12 | messages_exist = 13 | SubscriberList 14 | .joins(matched_messages: :message) 15 | .where("subscriptions.subscriber_list_id = subscriber_lists.id") 16 | .where("messages.created_at >= ?", digest_run.starts_at) 17 | .where("messages.created_at < ?", digest_run.ends_at) 18 | .arel 19 | .exists 20 | 21 | scope = Subscriber.joins(:active_subscriptions) 22 | .where("subscriptions.frequency": digest_run.range) 23 | 24 | scope.where(content_changes_exist) 25 | .or(scope.where(messages_exist)) 26 | .distinct 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /app/queries/email_criteria_query.rb: -------------------------------------------------------------------------------- 1 | class EmailCriteriaQuery 2 | attr_reader :govuk_path, :draft 3 | 4 | def initialize(govuk_path:, draft: false) 5 | @govuk_path = govuk_path 6 | @draft = draft 7 | end 8 | 9 | def call 10 | content_item = content_store_client.content_item(govuk_path).to_hash 11 | 12 | EmailAlertCriteria.new(content_item:).would_trigger_alert? 13 | end 14 | 15 | private 16 | 17 | def content_store_client 18 | return GdsApi.content_store unless @draft 19 | 20 | # We don't appear to need a token for the #content_item method? 21 | GdsApi::ContentStore.new(Plek.find("draft-content-store"), bearer_token: "") 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /app/queries/find_latest_matching_subscription.rb: -------------------------------------------------------------------------------- 1 | class FindLatestMatchingSubscription 2 | def self.call(original_subscription) 3 | subscriber_list_id = original_subscription.subscriber_list_id 4 | subscriber_id = original_subscription.subscriber_id 5 | Subscription 6 | .where(subscriber_list_id:, subscriber_id:) 7 | .order("created_at DESC") 8 | .first 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/queries/find_without_links_and_tags_and_content_id.rb: -------------------------------------------------------------------------------- 1 | class FindWithoutLinksAndTagsAndContentId 2 | def initialize(scope: SubscriberList) 3 | @scope = scope 4 | end 5 | 6 | def call 7 | @scope.where("tags::text = '{}'::text AND links::text = '{}'::text AND content_id::text IS null") 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/queries/subscriber_lists_by_path_query.rb: -------------------------------------------------------------------------------- 1 | class SubscriberListsByPathQuery 2 | attr_reader :govuk_path, :draft 3 | 4 | def initialize(govuk_path:, draft: false) 5 | @govuk_path = govuk_path 6 | @draft = draft 7 | end 8 | 9 | def call 10 | content_item = content_store_client.content_item(govuk_path).to_hash 11 | 12 | SubscriberListsByContentItemQuery.new(content_item).call 13 | end 14 | 15 | private 16 | 17 | def content_store_client 18 | return GdsApi.content_store unless @draft 19 | 20 | # We don't appear to need a token for the #content_item method? 21 | GdsApi::ContentStore.new(Plek.find("draft-content-store"), bearer_token: "") 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /app/queries/subscriber_lists_for_finder_query.rb: -------------------------------------------------------------------------------- 1 | class SubscriberListsForFinderQuery 2 | attr_reader :govuk_path 3 | 4 | class NotAFinderError < StandardError; end 5 | 6 | def initialize(govuk_path:) 7 | @govuk_path = govuk_path 8 | end 9 | 10 | SELECT_FOR_TAGS_TEMPLATE = <<-SQL.freeze 11 | SELECT EXISTS ( 12 | SELECT formats FROM ( 13 | SELECT json_array_elements_text(tags -> ? -> 'any') AS formats 14 | ) AS allowed_formats WHERE formats = ? 15 | ) 16 | SQL 17 | 18 | def call 19 | content_item = GdsApi.content_store.content_item(govuk_path).to_hash 20 | raise NotAFinderError unless content_item["document_type"] == "finder" 21 | 22 | lists = [] 23 | content_item["details"]["filter"].each_key do |filter_name| 24 | Array(content_item["details"]["filter"][filter_name]).each do |filter_value| 25 | lists += SubscriberList.where(SELECT_FOR_TAGS_TEMPLATE, filter_name, filter_value) 26 | end 27 | end 28 | 29 | lists.uniq 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /app/services/auth_token_generator_service.rb: -------------------------------------------------------------------------------- 1 | class AuthTokenGeneratorService 2 | include Callable 3 | 4 | CIPHER = "aes-256-gcm".freeze 5 | OPTIONS = { cipher: CIPHER, serializer: JSON }.freeze 6 | 7 | attr_reader :data, :expiry 8 | 9 | def initialize(data, expiry: 1.week) 10 | @data = data 11 | @expiry = expiry 12 | end 13 | 14 | def self.call(*args) 15 | new(*args).call 16 | end 17 | 18 | def call 19 | self.class.crypt.encrypt_and_sign(data, expires_in: expiry) 20 | end 21 | 22 | def self.crypt 23 | @crypt ||= begin 24 | secret = Rails.application.credentials.email_alert_auth_token 25 | len = ActiveSupport::MessageEncryptor.key_len(CIPHER) 26 | key = ActiveSupport::KeyGenerator.new(secret).generate_key("", len) 27 | ActiveSupport::MessageEncryptor.new(key, **OPTIONS) 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /app/services/bulk_unsubscribe_list_service.rb: -------------------------------------------------------------------------------- 1 | class BulkUnsubscribeListService 2 | include Callable 3 | 4 | attr_reader :subscriber_list, :params, :govuk_request_id, :user 5 | 6 | def initialize(subscriber_list:, params:, govuk_request_id:, user: nil) 7 | @subscriber_list = subscriber_list 8 | @params = params 9 | @govuk_request_id = govuk_request_id 10 | @user = user 11 | end 12 | 13 | def call 14 | message = Message.create!(message_params) if message_params 15 | Metrics.message_created if message 16 | BulkUnsubscribeListWorker.perform_async( 17 | subscriber_list.id, 18 | message&.id, 19 | ) 20 | end 21 | 22 | def message_params 23 | return unless params[:body] 24 | 25 | params 26 | .slice(:body, :sender_message_id) 27 | .merge( 28 | title: subscriber_list.title, 29 | criteria_rules: [{ id: subscriber_list.id }], 30 | govuk_request_id:, 31 | signon_user_uid: user&.uid, 32 | omit_footer_unsubscribe_link: true, 33 | override_subscription_frequency_to_immediate: true, 34 | ) 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /app/services/check_notify_email_service.rb: -------------------------------------------------------------------------------- 1 | class CheckNotifyEmailService 2 | def initialize(status) 3 | @status = status 4 | @client = Notifications::Client.new(Rails.application.credentials.notify_api_key) 5 | end 6 | 7 | def present?(reference) 8 | results = @client.get_notifications( 9 | template_type: "email", 10 | status: @status, 11 | reference:, 12 | ) 13 | 14 | results.collection.count.positive? 15 | rescue Notifications::Client::RequestError, Net::OpenTimeout, Net::ReadTimeout, SocketError => e 16 | Rails.logger.debug("Unable to contact Notify to determine status of email reference #{reference}: #{e}") 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /app/services/digest_initiator_service.rb: -------------------------------------------------------------------------------- 1 | class DigestInitiatorService 2 | include Callable 3 | 4 | def initialize(date:, range:) 5 | @range = range 6 | @date = date 7 | end 8 | 9 | def call 10 | digest_run = DigestRun.find_or_create_by!(date:, range:) 11 | return if digest_run.processed_at 12 | 13 | create_digest_run_subscribers(digest_run) 14 | digest_run.update!(processed_at: Time.zone.now) 15 | end 16 | 17 | private 18 | 19 | attr_reader :range, :date 20 | 21 | def create_digest_run_subscribers(digest_run) 22 | Metrics.digest_initiator_service(range) do 23 | subscriber_ids = DigestRunSubscriberQuery.call(digest_run:).pluck(:id) 24 | 25 | subscriber_ids.each_slice(1000) do |subscriber_ids_chunk| 26 | digest_run_subscriber_ids = DigestRunSubscriber.populate(digest_run, subscriber_ids_chunk) 27 | 28 | enqueue_jobs(digest_run_subscriber_ids) 29 | end 30 | end 31 | end 32 | 33 | def enqueue_jobs(digest_run_subscriber_ids) 34 | digest_run_subscriber_ids.each do |digest_run_subscriber_id| 35 | DigestEmailGenerationWorker.perform_async(digest_run_subscriber_id) 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /app/services/matched_content_change_generation_service.rb: -------------------------------------------------------------------------------- 1 | class MatchedContentChangeGenerationService 2 | include Callable 3 | 4 | def initialize(content_change) 5 | @content_change = content_change 6 | end 7 | 8 | def call 9 | # if we have records already, then we expect the process completed 10 | # successfully previously since the insert is an atomic operation 11 | return if MatchedContentChange.exists?(content_change:) || subscriber_lists.empty? 12 | 13 | now = Time.zone.now 14 | records = subscriber_lists.map do |list| 15 | { 16 | content_change_id: content_change.id, 17 | subscriber_list_id: list.id, 18 | created_at: now, 19 | updated_at: now, 20 | } 21 | end 22 | 23 | MatchedContentChange.insert_all!(records) 24 | end 25 | 26 | private 27 | 28 | attr_reader :content_change 29 | 30 | def subscriber_lists 31 | @subscriber_lists ||= SubscriberListQuery.new( 32 | content_id: content_change.content_id, 33 | tags: content_change.tags, 34 | links: content_change.links, 35 | document_type: content_change.document_type, 36 | email_document_supertype: content_change.email_document_supertype, 37 | government_document_supertype: content_change.government_document_supertype, 38 | ).lists 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /app/services/matched_message_generation_service.rb: -------------------------------------------------------------------------------- 1 | class MatchedMessageGenerationService 2 | include Callable 3 | 4 | def initialize(message) 5 | @message = message 6 | end 7 | 8 | def call 9 | # if we already have records already, then we expect the process completed 10 | # successfully previously since the insert is an atomic operation 11 | return if MatchedMessage.exists?(message:) || subscriber_lists.empty? 12 | 13 | now = Time.zone.now 14 | records = subscriber_lists.map do |list| 15 | { 16 | message_id: message.id, 17 | subscriber_list_id: list.id, 18 | created_at: now, 19 | updated_at: now, 20 | } 21 | end 22 | 23 | MatchedMessage.insert_all!(records) 24 | end 25 | 26 | private 27 | 28 | attr_reader :message 29 | 30 | def subscriber_lists 31 | @subscriber_lists ||= SubscriberList.matching_criteria_rules(message.criteria_rules) 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /app/services/send_email_service.rb: -------------------------------------------------------------------------------- 1 | class SendEmailService 2 | include Callable 3 | 4 | class NotifyCommunicationFailure < RuntimeError; end 5 | 6 | def initialize(email:, metrics: {}) 7 | @email = email 8 | @metrics = metrics 9 | end 10 | 11 | def call 12 | if send_to_notify? 13 | Metrics.email_send_request("notify") { SendNotifyEmail.call(email) } 14 | else 15 | Metrics.email_send_request("pseudo") { SendPseudoEmail.call(email) } 16 | end 17 | 18 | record_sent_metrics 19 | end 20 | 21 | private 22 | 23 | attr_reader :email, :metrics 24 | 25 | def send_to_notify? 26 | return true if ENV["GOVUK_NOTIFY_RECIPIENTS"] == "*" 27 | 28 | notify_recipients = ENV.fetch("GOVUK_NOTIFY_RECIPIENTS", "").split(",").map(&:strip) 29 | notify_recipients.include?(email.address) 30 | end 31 | 32 | def record_sent_metrics 33 | return unless email.sent_at 34 | return unless metrics[:content_change_created_at] 35 | 36 | Metrics.content_change_created_until_email_sent( 37 | metrics[:content_change_created_at], 38 | email.sent_at, 39 | ) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /app/services/send_email_service/send_pseudo_email.rb: -------------------------------------------------------------------------------- 1 | class SendEmailService::SendPseudoEmail 2 | def initialize(email) 3 | @email = email 4 | end 5 | 6 | def self.call(*args) 7 | new(*args).call 8 | end 9 | 10 | def call 11 | Rails.logger.info <<~INFO 12 | Logging email (#{email.id}) we'd have attempted to send to #{email.address} 13 | Subject: #{email.subject} 14 | INFO 15 | 16 | Metrics.sent_to_pseudo_successfully 17 | email.update!(status: :sent, sent_at: Time.zone.now) 18 | end 19 | 20 | private 21 | 22 | attr_reader :email 23 | end 24 | -------------------------------------------------------------------------------- /app/services/unsubscribe_all_service.rb: -------------------------------------------------------------------------------- 1 | class UnsubscribeAllService 2 | include Callable 3 | 4 | attr_reader :subscriber, :reason 5 | 6 | def initialize(subscriber, reason) 7 | @subscriber = subscriber 8 | @reason = reason 9 | end 10 | 11 | def call 12 | ended_time = Time.zone.now 13 | ended_count = subscriber.active_subscriptions 14 | .update_all(ended_at: ended_time, 15 | updated_at: ended_time, 16 | ended_reason: reason) 17 | 18 | Metrics.unsubscribed(reason, ended_count) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /app/services/update_last_alerted_at_subscriber_list_service.rb: -------------------------------------------------------------------------------- 1 | class UpdateLastAlertedAtSubscriberListService 2 | include Callable 3 | 4 | def initialize(content_change) 5 | @content_change = content_change 6 | end 7 | 8 | def call 9 | subscriber_list_ids = MatchedContentChange.where(content_change_id: content_change.id).pluck(:subscriber_list_id) 10 | SubscriberList.where(id: subscriber_list_ids).update_all(last_alerted_at: Time.zone.now) 11 | end 12 | 13 | private 14 | 15 | attr_reader :content_change 16 | end 17 | -------------------------------------------------------------------------------- /app/validators/email_address_validator.rb: -------------------------------------------------------------------------------- 1 | class EmailAddressValidator < ActiveModel::EachValidator 2 | def validate_each(record, attribute, value) 3 | if !value.nil? && !valid_email_address?(value) 4 | record.errors.add(attribute, "is not an email address") 5 | end 6 | end 7 | 8 | private 9 | 10 | def valid_email_address?(email_address) 11 | contains_one_at_sign?(email_address) && 12 | contains_a_valid_domain?(email_address) && 13 | is_a_single_email_address?(email_address) && 14 | does_not_contain_whitespace?(email_address) 15 | end 16 | 17 | def contains_one_at_sign?(email_address) 18 | email_address.scan("@").length == 1 19 | end 20 | 21 | def contains_a_valid_domain?(email_address) 22 | domain = email_address.split("@")[1] 23 | return false if domain.nil? 24 | 25 | domain_contains_at_least_one_dot?(domain) || 26 | domain_is_an_ip_address?(domain) 27 | end 28 | 29 | def is_a_single_email_address?(email_address) 30 | email_address.scan(",").empty? 31 | end 32 | 33 | def does_not_contain_whitespace?(email_address) 34 | email_address !~ /\s/ 35 | end 36 | 37 | def domain_contains_at_least_one_dot?(domain) 38 | !domain.start_with?(".") && !domain.scan(".").empty? 39 | end 40 | 41 | def domain_is_an_ip_address?(domain) 42 | domain.start_with?("[") && domain.end_with?("]") 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /app/validators/links_validator.rb: -------------------------------------------------------------------------------- 1 | class LinksValidator < ActiveModel::EachValidator 2 | def validate_each(record, attribute, links) 3 | invalidly_formatted = invalid_formatted_links(links) 4 | if invalidly_formatted.any? 5 | record.errors.add(attribute, "#{invalidly_formatted.to_sentence} has a value with an invalid format.") 6 | end 7 | end 8 | 9 | private 10 | 11 | def invalid_formatted_links(links) 12 | invalid = links.select do |_key, link_values| 13 | link_values.flat_map(&:last) 14 | .any? { |link| !link.to_s.match?(/\A[a-zA-Z0-9\-_]*\z/) } 15 | end 16 | 17 | invalid.keys 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /app/validators/root_relative_url_validator.rb: -------------------------------------------------------------------------------- 1 | class RootRelativeUrlValidator < ActiveModel::EachValidator 2 | def validate_each(record, attribute, value) 3 | unless valid_url?(value) 4 | record.errors.add(attribute, "must be a root-relative URL") 5 | end 6 | end 7 | 8 | private 9 | 10 | def valid_url?(url) 11 | parsed = URI.parse(url) 12 | parsed.relative? && !parsed.host && url[0] == "/" 13 | rescue URI::InvalidURIError 14 | false 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/validators/tags_validator.rb: -------------------------------------------------------------------------------- 1 | class TagsValidator < ActiveModel::EachValidator 2 | def validate_each(record, attribute, tags) 3 | unless tag_values_are_arrays(tags) 4 | record.errors.add(attribute, "All tag values must be sent as Arrays") 5 | end 6 | 7 | if invalid_tags(tags).any? 8 | record.errors.add(attribute, "#{invalid_tags(tags).to_sentence} are not valid tags.") 9 | end 10 | 11 | invalidly_formatted = invalid_formatted_tags(tags) 12 | if invalidly_formatted.any? 13 | record.errors.add(attribute, "#{invalidly_formatted.to_sentence} has a value with an invalid format.") 14 | end 15 | end 16 | 17 | private 18 | 19 | def invalid_tags(tags) 20 | tags.keys - ValidTags::ALLOWED_TAGS 21 | end 22 | 23 | def tag_values_are_arrays(tags) 24 | tags.values.all? do |hash| 25 | hash.all? do |operator, values| 26 | %i[all any].include?(operator) && values.is_a?(Array) 27 | end 28 | end 29 | end 30 | 31 | def invalid_formatted_tags(tags) 32 | invalid = tags.select do |_key, tag_values| 33 | tag_values.flat_map(&:last) 34 | .any? { |tag| !tag.to_s.match?(/\A[a-zA-Z0-9\-_\/]*\z/) } 35 | end 36 | 37 | invalid.keys 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /app/validators/uuid_validator.rb: -------------------------------------------------------------------------------- 1 | class UuidValidator < ActiveModel::EachValidator 2 | UUID_REGEX = /\A[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\Z/i 3 | 4 | def validate_each(record, attribute, value) 5 | unless UUID_REGEX =~ value 6 | record.errors.add(attribute, "is not a valid UUID") 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/workers/application_worker.rb: -------------------------------------------------------------------------------- 1 | class ApplicationWorker 2 | include Sidekiq::Worker 3 | 4 | private 5 | 6 | def run_with_advisory_lock(model, unique_ref, &block) 7 | ApplicationRecord.with_advisory_lock("#{model}-#{unique_ref}", timeout_seconds: 0, &block) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/workers/bulk_unsubscribe_list_worker.rb: -------------------------------------------------------------------------------- 1 | class BulkUnsubscribeListWorker < ApplicationWorker 2 | sidekiq_options queue: :process_and_generate_emails 3 | 4 | def perform(subscriber_list_id, message_id) 5 | run_with_advisory_lock(SubscriberList, subscriber_list_id) do 6 | ProcessMessageWorker.new.perform(message_id) if message_id 7 | 8 | Subscription.active.where(subscriber_list_id:).update_all( 9 | ended_reason: :bulk_unsubscribed, 10 | ended_at: Time.zone.now, 11 | ) 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/workers/daily_digest_initiator_worker.rb: -------------------------------------------------------------------------------- 1 | class DailyDigestInitiatorWorker < ApplicationWorker 2 | def perform(date = Date.current.to_s) 3 | run_with_advisory_lock(DigestRun, "#{date}-#{Frequency::DAILY}") do 4 | DigestInitiatorService.call(date: Date.parse(date), range: Frequency::DAILY) 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/workers/digest_run_completion_marker_worker.rb: -------------------------------------------------------------------------------- 1 | class DigestRunCompletionMarkerWorker < ApplicationWorker 2 | def perform 3 | candidates = DigestRun.where.not(processed_at: nil).where(completed_at: nil) 4 | candidates.find_each do |digest_run| 5 | unless DigestRunSubscriber.where(processed_at: nil, digest_run:).exists? 6 | digest_run.mark_as_completed 7 | end 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/workers/email_deletion_worker.rb: -------------------------------------------------------------------------------- 1 | class EmailDeletionWorker < ApplicationWorker 2 | def perform 3 | run_with_advisory_lock(Email, "delete") do 4 | start_time = Time.zone.now 5 | deleted_count = Email.where("created_at < ?", 1.week.ago).delete_all 6 | log_complete(deleted_count, start_time, Time.zone.now) 7 | end 8 | end 9 | 10 | private 11 | 12 | def log_complete(deleted, start_time, end_time) 13 | seconds = (end_time - start_time).round(2) 14 | message = "Deleted #{deleted} emails in #{seconds} seconds" 15 | logger.info(message) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/workers/metrics_collection_worker.rb: -------------------------------------------------------------------------------- 1 | class MetricsCollectionWorker < ApplicationWorker 2 | def perform 3 | ContentChangeExporter.call 4 | DigestRunExporter.call 5 | MessageExporter.call 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/workers/metrics_collection_worker/base_exporter.rb: -------------------------------------------------------------------------------- 1 | class MetricsCollectionWorker::BaseExporter 2 | def self.call 3 | new.call 4 | end 5 | 6 | private_class_method :new 7 | end 8 | -------------------------------------------------------------------------------- /app/workers/metrics_collection_worker/content_change_exporter.rb: -------------------------------------------------------------------------------- 1 | class MetricsCollectionWorker::ContentChangeExporter < MetricsCollectionWorker::BaseExporter 2 | def call 3 | GovukStatsd.gauge("content_changes.unprocessed_total", unprocessed_content_changes) 4 | end 5 | 6 | private 7 | 8 | def unprocessed_content_changes 9 | ContentChange 10 | .where("created_at < ?", unprocessed_latency.ago) 11 | .where(processed_at: nil) 12 | .count 13 | end 14 | 15 | def unprocessed_latency 16 | 120.minutes 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /app/workers/metrics_collection_worker/digest_run_exporter.rb: -------------------------------------------------------------------------------- 1 | class MetricsCollectionWorker::DigestRunExporter < MetricsCollectionWorker::BaseExporter 2 | def call 3 | critical_digest_runs = DigestRun.where("created_at < ?", 2.hours.ago) 4 | .where(completed_at: nil) 5 | .count 6 | 7 | GovukStatsd.gauge("digest_runs.critical_total", critical_digest_runs) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /app/workers/metrics_collection_worker/message_exporter.rb: -------------------------------------------------------------------------------- 1 | class MetricsCollectionWorker::MessageExporter < MetricsCollectionWorker::BaseExporter 2 | def call 3 | GovukStatsd.gauge("messages.unprocessed_total", unprocessed_messages) 4 | end 5 | 6 | private 7 | 8 | def unprocessed_messages 9 | Message 10 | .where("created_at < ?", unprocessed_latency.ago) 11 | .where(processed_at: nil) 12 | .count 13 | end 14 | 15 | def unprocessed_latency 16 | 120.minutes 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /app/workers/nullify_subscribers_worker.rb: -------------------------------------------------------------------------------- 1 | class NullifySubscribersWorker < ApplicationWorker 2 | def perform 3 | run_with_advisory_lock(Subscriber, "nullify") do 4 | nullifyable_subscribers.each do |s| 5 | begin 6 | GdsApi.account_api.delete_user_by_subject_identifier(subject_identifier: s.govuk_account_id) unless s.govuk_account_id.nil? 7 | rescue GdsApi::HTTPNotFound 8 | Rails.logger.warn("NullifySubscribersWorker tried to remove account id #{s.govuk_account_id}, but couldn't find it.") 9 | end 10 | s.update!(address: nil, govuk_account_id: nil, updated_at: Time.zone.now) 11 | end 12 | end 13 | end 14 | 15 | private 16 | 17 | def nullifyable_subscribers 18 | recently_active_subscriptions = Subscription 19 | .where("subscriptions.subscriber_id = subscribers.id") 20 | .where("ended_at IS NULL OR ended_at > ?", nullifyable_period) 21 | .arel.exists 22 | 23 | Subscriber 24 | .not_nullified 25 | .where("created_at < ?", nullifyable_period) 26 | .where.not(recently_active_subscriptions) 27 | end 28 | 29 | def nullifyable_period 30 | @nullifyable_period ||= 28.days.ago 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /app/workers/process_content_change_worker.rb: -------------------------------------------------------------------------------- 1 | class ProcessContentChangeWorker < ApplicationWorker 2 | sidekiq_options queue: :process_and_generate_emails 3 | 4 | def perform(content_change_id) 5 | run_with_advisory_lock(ContentChange, content_change_id) do 6 | content_change = ContentChange.find(content_change_id) 7 | return if content_change.processed_at 8 | 9 | MatchedContentChangeGenerationService.call(content_change) 10 | UpdateLastAlertedAtSubscriberListService.call(content_change) 11 | ImmediateEmailGenerationService.call(content_change) 12 | 13 | content_change.update!(processed_at: Time.zone.now) 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /app/workers/process_message_worker.rb: -------------------------------------------------------------------------------- 1 | class ProcessMessageWorker < ApplicationWorker 2 | sidekiq_options queue: :process_and_generate_emails 3 | 4 | def perform(message_id) 5 | run_with_advisory_lock(Message, message_id) do 6 | message = Message.find(message_id) 7 | return if message.processed_at 8 | 9 | MatchedMessageGenerationService.call(message) 10 | ImmediateEmailGenerationService.call(message) 11 | 12 | message.update!(processed_at: Time.zone.now) 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /app/workers/recover_lost_jobs_worker.rb: -------------------------------------------------------------------------------- 1 | class RecoverLostJobsWorker < ApplicationWorker 2 | def perform 3 | RecoverLostJobsWorker::UnprocessedCheck.new.call 4 | RecoverLostJobsWorker::MissingDigestRunsCheck.new.call 5 | RecoverLostJobsWorker::OldPendingEmailsCheck.new.call 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /app/workers/recover_lost_jobs_worker/missing_digest_runs_check.rb: -------------------------------------------------------------------------------- 1 | class RecoverLostJobsWorker::MissingDigestRunsCheck 2 | def call 3 | recover(DailyDigestInitiatorWorker, non_existent_daily_digests) 4 | recover(WeeklyDigestInitiatorWorker, non_existent_weekly_digests) 5 | end 6 | 7 | private 8 | 9 | def non_existent_daily_digests 10 | expected_digest_week 11 | .map { |date| DigestRun.find_or_initialize_by(date:, range: :daily) } 12 | .reject(&:persisted?) 13 | end 14 | 15 | def non_existent_weekly_digests 16 | [expected_digest_week.find(&:saturday?)] 17 | .map { |date| DigestRun.find_or_initialize_by(date:, range: :weekly) } 18 | .reject(&:persisted?) 19 | end 20 | 21 | def expected_digest_week 22 | digestion_time = Time.zone.parse("#{DigestRun::DIGEST_RANGE_HOUR}:00") 23 | cutoff_with_delay = digestion_time + 1.hour 24 | end_date = Time.zone.now > cutoff_with_delay ? Time.zone.today : Time.zone.yesterday 25 | (end_date - 6.days)..end_date 26 | end 27 | 28 | def recover(worker, digests) 29 | digests.each { |digest| worker.perform_async(digest.date.to_s) } 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /app/workers/recover_lost_jobs_worker/old_pending_emails_check.rb: -------------------------------------------------------------------------------- 1 | class RecoverLostJobsWorker::OldPendingEmailsCheck 2 | def call 3 | old_pending_emails = Email.where(status: :pending) 4 | .where("created_at <= ?", 3.hours.ago) 5 | 6 | recover(old_pending_emails) 7 | end 8 | 9 | private 10 | 11 | def recover(old_pending_emails) 12 | old_pending_emails.in_batches do |relation| 13 | relation.pluck(:id).each do |id| 14 | SendEmailWorker.perform_async_in_queue(id, queue: :send_email_immediate) 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /app/workers/recover_lost_jobs_worker/unprocessed_check.rb: -------------------------------------------------------------------------------- 1 | class RecoverLostJobsWorker::UnprocessedCheck 2 | def call 3 | recover(ProcessContentChangeWorker, old_unprocessed(ContentChange).pluck(:id)) 4 | recover(ProcessMessageWorker, old_unprocessed(Message).pluck(:id)) 5 | recover(DigestEmailGenerationWorker, old_unprocessed(DigestRunSubscriber).pluck(:id)) 6 | recover(DailyDigestInitiatorWorker, old_unprocessed(DigestRun.daily).pluck(:date).map(&:to_s)) 7 | recover(WeeklyDigestInitiatorWorker, old_unprocessed(DigestRun.weekly).pluck(:date).map(&:to_s)) 8 | end 9 | 10 | private 11 | 12 | def old_unprocessed(scope) 13 | scope.where(processed_at: nil).where("created_at <= ?", 1.hour.ago) 14 | end 15 | 16 | def recover(worker, work) 17 | work.each { |arg| worker.perform_async(arg) } 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /app/workers/subscriber_list_audit_worker.rb: -------------------------------------------------------------------------------- 1 | class SubscriberListAuditWorker < ApplicationWorker 2 | sidekiq_options queue: :subscriber_list_audit 3 | 4 | def perform(url_batch, audit_start_time_string) 5 | audit_start_time = Time.zone.parse(audit_start_time_string) 6 | content_store_client = GdsApi.content_store 7 | return unless SubscriberList.unaudited_since(audit_start_time).any? 8 | 9 | url_batch.each do |url| 10 | parsed_url = URI.parse(url) 11 | govuk_path = parsed_url.path 12 | begin 13 | content_item = content_store_client.content_item(govuk_path).to_hash 14 | 15 | if EmailAlertCriteria.new(content_item:).would_trigger_alert? 16 | lists = SubscriberListsByContentItemQuery.new(content_item).call 17 | lists.each { |list| list.update_column(:last_audited_at, audit_start_time) } 18 | end 19 | rescue StandardError 20 | # We don't really need to do much here, just not crash the worker 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /app/workers/weekly_digest_initiator_worker.rb: -------------------------------------------------------------------------------- 1 | class WeeklyDigestInitiatorWorker < ApplicationWorker 2 | def perform(date = Date.current.to_s) 3 | run_with_advisory_lock(DigestRun, "#{date}-#{Frequency::WEEKLY}") do 4 | DigestInitiatorService.call(date: Date.parse(date), range: Frequency::WEEKLY) 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /bin/brakeman: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "rubygems" 3 | require "bundler/setup" 4 | 5 | ARGV.unshift("--ensure-latest") 6 | 7 | load Gem.bin_path("brakeman", "brakeman") 8 | -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) 3 | load Gem.bin_path("bundler", "bundle") 4 | -------------------------------------------------------------------------------- /bin/dev: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | exec "./bin/rails", "server", *ARGV 3 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path("../config/application", __dir__) 3 | require_relative "../config/boot" 4 | require "rails/commands" 5 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative "../config/boot" 3 | require "rake" 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /bin/rubocop: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "rubygems" 3 | require "bundler/setup" 4 | 5 | # explicit rubocop config increases performance slightly while avoiding config confusion. 6 | ARGV.unshift("--config", File.expand_path("../.rubocop.yml", __dir__)) 7 | 8 | load Gem.bin_path("rubocop", "rubocop") 9 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "fileutils" 3 | 4 | # path to your application root. 5 | APP_ROOT = File.expand_path("..", __dir__) 6 | 7 | def system!(*args) 8 | system(*args, exception: true) 9 | end 10 | 11 | FileUtils.chdir APP_ROOT do 12 | # This script is a way to set up or update your development environment automatically. 13 | # This script is idempotent, so that you can run it at any time and get an expectable outcome. 14 | # Add necessary setup steps to this file. 15 | 16 | puts "== Installing dependencies ==" 17 | system! "gem install bundler --conservative" 18 | system("bundle check") || system!("bundle install") 19 | 20 | # puts "\n== Copying sample files ==" 21 | # unless File.exist?("config/database.yml") 22 | # FileUtils.cp "config/database.yml.sample", "config/database.yml" 23 | # end 24 | 25 | puts "\n== Preparing database ==" 26 | system! "bin/rails db:prepare" 27 | 28 | puts "\n== Removing old logs and tempfiles ==" 29 | system! "bin/rails log:clear tmp:clear" 30 | 31 | puts "\n== Restarting application server ==" 32 | system! "bin/rails restart" 33 | end 34 | -------------------------------------------------------------------------------- /bin/update: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require "pathname" 3 | require "fileutils" 4 | 5 | # path to your application root. 6 | APP_ROOT = Pathname.new File.expand_path("..", __dir__) 7 | 8 | def system!(*args) 9 | system(*args) || abort("\n== Command #{args} failed ==") 10 | end 11 | 12 | Dir.chdir APP_ROOT do 13 | # This script is a way to update your development environment automatically. 14 | # Add necessary update steps to this file. 15 | 16 | puts "== Installing dependencies ==" 17 | system! "gem install bundler --conservative" 18 | system("bundle check") || system!("bundle install") 19 | 20 | puts "\n== Updating database ==" 21 | system! "bin/rails db:migrate" 22 | 23 | puts "\n== Removing old logs and tempfiles ==" 24 | system! "bin/rails log:clear tmp:clear" 25 | 26 | puts "\n== Restarting application server ==" 27 | system! "bin/rails restart" 28 | end 29 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require_relative "config/environment" 4 | 5 | run Rails.application 6 | Rails.application.load_server 7 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) 2 | 3 | require "bundler/setup" # Set up gems listed in the Gemfile. 4 | require "bootsnap/setup" # Speed up boot time by caching expensive operations. 5 | -------------------------------------------------------------------------------- /config/brakeman.ignore: -------------------------------------------------------------------------------- 1 | { 2 | "ignored_warnings": [ 3 | { 4 | "warning_type": "SQL Injection", 5 | "warning_code": 0, 6 | "fingerprint": "30b19388240b1f6855a70229fc36d62209dbef1b11b75c0464f25e59738fba2c", 7 | "check_name": "SQL", 8 | "message": "Possible SQL injection", 9 | "file": "app/queries/subscriber_lists_by_criteria_query.rb", 10 | "line": 49, 11 | "link": "https://brakemanscanner.org/docs/warning_types/sql_injection/", 12 | "code": "scope.where(\":value IN (SELECT json_array_elements(#{field}->:key->'any')::text)\", :key => key, :value => (\"\\\"#{value.gsub(\"\\\"\", \"\\\\\\\"\")}\\\"\"))", 13 | "render_path": null, 14 | "location": { 15 | "type": "method", 16 | "class": "SubscriberListsByCriteriaQuery", 17 | "method": "type_rule" 18 | }, 19 | "user_input": "field", 20 | "confidence": "Weak", 21 | "cwe_id": [ 22 | 89 23 | ], 24 | "note": "" 25 | } 26 | ], 27 | "updated": "2022-10-27 16:17:02 +0100", 28 | "brakeman_version": "5.3.1" 29 | } 30 | -------------------------------------------------------------------------------- /config/bulk_email/email_addresses.txt: -------------------------------------------------------------------------------- 1 | test@example.com 2 | -------------------------------------------------------------------------------- /config/database.yml: -------------------------------------------------------------------------------- 1 | # This file overwritten on deploy 2 | 3 | default: &default 4 | adapter: postgresql 5 | encoding: unicode 6 | # For details on connection pooling, see rails configuration guide 7 | # http://guides.rubyonrails.org/configuring.html#database-pooling 8 | pool: 50 9 | 10 | development: 11 | <<: *default 12 | database: email-alert-api_development 13 | 14 | test: 15 | <<: *default 16 | database: email-alert-api_test 17 | url: <%= ENV["TEST_DATABASE_URL"] %> 18 | 19 | production: 20 | <<: *default 21 | database: email-alert-api_production 22 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative "application" 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /config/initializers/application_controller_renderer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # ActiveSupport::Reloader.to_prepare do 4 | # ApplicationController.renderer.defaults.merge!( 5 | # http_host: 'example.org', 6 | # https: false 7 | # ) 8 | # end 9 | -------------------------------------------------------------------------------- /config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | # Rails.application.config.assets.version = "1.0" 5 | 6 | # Add additional assets to the asset load path. 7 | # Rails.application.config.assets.paths << Emoji.images_path 8 | -------------------------------------------------------------------------------- /config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| /my_noisy_library/.match?(line) } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code 7 | # by setting BACKTRACE=1 before calling your invocation, like "BACKTRACE=1 ./bin/rails runner 'MyClass.perform'". 8 | Rails.backtrace_cleaner.remove_silencers! if ENV["BACKTRACE"] 9 | -------------------------------------------------------------------------------- /config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Specify a serializer for the signed and encrypted cookie jars. 4 | # Valid options are :json, :marshal, and :hybrid. 5 | Rails.application.config.action_dispatch.cookies_serializer = :json 6 | -------------------------------------------------------------------------------- /config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure parameters to be partially matched (e.g. passw matches password) and filtered from the log file. 4 | # Use this to limit dissemination of sensitive information. 5 | # See the ActiveSupport::ParameterFilter documentation for supported notations and behaviors. 6 | Rails.application.config.filter_parameters += %i[ 7 | passw email secret token _key crypt salt certificate otp ssn cvv cvc 8 | ] 9 | -------------------------------------------------------------------------------- /config/initializers/gds_sso.rb: -------------------------------------------------------------------------------- 1 | GDS::SSO.config do |config| 2 | config.additional_mock_permissions_required = %w[internal_app] 3 | end 4 | -------------------------------------------------------------------------------- /config/initializers/govuk_error.rb: -------------------------------------------------------------------------------- 1 | GovukError.configure do |config| 2 | config.excluded_exceptions += %w[ 3 | SendEmailService::NotifyCommunicationFailure 4 | RatelimitExceededError 5 | ] 6 | 7 | config.rails.report_rescued_exceptions = false 8 | end 9 | -------------------------------------------------------------------------------- /config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, "\\1en" 8 | # inflect.singular /^(ox)en/i, "\\1" 9 | # inflect.irregular "person", "people" 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym "RESTful" 16 | # end 17 | -------------------------------------------------------------------------------- /config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | -------------------------------------------------------------------------------- /config/initializers/prometheus.rb: -------------------------------------------------------------------------------- 1 | require "govuk_app_config/govuk_prometheus_exporter" 2 | require "collectors/global_prometheus_collector" 3 | 4 | GovukPrometheusExporter.configure(collectors: [Collectors::GlobalPrometheusCollector]) 5 | -------------------------------------------------------------------------------- /config/initializers/secrets_to_credentials.rb: -------------------------------------------------------------------------------- 1 | # Rails 7 has begung to deprecate Rails.application.secrets in favour 2 | # of Rails.application.credentials, but that adds the burden of master key 3 | # adminstration without giving us any benefit (because our production 4 | # secrets are handled as env vars, not committed to our repo. Here we 5 | # loads the config/secrets.YML values into Rails.application.credentials, 6 | # retaining the existing behaviour while dropping deprecated references. 7 | 8 | Rails.application.credentials.merge!(Rails.application.config_for(:secrets)) 9 | -------------------------------------------------------------------------------- /config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.session_store :cookie_store, key: "_email-alert-api_session" 4 | -------------------------------------------------------------------------------- /config/initializers/sidekiq.rb: -------------------------------------------------------------------------------- 1 | # Set strict args so we're ready for Sidekiq 7 2 | Sidekiq.strict_args! 3 | 4 | Sidekiq.configure_server do |config| 5 | config.logger.level = Rails.logger.level 6 | end 7 | -------------------------------------------------------------------------------- /config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /config/initializers/zeitwerk.rb: -------------------------------------------------------------------------------- 1 | Rails.autoloaders.each do |autoloader| 2 | autoloader.inflector.inflect( 3 | "symbolize_json" => "SymbolizeJSON", 4 | ) 5 | end 6 | -------------------------------------------------------------------------------- /config/puma.rb: -------------------------------------------------------------------------------- 1 | require "govuk_app_config/govuk_puma" 2 | GovukPuma.configure_rails(self) 3 | -------------------------------------------------------------------------------- /config/spring.rb: -------------------------------------------------------------------------------- 1 | %w[ 2 | .ruby-version 3 | .rbenv-vars 4 | tmp/restart.txt 5 | tmp/caching-dev.txt 6 | ].each { |path| Spring.watch(path) } 7 | -------------------------------------------------------------------------------- /db/migrate/20141025105641_create_subscriber_list.rb: -------------------------------------------------------------------------------- 1 | class CreateSubscriberList < ActiveRecord::Migration[4.2] 2 | def change 3 | create_table :subscriber_lists, force: true do |t| 4 | t.string :title, limit: 255 5 | t.string :gov_delivery_id, limit: 255 6 | #t.hstore :tags 7 | t.timestamps 8 | end 9 | 10 | #add_index :subscriber_lists, :tags, using: :gin 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/20150428072646_seed_policy_subscriptions.rb: -------------------------------------------------------------------------------- 1 | # rubocop:disable Lint/UnreachableCode 2 | 3 | require "csv" 4 | 5 | class SeedPolicySubscriptions < ActiveRecord::Migration[4.2] 6 | def change 7 | return 8 | 9 | slug_mapping = {} 10 | mapping_csv_path = File.join(File.dirname(__FILE__), "20150428072646_seed_policy_subscriptions_mapping.csv") 11 | CSV.foreach(mapping_csv_path, headers: true) do |mapping| 12 | slug_mapping[mapping["old_slug"]] = mapping["new_slug"] 13 | end 14 | 15 | csv_path = File.join(File.dirname(__FILE__), "20150428072646_seed_policy_subscriptions.csv") 16 | 17 | CSV.foreach(csv_path) do |(slug, gov_delivery_id)| 18 | old_slug = slug.gsub(%r{^/}, "") 19 | 20 | if (new_slug = slug_mapping[old_slug]) 21 | SubscriberList.create!( 22 | gov_delivery_id: gov_delivery_id, 23 | tags: { 24 | policies: [new_slug], 25 | }, 26 | ) 27 | else 28 | puts "No mapping to create subscription for #{old_slug}" 29 | end 30 | end 31 | end 32 | end 33 | 34 | # rubocop:enable Lint/UnreachableCode 35 | -------------------------------------------------------------------------------- /db/migrate/20150508081921_reslug_social_equality.rb: -------------------------------------------------------------------------------- 1 | # rubocop:disable Lint/UnreachableCode 2 | 3 | class ReslugSocialEquality < ActiveRecord::Migration[4.2] 4 | def up 5 | return 6 | 7 | list = SubscriberListQuery.where_tags_equal(policies: %w[social-equality]).first 8 | 9 | if list 10 | list.update(tags: { 11 | policies: %w[equality], 12 | }) 13 | list.reload 14 | puts %{Reslugged "social-equality" to #{list.tags[:policies]}} 15 | else 16 | puts %{Could not find a subscriber list with the policy tag "social-equality"} 17 | end 18 | end 19 | 20 | def down 21 | list = SubscriberListQuery.where_tags_equal(policies: %w[equality]).first 22 | 23 | if list 24 | list.update(tags: { 25 | policies: %w[social-equality], 26 | }) 27 | list.reload 28 | puts %{Reslugged "equality" to #{list.tags[:policies]}} 29 | else 30 | puts %{Could not find a subscriber list with the policy tag "equality"} 31 | end 32 | end 33 | end 34 | 35 | # rubocop:enable Lint/UnreachableCode 36 | -------------------------------------------------------------------------------- /db/migrate/20151001102405_add_links_to_subscriber_list.rb: -------------------------------------------------------------------------------- 1 | class AddLinksToSubscriberList < ActiveRecord::Migration[4.2] 2 | def change 3 | #add_column :subscriber_lists, :links, :hstore, null: false, default: {} 4 | 5 | #add_index :subscriber_lists, :links, using: :gin 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20151001154627_add_null_false_to_tags_field.rb: -------------------------------------------------------------------------------- 1 | class AddNullFalseToTagsField < ActiveRecord::Migration[4.2] 2 | def change 3 | #change_column :subscriber_lists, :tags, :hstore, null: false, default: {} 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20151102165117_rename_policy_links_to_parent.rb: -------------------------------------------------------------------------------- 1 | # rubocop:disable Lint/UnreachableCode 2 | 3 | class RenamePolicyLinksToParent < ActiveRecord::Migration[4.2] 4 | def up 5 | return 6 | 7 | subscriber_lists_with_key(:policy).each do |list| 8 | content_id = list.links[:policy] 9 | list.links = { parent: content_id } 10 | list.save! 11 | end 12 | end 13 | 14 | def down 15 | subscriber_lists_with_key(:parent).each do |list| 16 | content_id = list.links[:parent] 17 | list.links = { policy: content_id } 18 | list.save! 19 | end 20 | end 21 | 22 | def subscriber_lists_with_key(key) 23 | SubscriberList.where("(links -> :key) IS NOT NULL", key: key) 24 | end 25 | end 26 | 27 | # rubocop:enable Lint/UnreachableCode 28 | -------------------------------------------------------------------------------- /db/migrate/20151111124933_remove_policy_subscriber_lists_with_erroneous_links.rb: -------------------------------------------------------------------------------- 1 | # rubocop:disable Lint/UnreachableCode 2 | 3 | class RemovePolicySubscriberListsWithErroneousLinks < ActiveRecord::Migration[4.2] 4 | def up 5 | return 6 | 7 | subscriber_lists_with_key(:policies).each(&:destroy!) 8 | end 9 | 10 | def down 11 | # noop 12 | end 13 | 14 | def subscriber_lists_with_key(key) 15 | SubscriberList.where("(tags -> :key) IS NOT NULL", key: key) 16 | end 17 | end 18 | 19 | # rubocop:enable Lint/UnreachableCode 20 | -------------------------------------------------------------------------------- /db/migrate/20151111150405_rename_parent_links_to_policies.rb: -------------------------------------------------------------------------------- 1 | # rubocop:disable Lint/UnreachableCode 2 | 3 | class RenameParentLinksToPolicies < ActiveRecord::Migration[4.2] 4 | def up 5 | return 6 | 7 | subscriber_lists_with_key(:parent).each do |sl| 8 | sl.links = { policies: sl.links[:parent] } 9 | sl.save! 10 | end 11 | end 12 | 13 | def down 14 | subscriber_lists_with_key(:policies).each do |sl| 15 | sl.links = { parent: sl.links[:policies] } 16 | sl.save! 17 | end 18 | end 19 | 20 | def subscriber_lists_with_key(key) 21 | SubscriberList.where("(links -> :key) IS NOT NULL", key: key) 22 | end 23 | end 24 | 25 | # rubocop:enable Lint/UnreachableCode 26 | -------------------------------------------------------------------------------- /db/migrate/20151112144850_rename_policy_tags_to_policies.rb: -------------------------------------------------------------------------------- 1 | # rubocop:disable Lint/UnreachableCode 2 | 3 | class RenamePolicyTagsToPolicies < ActiveRecord::Migration[4.2] 4 | def up 5 | return 6 | 7 | subscriber_lists_with_key(:policy).each do |sl| 8 | sl.tags = { policies: sl.tags[:policy] } 9 | sl.save! 10 | end 11 | end 12 | 13 | def down 14 | subscriber_lists_with_key(:policies).each do |sl| 15 | sl.tags = { policy: sl.tags[:policies] } 16 | sl.save! 17 | end 18 | end 19 | 20 | def subscriber_lists_with_key(key) 21 | SubscriberList.where("(tags -> :key) IS NOT NULL", key: key) 22 | end 23 | end 24 | 25 | # rubocop:enable Lint/UnreachableCode 26 | -------------------------------------------------------------------------------- /db/migrate/20160205102023_add_document_type_to_subscriber_list.rb: -------------------------------------------------------------------------------- 1 | class AddDocumentTypeToSubscriberList < ActiveRecord::Migration[4.2] 2 | def change 3 | add_column :subscriber_lists, :document_type, :string, default: "", null: false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20160209143102_add_foreign_travel_advice_subscriber_lists.rb: -------------------------------------------------------------------------------- 1 | # rubocop:disable Lint/UnreachableCode 2 | 3 | require "csv" 4 | 5 | class AddForeignTravelAdviceSubscriberLists < ActiveRecord::Migration[4.2] 6 | def change 7 | return 8 | 9 | csv_path = File.join(File.dirname(__FILE__), "20160209143102_add_foreign_travel_advice_subscriber_lists.csv") 10 | 11 | CSV.foreach(csv_path) do |gov_delivery_id, country_slug| 12 | SubscriberList.create!( 13 | gov_delivery_id: gov_delivery_id, 14 | links: { 15 | countries: [country_slug], 16 | }, 17 | document_type: "travel_advice", 18 | ) 19 | puts "Created subscriber list for #{gov_delivery_id} -> #{country_slug}" 20 | end 21 | end 22 | end 23 | 24 | # rubocop:enable Lint/UnreachableCode 25 | -------------------------------------------------------------------------------- /db/migrate/20160215144137_add_content_i_ds_to_travel_advice_topics.rb: -------------------------------------------------------------------------------- 1 | # rubocop:disable Lint/UnreachableCode 2 | 3 | require "csv" 4 | 5 | class AddContentIDsToTravelAdviceTopics < ActiveRecord::Migration[4.2] 6 | def change 7 | return 8 | 9 | csv_path = File.join(File.dirname(__FILE__), "20160215144137_add_content_i_ds_to_travel_advice_topics.csv") 10 | 11 | # Clean up after last migration - delete the erroneous tags 12 | s = SubscriberList.find_by(gov_delivery_id: "gov_delivery_id") 13 | s.destroy! 14 | puts "Destroyed erroneous topic" 15 | 16 | CSV.foreach(csv_path, headers: true, return_headers: false) do |row| 17 | s = SubscriberList.find_by(gov_delivery_id: row["gov_delivery_id"]) 18 | s.links = { 19 | countries: [row["content_id"]], 20 | } 21 | s.save 22 | puts "Updated #{row['gov_delivery_id']}" 23 | end 24 | end 25 | end 26 | 27 | # rubocop:enable Lint/UnreachableCode 28 | -------------------------------------------------------------------------------- /db/migrate/20160224152054_add_all_countries_subscriber_list.rb: -------------------------------------------------------------------------------- 1 | # rubocop:disable Lint/UnreachableCode 2 | 3 | class AddAllCountriesSubscriberList < ActiveRecord::Migration[4.2] 4 | def change 5 | return 6 | 7 | SubscriberList.create!( 8 | gov_delivery_id: "UKGOVUK_391", 9 | document_type: "travel_advice", 10 | ) 11 | end 12 | end 13 | 14 | # rubocop:enable Lint/UnreachableCode 15 | -------------------------------------------------------------------------------- /db/migrate/20160718090427_add_json_columns_to_subscriber_list.rb: -------------------------------------------------------------------------------- 1 | class AddJsonColumnsToSubscriberList < ActiveRecord::Migration[4.2] 2 | def change 3 | add_column :subscriber_lists, :tags_json, :json, default: {}, null: false 4 | add_column :subscriber_lists, :links_json, :json, default: {}, null: false 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20160905121642_remove_hstore.rb: -------------------------------------------------------------------------------- 1 | class RemoveHstore < ActiveRecord::Migration[4.2] 2 | def up 3 | #remove_column :subscriber_lists, :tags 4 | #remove_column :subscriber_lists, :links 5 | add_column :subscriber_lists, :tags, :json, default: {}, null: false 6 | add_column :subscriber_lists, :links, :json, default: {}, null: false 7 | 8 | SubscriberList.connection.update("UPDATE subscriber_lists SET tags = tags_json, links = links_json") 9 | end 10 | 11 | def down 12 | remove_column :subscriber_lists, :tags 13 | remove_column :subscriber_lists, :links 14 | add_column :subscriber_lists, :tags, :hstore, default: {}, null: false 15 | add_column :subscriber_lists, :links, :hstore, default: {}, null: false 16 | 17 | SubscriberList.all.each do |subscriber_list| 18 | subscriber_list.update_columns( 19 | tags: subscriber_list.tags_json, 20 | links: subscriber_list.links_json, 21 | ) 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /db/migrate/20170208150700_remove_temp_json_fields.rb: -------------------------------------------------------------------------------- 1 | class RemoveTempJsonFields < ActiveRecord::Migration[4.2] 2 | def change 3 | remove_column :subscriber_lists, :links_json 4 | remove_column :subscriber_lists, :tags_json 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20170221141514_create_notification_log.rb: -------------------------------------------------------------------------------- 1 | class CreateNotificationLog < ActiveRecord::Migration[4.2] 2 | def change 3 | create_table :notification_logs do |t| 4 | t.string :govuk_request_id, index: true, default: "" 5 | t.string :content_id, default: "" 6 | t.datetime :public_updated_at 7 | t.json :links, default: {} 8 | t.json :tags, default: {} 9 | t.string :document_type, default: "" 10 | t.string :emailing_app, default: "", null: false 11 | t.json :gov_delivery_ids, default: [] 12 | t.string :publishing_app, default: "" 13 | 14 | t.timestamps null: false 15 | end 16 | 17 | add_index :notification_logs, %i[content_id public_updated_at] 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /db/migrate/20170302162543_add_enabled_flag_to_subscriber_lists.rb: -------------------------------------------------------------------------------- 1 | class AddEnabledFlagToSubscriberLists < ActiveRecord::Migration[4.2] 2 | def change 3 | add_column :subscriber_lists, :enabled, :boolean, default: true, null: false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20170302162818_add_enabled_disabled_gov_delivery_ids_to_notification_log.rb: -------------------------------------------------------------------------------- 1 | class AddEnabledDisabledGovDeliveryIdsToNotificationLog < ActiveRecord::Migration[4.2] 2 | def change 3 | add_column :notification_logs, :enabled_gov_delivery_ids, :json, default: [] 4 | add_column :notification_logs, :disabled_gov_delivery_ids, :json, default: [] 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20170320170223_add_supertype_fields.rb: -------------------------------------------------------------------------------- 1 | class AddSupertypeFields < ActiveRecord::Migration[4.2] 2 | def change 3 | add_column :subscriber_lists, :email_document_supertype, :string, default: "", null: false 4 | add_column :subscriber_lists, :government_document_supertype, :string, default: "", null: false 5 | add_column :notification_logs, :email_document_supertype, :string, default: "" 6 | add_column :notification_logs, :government_document_supertype, :string, default: "" 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20170327104203_add_migrated_from_gov_uk_delivery_to_subscriber_list.rb: -------------------------------------------------------------------------------- 1 | class AddMigratedFromGovUkDeliveryToSubscriberList < ActiveRecord::Migration[4.2] 2 | def change 3 | add_column :subscriber_lists, :migrated_from_gov_uk_delivery, :boolean, default: false, null: false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20170720135533_add_subscriber_count_to_subscriber_lists.rb: -------------------------------------------------------------------------------- 1 | class AddSubscriberCountToSubscriberLists < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :subscriber_lists, :subscriber_count, :integer 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20170922091721_remove_redundant_columns_after_q2_mission.rb: -------------------------------------------------------------------------------- 1 | class RemoveRedundantColumnsAfterQ2Mission < ActiveRecord::Migration[5.1] 2 | def change 3 | remove_column :notification_logs, :enabled_gov_delivery_ids 4 | remove_column :notification_logs, :disabled_gov_delivery_ids 5 | remove_column :notification_logs, :emailing_app 6 | 7 | remove_column :subscriber_lists, :enabled 8 | 9 | # A list of migrated gov_delivery_ids is available in Google Drive: 10 | # https://drive.google.com/a/digital.cabinet-office.gov.uk/file/d/0B6JKO797SExjMm90eTB0ekxURTg/view 11 | remove_column :subscriber_lists, :migrated_from_gov_uk_delivery 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20171016143522_create_subscribers.rb: -------------------------------------------------------------------------------- 1 | class CreateSubscribers < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :subscribers do |t| 4 | t.string :address 5 | t.timestamps 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20171019072332_make_subscriber_address_not_null.rb: -------------------------------------------------------------------------------- 1 | class MakeSubscriberAddressNotNull < ActiveRecord::Migration[5.1] 2 | def change 3 | change_column_null :subscribers, :address, false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20171019072924_make_subscriber_address_unique.rb: -------------------------------------------------------------------------------- 1 | class MakeSubscriberAddressUnique < ActiveRecord::Migration[5.1] 2 | def change 3 | add_index :subscribers, :address, unique: true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20171020071004_create_subscriptions.rb: -------------------------------------------------------------------------------- 1 | class CreateSubscriptions < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :subscriptions do |t| 4 | t.references :subscriber, null: false, foreign_key: { on_delete: :cascade } 5 | t.references :subscriber_list, null: false, foreign_key: true 6 | t.timestamps 7 | end 8 | 9 | add_index :subscriptions, %i(subscriber_id subscriber_list_id), unique: true 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20171020091213_create_notifications.rb: -------------------------------------------------------------------------------- 1 | class CreateNotifications < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :notifications do |t| 4 | t.uuid :content_id, null: false 5 | t.text :title, null: false 6 | t.text :base_path, null: false 7 | t.text :change_note, null: false 8 | t.text :description, null: false 9 | t.json :links, null: false, default: {} 10 | t.json :tags, null: false, default: {} 11 | t.datetime :public_updated_at, null: false 12 | t.string :email_document_supertype, null: false 13 | t.string :government_document_supertype, null: false 14 | t.string :govuk_request_id, null: false 15 | t.string :document_type, null: false 16 | t.string :publishing_app, null: false 17 | t.timestamps 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /db/migrate/20171023094334_create_emails.rb: -------------------------------------------------------------------------------- 1 | class CreateEmails < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :emails do |t| 4 | t.string :subject, null: false 5 | t.text :body, null: false 6 | t.references :notification, null: false, foreign_key: true 7 | t.timestamps 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20171109085657_create_delivery_attempt.rb: -------------------------------------------------------------------------------- 1 | class CreateDeliveryAttempt < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :delivery_attempts do |t| 4 | t.references :email, null: false, foreign_key: true 5 | t.integer :status, null: false 6 | t.integer :provider, null: false 7 | t.string :reference, null: false 8 | t.timestamps 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20171109091838_add_address_to_email.rb: -------------------------------------------------------------------------------- 1 | class AddAddressToEmail < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :emails, :address, :string, default: "", null: false 4 | change_column_default :emails, :address, nil 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20171110163345_make_subscriber_address_nullable.rb: -------------------------------------------------------------------------------- 1 | class MakeSubscriberAddressNullable < ActiveRecord::Migration[5.1] 2 | def change 3 | change_column_null :subscribers, :address, true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20171113162352_rename_notifications_to_content_changes.rb: -------------------------------------------------------------------------------- 1 | class RenameNotificationsToContentChanges < ActiveRecord::Migration[5.1] 2 | def change 3 | rename_table :notifications, :content_changes 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20171113163731_update_emails_reference_from_notifications_to_content_changes.rb: -------------------------------------------------------------------------------- 1 | class UpdateEmailsReferenceFromNotificationsToContentChanges < ActiveRecord::Migration[5.1] 2 | def change 3 | remove_column :emails, :notification_id, :integer 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20171114171050_add_subscription_content.rb: -------------------------------------------------------------------------------- 1 | class AddSubscriptionContent < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :subscription_contents do |t| 4 | t.references :subscription, null: false, foreign_key: true 5 | t.references :content_change, null: false, foreign_key: true 6 | t.references :email, null: true, foreign_key: true 7 | t.timestamps 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20171115162823_index_delivery_attempt_email_id_updated_at.rb: -------------------------------------------------------------------------------- 1 | class IndexDeliveryAttemptEmailIdUpdatedAt < ActiveRecord::Migration[5.1] 2 | def change 3 | add_index :delivery_attempts, %i[email_id updated_at] 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20171123142518_add_uuid_to_subscriptions.rb: -------------------------------------------------------------------------------- 1 | class AddUuidToSubscriptions < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :subscriptions, :uuid, :uuid 4 | add_index :subscriptions, :uuid 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20171124093729_add_processed_at_to_content_change.rb: -------------------------------------------------------------------------------- 1 | class AddProcessedAtToContentChange < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :content_changes, :processed_at, :datetime 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20171124105714_uuid_constraints.rb: -------------------------------------------------------------------------------- 1 | class UuidConstraints < ActiveRecord::Migration[5.1] 2 | def change 3 | change_column_null :subscriptions, :uuid, false 4 | 5 | remove_index :subscriptions, :uuid 6 | add_index :subscriptions, :uuid, unique: true 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20171130194217_create_users.rb: -------------------------------------------------------------------------------- 1 | class CreateUsers < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :users do |t| 4 | t.string "name" 5 | t.string "email" 6 | t.string "uid" 7 | t.string "organisation_slug" 8 | t.string "organisation_content_id" 9 | t.string "permissions", array: true, default: [] 10 | t.boolean "remotely_signed_out", default: false 11 | t.boolean "disabled", default: false 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /db/migrate/20171201082903_remove_null_gov_delivery_ids.rb: -------------------------------------------------------------------------------- 1 | class RemoveNullGovDeliveryIds < ActiveRecord::Migration[5.1] 2 | def up 3 | SubscriberList.where(gov_delivery_id: nil).destroy_all 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20171201094128_make_subscriber_list_title_not_null.rb: -------------------------------------------------------------------------------- 1 | class MakeSubscriberListTitleNotNull < ActiveRecord::Migration[5.1] 2 | def change 3 | change_column_null :subscriber_lists, :title, false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20171208081924_add_priority_to_change_content.rb: -------------------------------------------------------------------------------- 1 | class AddPriorityToChangeContent < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :content_changes, :priority, :integer, default: 0 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20171212130557_add_indexes_to_subscriber_list.rb: -------------------------------------------------------------------------------- 1 | class AddIndexesToSubscriberList < ActiveRecord::Migration[5.1] 2 | disable_ddl_transaction! 3 | 4 | def change 5 | add_index :subscriber_lists, [:document_type], algorithm: :concurrently 6 | add_index :subscriber_lists, [:email_document_supertype], algorithm: :concurrently 7 | add_index :subscriber_lists, [:government_document_supertype], algorithm: :concurrently 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20171212133939_allow_nullable_subscription_contents_subscription_id.rb: -------------------------------------------------------------------------------- 1 | class AllowNullableSubscriptionContentsSubscriptionId < ActiveRecord::Migration[5.1] 2 | def change 3 | change_column_null :subscription_contents, :subscription_id, true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20171214113834_delete_test_emails_and_delivery_attempts.rb: -------------------------------------------------------------------------------- 1 | class DeleteTestEmailsAndDeliveryAttempts < ActiveRecord::Migration[5.1] 2 | def up 3 | # Emails, DeliveryAttempts and SubscriptionContents at this point are all test data 4 | SubscriptionContent.delete_all 5 | DeliveryAttempt.delete_all 6 | Email.delete_all 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20171215154302_delivery_attempt_delete_cascade.rb: -------------------------------------------------------------------------------- 1 | class DeliveryAttemptDeleteCascade < ActiveRecord::Migration[5.1] 2 | def change 3 | remove_foreign_key :delivery_attempts, :emails 4 | add_foreign_key :delivery_attempts, :emails, on_delete: :cascade 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20171215161358_cascade_nullify_subscription_contents_emails.rb: -------------------------------------------------------------------------------- 1 | class CascadeNullifySubscriptionContentsEmails < ActiveRecord::Migration[5.1] 2 | def change 3 | remove_foreign_key :subscription_contents, :emails 4 | add_foreign_key :subscription_contents, :emails, on_delete: :nullify 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20171215163908_cascade_nullify_subscription_contents_subscriptions.rb: -------------------------------------------------------------------------------- 1 | class CascadeNullifySubscriptionContentsSubscriptions < ActiveRecord::Migration[5.1] 2 | def change 3 | remove_foreign_key :subscription_contents, :subscriptions 4 | add_foreign_key :subscription_contents, :subscriptions, on_delete: :nullify 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20171219103753_remove_duplicate_subscriber_list.rb: -------------------------------------------------------------------------------- 1 | class RemoveDuplicateSubscriberList < ActiveRecord::Migration[5.1] 2 | def change 3 | # production & staging 4 | SubscriberList.where( 5 | title: "news stories related to UK Visas and Immigration and China", 6 | gov_delivery_id: "UKGOVUK_35116", 7 | ).delete_all 8 | 9 | # integration 10 | SubscriberList.where( 11 | title: "news stories related to UK Visas and Immigration and China", 12 | gov_delivery_id: "UKGOVUKDUP_35116", 13 | ).delete_all 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /db/migrate/20171219104253_make_subscriber_list_title_unique.rb: -------------------------------------------------------------------------------- 1 | class MakeSubscriberListTitleUnique < ActiveRecord::Migration[5.1] 2 | disable_ddl_transaction! 3 | 4 | def change 5 | add_index :subscriber_lists, [:title], unique: true, algorithm: :concurrently 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20180116163006_store_signon_user_ids.rb: -------------------------------------------------------------------------------- 1 | class StoreSignonUserIds < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :content_changes, :signon_user_uid, :string 4 | add_column :subscriber_lists, :signon_user_uid, :string 5 | add_column :subscriptions, :signon_user_uid, :string 6 | add_column :subscribers, :signon_user_uid, :string 7 | add_column :delivery_attempts, :signon_user_uid, :string 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20180118085957_add_frequency_to_subscriptions.rb: -------------------------------------------------------------------------------- 1 | class AddFrequencyToSubscriptions < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :subscriptions, :frequency, :integer, null: false, default: 0 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20180118134516_create_digest_runs.rb: -------------------------------------------------------------------------------- 1 | class CreateDigestRuns < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :digest_runs do |t| 4 | t.date :date, null: false 5 | t.datetime :starts_at, null: false 6 | t.datetime :ends_at, null: false 7 | t.integer :range, null: false 8 | t.datetime :completed_at 9 | t.timestamps 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/20180123093411_add_matched_content_change.rb: -------------------------------------------------------------------------------- 1 | class AddMatchedContentChange < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :matched_content_changes do |t| 4 | t.references :content_change, null: false, foreign_key: true 5 | t.references :subscriber_list, null: false, foreign_key: true 6 | t.timestamps 7 | end 8 | 9 | add_index :matched_content_changes, %i(content_change_id subscriber_list_id), unique: true, name: "index_matched_content_changes_content_change_subscriber_list" 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20180126144040_create_digest_run_subscribers.rb: -------------------------------------------------------------------------------- 1 | class CreateDigestRunSubscribers < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :digest_run_subscribers do |t| 4 | t.integer :digest_run_id, null: false 5 | t.integer :subscriber_id, null: false 6 | t.datetime :completed_at 7 | t.timestamps 8 | end 9 | 10 | add_foreign_key :digest_run_subscribers, :digest_runs, on_delete: :cascade 11 | add_foreign_key :digest_run_subscribers, :subscribers, on_delete: :cascade 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20180129081557_add_digest_run_subscriber_id_to_subscription_contents.rb: -------------------------------------------------------------------------------- 1 | class AddDigestRunSubscriberIdToSubscriptionContents < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :subscription_contents, :digest_run_subscriber_id, :integer 4 | add_foreign_key :subscription_contents, :digest_run_subscribers, on_delete: :cascade 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20180205165003_add_deleted_at_to_subscriptions.rb: -------------------------------------------------------------------------------- 1 | class AddDeletedAtToSubscriptions < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :subscriptions, :deleted_at, :datetime 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20180212121358_add_unique_index_on_reference.rb: -------------------------------------------------------------------------------- 1 | class AddUniqueIndexOnReference < ActiveRecord::Migration[5.1] 2 | disable_ddl_transaction! 3 | 4 | def change 5 | add_index :delivery_attempts, :reference, unique: true, algorithm: :concurrently 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20180215144634_add_case_insensitive_index_on_emails.rb: -------------------------------------------------------------------------------- 1 | class AddCaseInsensitiveIndexOnEmails < ActiveRecord::Migration[5.1] 2 | def change 3 | remove_index :subscribers, :address 4 | add_index :subscribers, "LOWER(address)", name: "index_subscribers_on_lower_address", unique: true 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20180216100846_add_subscription_contents_index.rb: -------------------------------------------------------------------------------- 1 | class AddSubscriptionContentsIndex < ActiveRecord::Migration[5.1] 2 | def change 3 | add_index :subscription_contents, :digest_run_subscriber_id 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20180220125651_make_delivery_attempt_reference_uuid.rb: -------------------------------------------------------------------------------- 1 | class MakeDeliveryAttemptReferenceUuid < ActiveRecord::Migration[5.1] 2 | def up 3 | DeliveryAttempt.find_each do |delivery_attempt| 4 | delivery_attempt.update(reference: SecureRandom.uuid) 5 | end 6 | 7 | change_column :delivery_attempts, :reference, "UUID USING reference::uuid" 8 | end 9 | 10 | def down 11 | change_column :delivery_attempts, :reference, :text 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20180222142356_add_subscriber_count_to_digest_run.rb: -------------------------------------------------------------------------------- 1 | class AddSubscriberCountToDigestRun < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :digest_runs, :subscriber_count, :integer 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20180226101223_add_finished_sending_at_to_emails.rb: -------------------------------------------------------------------------------- 1 | class AddFinishedSendingAtToEmails < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :emails, :finished_sending_at, :datetime 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20180226101630_index_finished_sending_at_on_emails.rb: -------------------------------------------------------------------------------- 1 | class IndexFinishedSendingAtOnEmails < ActiveRecord::Migration[5.1] 2 | disable_ddl_transaction! 3 | 4 | def change 5 | add_index :emails, :finished_sending_at, algorithm: :concurrently 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20180228112734_create_email_archive.rb: -------------------------------------------------------------------------------- 1 | class CreateEmailArchive < ActiveRecord::Migration[5.1] 2 | def change 3 | create_table :email_archives, id: :uuid, default: "uuid_generate_v4()" do |t| 4 | t.string "subject", null: false 5 | t.bigint "subscriber_id" 6 | t.json "content_change" 7 | t.boolean "sent", null: false 8 | t.datetime "created_at", null: false 9 | t.datetime "archived_at", null: false 10 | t.datetime "finished_sending_at", null: false 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20180228122502_add_archived_at_to_emails.rb: -------------------------------------------------------------------------------- 1 | class AddArchivedAtToEmails < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :emails, :archived_at, :datetime 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20180228122604_index_archived_at_on_emails.rb: -------------------------------------------------------------------------------- 1 | class IndexArchivedAtOnEmails < ActiveRecord::Migration[5.1] 2 | disable_ddl_transaction! 3 | 4 | def change 5 | add_index :emails, :archived_at, algorithm: :concurrently 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20180228132051_add_deactivated_at_to_subscriber.rb: -------------------------------------------------------------------------------- 1 | class AddDeactivatedAtToSubscriber < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :subscribers, :deactivated_at, :datetime 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20180228144454_add_source_to_subscriptions.rb: -------------------------------------------------------------------------------- 1 | class AddSourceToSubscriptions < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :subscriptions, :source, :integer, default: 0, null: false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20180301130127_remove_old_uuid_fields.rb: -------------------------------------------------------------------------------- 1 | class RemoveOldUuidFields < ActiveRecord::Migration[5.1] 2 | def up 3 | remove_column :delivery_attempts, :reference 4 | remove_column :subscriptions, :uuid 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20180301132513_add_subscription_ended_at.rb: -------------------------------------------------------------------------------- 1 | class AddSubscriptionEndedAt < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :subscriptions, :ended_at, :datetime 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20180301141036_remove_deleted_at_from_subscription.rb: -------------------------------------------------------------------------------- 1 | class RemoveDeletedAtFromSubscription < ActiveRecord::Migration[5.1] 2 | def change 3 | remove_column :subscriptions, :deleted_at, :datetime 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20180301141950_add_ended_reason_to_subscriptions.rb: -------------------------------------------------------------------------------- 1 | class AddEndedReasonToSubscriptions < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :subscriptions, :ended_reason, :integer 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20180301151800_remove_unique_index_on_subscriptions.rb: -------------------------------------------------------------------------------- 1 | class RemoveUniqueIndexOnSubscriptions < ActiveRecord::Migration[5.1] 2 | def up 3 | remove_index :subscriptions, %w(subscriber_id subscriber_list_id) 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20180301153539_add_unique_index_on_active_subscriptions.rb: -------------------------------------------------------------------------------- 1 | class AddUniqueIndexOnActiveSubscriptions < ActiveRecord::Migration[5.1] 2 | disable_ddl_transaction! 3 | 4 | def up 5 | add_index :subscriptions, 6 | %w(subscriber_id subscriber_list_id), 7 | unique: true, 8 | where: "(ended_at IS NULL)" 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20180301180432_update_subscriber_list_titles.rb: -------------------------------------------------------------------------------- 1 | require "csv" 2 | 3 | class UpdateSubscriberListTitles < ActiveRecord::Migration[5.1] 4 | def up 5 | csv_file = Rails.root.join( 6 | "db", "migrate", "data", "subscriber-list-titles-2018-03-05.csv" 7 | ) 8 | 9 | CSV.read(csv_file, headers: :first_row) 10 | .reject { |item| update_list(item["id"], item["gov_delivery_id"], item["title"], true) } 11 | .each { |item| update_list(item["id"], item["gov_delivery_id"], item["title"], false) } 12 | end 13 | 14 | private 15 | 16 | def update_list(id, gov_delivery_id, title, first_attempt = true) 17 | list = SubscriberList.find(id) 18 | return skip_govdelivery_id(id) if list.gov_delivery_id != gov_delivery_id 19 | return true if list.title == title 20 | 21 | if SubscriberList.where(title: title).where.not(id: id).exists? 22 | suffix = first_attempt ? "will retry" : "wont update" 23 | puts "SubscriberList with id #{id} conflicts, #{suffix}" 24 | false 25 | else 26 | list.update!(title: title) 27 | end 28 | rescue ActiveRecord::RecordNotFound 29 | puts "No SubscriberList with id #{id}" 30 | true 31 | end 32 | 33 | def skip_govdelivery_id(id) 34 | puts "Skipping SubscriberList id #{id} due to gov_delivery_id mismatch" 35 | true 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /db/migrate/20180302090139_remove_incorrect_foreign_keys.rb: -------------------------------------------------------------------------------- 1 | class RemoveIncorrectForeignKeys < ActiveRecord::Migration[5.1] 2 | def change 3 | remove_foreign_key :matched_content_changes, :content_changes 4 | remove_foreign_key :matched_content_changes, :subscriber_lists 5 | 6 | remove_foreign_key :subscription_contents, :emails 7 | remove_foreign_key :subscription_contents, :subscriptions 8 | 9 | remove_foreign_key :subscriptions, :subscriber_lists 10 | remove_foreign_key :subscriptions, :subscribers 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/20180302090154_add_foreign_key_on_deletes.rb: -------------------------------------------------------------------------------- 1 | class AddForeignKeyOnDeletes < ActiveRecord::Migration[5.1] 2 | disable_ddl_transaction! 3 | 4 | def change 5 | add_foreign_key :matched_content_changes, :content_changes, on_delete: :cascade 6 | add_foreign_key :matched_content_changes, :subscriber_lists, on_delete: :cascade 7 | 8 | add_foreign_key :subscription_contents, :emails, on_delete: :cascade 9 | add_foreign_key :subscription_contents, :subscriptions, on_delete: :restrict 10 | 11 | add_foreign_key :subscriptions, :subscriber_lists, on_delete: :restrict 12 | add_foreign_key :subscriptions, :subscribers, on_delete: :restrict 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /db/migrate/20180305091124_increase_subscriber_list_title_length_limit.rb: -------------------------------------------------------------------------------- 1 | class IncreaseSubscriberListTitleLengthLimit < ActiveRecord::Migration[5.1] 2 | def up 3 | change_column :subscriber_lists, :title, :string, limit: 1000 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20180305094418_add_footnote_to_content_change.rb: -------------------------------------------------------------------------------- 1 | class AddFootnoteToContentChange < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :content_changes, :footnote, :text, null: false, default: "" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20180308105331_remove_subscriber_count_from_subscriber_lists.rb: -------------------------------------------------------------------------------- 1 | class RemoveSubscriberCountFromSubscriberLists < ActiveRecord::Migration[5.1] 2 | def change 3 | remove_column :subscriber_lists, :subscriber_count 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20180309114801_add_completed_at_column_to_delivery_attempt.rb: -------------------------------------------------------------------------------- 1 | class AddCompletedAtColumnToDeliveryAttempt < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :delivery_attempts, :completed_at, :datetime 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20180312105539_add_sent_at_column_to_delivery_attempts.rb: -------------------------------------------------------------------------------- 1 | class AddSentAtColumnToDeliveryAttempts < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :delivery_attempts, :sent_at, :datetime 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20180313081514_add_slug_to_subscriber_list.rb: -------------------------------------------------------------------------------- 1 | class AddSlugToSubscriberList < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :subscriber_lists, :slug, :string, limit: 1000 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20180313081630_make_subscriber_list_gov_delivery_id_not_null.rb: -------------------------------------------------------------------------------- 1 | class MakeSubscriberListGovDeliveryIdNotNull < ActiveRecord::Migration[5.1] 2 | def change 3 | change_column_null :subscriber_lists, :gov_delivery_id, false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20180313081909_make_subscriber_list_gov_delivery_id_longer.rb: -------------------------------------------------------------------------------- 1 | class MakeSubscriberListGovDeliveryIdLonger < ActiveRecord::Migration[5.1] 2 | def change 3 | change_column :subscriber_lists, :gov_delivery_id, :string, limit: 1000 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20180313083354_populate_all_slug_fields.rb: -------------------------------------------------------------------------------- 1 | class PopulateAllSlugFields < ActiveRecord::Migration[5.1] 2 | def up 3 | @taken_slugs = SubscriberList.pluck(:slug) 4 | 5 | SubscriberList.find_each.with_index do |subscriber_list, i| 6 | puts "Slugified #{i} subscriber lists..." if (i % 1000).zero? 7 | 8 | slug = slugify(subscriber_list.title) 9 | subscriber_list.update(slug: slug) 10 | taken_slugs << slug 11 | end 12 | end 13 | 14 | private 15 | 16 | attr_reader :taken_slugs 17 | 18 | def slugify(title) 19 | slug = title.parameterize 20 | index = 1 21 | 22 | while taken_slugs.include?(slug) 23 | index += 1 24 | slug = "#{title.parameterize}-#{index}" 25 | end 26 | 27 | slug 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /db/migrate/20180313084148_make_subscriber_list_slug_not_null.rb: -------------------------------------------------------------------------------- 1 | class MakeSubscriberListSlugNotNull < ActiveRecord::Migration[5.1] 2 | def change 3 | change_column_null :subscriber_lists, :slug, false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20180313090745_make_subscriber_list_slug_unique.rb: -------------------------------------------------------------------------------- 1 | class MakeSubscriberListSlugUnique < ActiveRecord::Migration[5.1] 2 | def change 3 | add_index :subscriber_lists, :slug, unique: true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20180313093530_make_subscriber_list_gov_delivery_id_nullable.rb: -------------------------------------------------------------------------------- 1 | class MakeSubscriberListGovDeliveryIdNullable < ActiveRecord::Migration[5.1] 2 | def change 3 | change_column_null :subscriber_lists, :gov_delivery_id, true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20180313093731_remove_gov_delivery_id_from_subscriber_lists.rb: -------------------------------------------------------------------------------- 1 | class RemoveGovDeliveryIdFromSubscriberLists < ActiveRecord::Migration[5.1] 2 | def up 3 | remove_column :subscriber_lists, :gov_delivery_id 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20180313152401_remove_notification_log.rb: -------------------------------------------------------------------------------- 1 | class RemoveNotificationLog < ActiveRecord::Migration[5.1] 2 | def change 3 | drop_table :notification_logs 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20180315000000_make_subscription_id_on_subscription_contents_not_null.rb: -------------------------------------------------------------------------------- 1 | class MakeSubscriptionIdOnSubscriptionContentsNotNull < ActiveRecord::Migration[5.1] 2 | def up 3 | deleted_count = SubscriptionContent.where(subscription_id: nil).delete_all 4 | 5 | puts "deleted #{deleted_count} rows" 6 | 7 | change_column_null :subscription_contents, :subscription_id, false 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20180315000001_clear_out_duplicate_subscription_contents.rb: -------------------------------------------------------------------------------- 1 | class ClearOutDuplicateSubscriptionContents < ActiveRecord::Migration[5.1] 2 | def up 3 | all_ids = SubscriptionContent.pluck(:id) 4 | 5 | ids_to_keep = SubscriptionContent 6 | .group(:content_change_id, :subscription_id) 7 | .pluck("MIN(id)") 8 | 9 | ids_to_delete = all_ids - ids_to_keep 10 | 11 | deleted_count = SubscriptionContent.where(id: ids_to_delete).delete_all 12 | 13 | puts "deleted #{deleted_count} rows" 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /db/migrate/20180315080842_add_subscriber_id_to_emails.rb: -------------------------------------------------------------------------------- 1 | class AddSubscriberIdToEmails < ActiveRecord::Migration[5.1] 2 | def up 3 | add_reference :emails, :subscriber, index: false 4 | 5 | execute %( 6 | ALTER TABLE "emails" 7 | ADD CONSTRAINT emails_subscriber_id_fk 8 | FOREIGN KEY ("subscriber_id") REFERENCES "subscribers" ("id") 9 | ON DELETE CASCADE NOT VALID 10 | ) 11 | end 12 | 13 | def down 14 | remove_reference :emails, :subscriber 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /db/migrate/20180315084923_add_unique_index_on_subscription_content.rb: -------------------------------------------------------------------------------- 1 | class AddUniqueIndexOnSubscriptionContent < ActiveRecord::Migration[5.1] 2 | disable_ddl_transaction! 3 | 4 | def change 5 | add_index :subscription_contents, 6 | %w(subscription_id content_change_id), 7 | name: "index_subscription_contents_on_subscription_and_content_change", 8 | unique: true, 9 | algorithm: :concurrently 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20180316115057_add_status_and_failure_reason_to_emails.rb: -------------------------------------------------------------------------------- 1 | class AddStatusAndFailureReasonToEmails < ActiveRecord::Migration[5.1] 2 | def change 3 | add_column :emails, :status, :integer 4 | add_column :emails, :failure_reason, :integer 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20180316120328_add_status_and_failure_reasons_indexes_to_emails.rb: -------------------------------------------------------------------------------- 1 | class AddStatusAndFailureReasonsIndexesToEmails < ActiveRecord::Migration[5.1] 2 | disable_ddl_transaction! 3 | 4 | def change 5 | add_index :emails, :status, algorithm: :concurrently 6 | add_index :emails, :failure_reason, algorithm: :concurrently 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20180316221116_set_email_statuses.rb: -------------------------------------------------------------------------------- 1 | class SetEmailStatuses < ActiveRecord::Migration[5.1] 2 | disable_ddl_transaction! 3 | 4 | def up 5 | DeliveryAttempt 6 | .where(status: :delivered) 7 | .in_batches(of: 10_000) 8 | .each_with_index do |batch, index| 9 | puts "Processed #{10_000 * index} sent emails" 10 | Email.where(id: batch.pluck(:email_id)).update_all(status: :sent) 11 | sleep(0.1) 12 | end 13 | 14 | DeliveryAttempt 15 | .where(status: :permanent_failure) 16 | .in_batches(of: 10_000) 17 | .each_with_index do |batch, index| 18 | puts "Processed #{10_000 * index} failed emails" 19 | Email.where(id: batch.pluck(:email_id)).update_all(status: :failed, failure_reason: :permanent_failure) 20 | sleep(0.1) 21 | end 22 | 23 | pending = Email.where(status: nil).update_all(status: :pending) 24 | puts "#{pending} emails marked as pending" 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /db/migrate/20180316223209_change_email_status_default.rb: -------------------------------------------------------------------------------- 1 | class ChangeEmailStatusDefault < ActiveRecord::Migration[5.1] 2 | def up 3 | change_column :emails, :status, :integer, default: 0, null: false 4 | end 5 | 6 | def down 7 | change_column :emails, :status, :integer, default: nil, null: true 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20180321101329_index_created_updated_uuid_tables.rb: -------------------------------------------------------------------------------- 1 | class IndexCreatedUpdatedUuidTables < ActiveRecord::Migration[5.1] 2 | disable_ddl_transaction! 3 | 4 | def change 5 | %i[emails delivery_attempts subscriptions content_changes].each do |table| 6 | add_index table, :created_at, algorithm: :concurrently 7 | add_index table, :updated_at, algorithm: :concurrently 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20180321103043_add_address_index_to_emails.rb: -------------------------------------------------------------------------------- 1 | class AddAddressIndexToEmails < ActiveRecord::Migration[5.1] 2 | disable_ddl_transaction! 3 | 4 | def change 5 | add_index :emails, :address, algorithm: :concurrently 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20180321104249_remove_status_and_failure_reason_indexes.rb: -------------------------------------------------------------------------------- 1 | class RemoveStatusAndFailureReasonIndexes < ActiveRecord::Migration[5.1] 2 | disable_ddl_transaction! 3 | 4 | def change 5 | remove_index :emails, column: :status, algorithm: :concurrently 6 | remove_index :emails, column: :failure_reason, algorithm: :concurrently 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20180321125113_add_email_status_indexes.rb: -------------------------------------------------------------------------------- 1 | class AddEmailStatusIndexes < ActiveRecord::Migration[5.1] 2 | disable_ddl_transaction! 3 | 4 | def change 5 | add_index :emails, %i[status archived_at], algorithm: :concurrently 6 | add_index :emails, :status, algorithm: :concurrently 7 | add_index :emails, :failure_reason, algorithm: :concurrently 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20180501155601_update_land_registry_titles.rb: -------------------------------------------------------------------------------- 1 | class UpdateLandRegistryTitles < ActiveRecord::Migration[5.2] 2 | def change 3 | list = SubscriberList.where("title LIKE '%Land Registry%' AND title NOT LIKE '%HM Land Registry%'") 4 | 5 | list.each do |item| 6 | item.title.sub! "Land Registry", "HM Land Registry" 7 | item.save! 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20180618132321_remove_title_index_from_subscriber_list.rb: -------------------------------------------------------------------------------- 1 | class RemoveTitleIndexFromSubscriberList < ActiveRecord::Migration[5.2] 2 | def change 3 | remove_index "subscriber_lists", name: "index_subscriber_lists_on_title" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20180625135732_add_index_to_subscription_content.rb: -------------------------------------------------------------------------------- 1 | class AddIndexToSubscriptionContent < ActiveRecord::Migration[5.2] 2 | disable_ddl_transaction! 3 | 4 | def change 5 | add_index :subscription_contents, :created_at, algorithm: :concurrently 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20180625143326_disable_subscriptions_with_inactive_subscribers.rb: -------------------------------------------------------------------------------- 1 | class DisableSubscriptionsWithInactiveSubscribers < ActiveRecord::Migration[5.2] 2 | def up 3 | subscriptions = Subscription.active.where(subscriber: Subscriber.deactivated) 4 | subscriptions.find_each do |subscription| 5 | subscription.end(reason: :unsubscribed) 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20180628071838_add_index_to_content_change_processed_at.rb: -------------------------------------------------------------------------------- 1 | class AddIndexToContentChangeProcessedAt < ActiveRecord::Migration[5.2] 2 | disable_ddl_transaction! 3 | 4 | def change 5 | add_index :content_changes, :processed_at, algorithm: :concurrently 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20180628072213_add_index_to_digest_run_completed_at.rb: -------------------------------------------------------------------------------- 1 | class AddIndexToDigestRunCompletedAt < ActiveRecord::Migration[5.2] 2 | disable_ddl_transaction! 3 | 4 | def change 5 | add_index :digest_runs, :completed_at, algorithm: :concurrently 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20180628072318_add_index_to_digest_run_created_at.rb: -------------------------------------------------------------------------------- 1 | class AddIndexToDigestRunCreatedAt < ActiveRecord::Migration[5.2] 2 | disable_ddl_transaction! 3 | 4 | def change 5 | add_index :digest_runs, :created_at, algorithm: :concurrently 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20180628134912_add_exported_at_to_email_archives.rb: -------------------------------------------------------------------------------- 1 | class AddExportedAtToEmailArchives < ActiveRecord::Migration[5.2] 2 | def change 3 | add_column :email_archives, :exported_at, :datetime 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20180628135936_add_indexes_to_email_archive.rb: -------------------------------------------------------------------------------- 1 | class AddIndexesToEmailArchive < ActiveRecord::Migration[5.2] 2 | disable_ddl_transaction! 3 | def change 4 | add_index :email_archives, :finished_sending_at, algorithm: :concurrently 5 | add_index :email_archives, :exported_at, algorithm: :concurrently 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20180705095148_remove_email_archive_table.rb: -------------------------------------------------------------------------------- 1 | class RemoveEmailArchiveTable < ActiveRecord::Migration[5.2] 2 | def up 3 | drop_table :email_archives 4 | end 5 | 6 | def down 7 | raise ActiveRecord::IrreversibleMigration 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20180723091112_add_marked_as_spam_to_email.rb: -------------------------------------------------------------------------------- 1 | class AddMarkedAsSpamToEmail < ActiveRecord::Migration[5.2] 2 | def change 3 | add_column :emails, :marked_as_spam, :boolean, default: nil 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20180727115914_drop_unused_indexes.rb: -------------------------------------------------------------------------------- 1 | class DropUnusedIndexes < ActiveRecord::Migration[5.2] 2 | def up 3 | remove_index :content_changes, name: :index_content_changes_on_updated_at 4 | remove_index :delivery_attempts, name: :index_delivery_attempts_on_updated_at 5 | remove_index :emails, name: :index_emails_on_status 6 | remove_index :emails, name: :index_emails_on_status_and_archived_at 7 | remove_index :emails, name: :index_emails_on_updated_at 8 | remove_index :subscription_contents, name: :index_subscription_contents_on_created_at if index_exists?(:subscription_contents, :index_subscription_contents_on_created_at) 9 | remove_index :subscriptions, name: :index_subscriptions_on_updated_at 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20180828153412_add_missing_policies_links_to_policy_subscriber_lists.rb: -------------------------------------------------------------------------------- 1 | class AddMissingPoliciesLinksToPolicySubscriberLists < ActiveRecord::Migration[5.2] 2 | def change 3 | SubscriberList.all.each do |subscriber_list| 4 | next unless subscriber_list.tags.key? :policies 5 | 6 | if subscriber_list.links.empty? 7 | policy_slug = subscriber_list.tags[:policies].first 8 | content_id = Services 9 | .content_store 10 | .content_item("/government/policies/#{policy_slug}") 11 | .to_h 12 | .fetch("content_id") 13 | 14 | subscriber_list.update!(links: { "policies" => [content_id] }) 15 | 16 | puts "updated: #{policy_slug} with content_id #{content_id}" 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /db/migrate/20180910095917_add_ended_email_id_to_subscriptions.rb: -------------------------------------------------------------------------------- 1 | class AddEndedEmailIdToSubscriptions < ActiveRecord::Migration[5.2] 2 | def change 3 | add_column :subscriptions, :ended_email_id, :uuid, null: true 4 | add_foreign_key( 5 | :subscriptions, 6 | :emails, 7 | column: :ended_email_id, 8 | ) 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20180917150259_remove_ended_email_id_foreign_key_from_subscriptions.rb: -------------------------------------------------------------------------------- 1 | class RemoveEndedEmailIdForeignKeyFromSubscriptions < ActiveRecord::Migration[5.2] 2 | def change 3 | remove_foreign_key :subscriptions, :emails 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20181119131532_update_slug_and_title_limit.rb: -------------------------------------------------------------------------------- 1 | class UpdateSlugAndTitleLimit < ActiveRecord::Migration[5.2] 2 | def change 3 | change_column :subscriber_lists, :title, :string, limit: 10000 4 | change_column :subscriber_lists, :slug, :string, limit: 10000 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20190125145045_add_content_purpose_supergroup_to_subscriber_list.rb: -------------------------------------------------------------------------------- 1 | class AddContentPurposeSupergroupToSubscriberList < ActiveRecord::Migration[5.2] 2 | def change 3 | add_column :subscriber_lists, :content_purpose_supergroup, :string, limit: 100 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20190206130316_add_reject_content_purpose_supergroup_to_subscriber_list.rb: -------------------------------------------------------------------------------- 1 | class AddRejectContentPurposeSupergroupToSubscriberList < ActiveRecord::Migration[5.2] 2 | def change 3 | add_column :subscriber_lists, :reject_content_purpose_supergroup, :string, limit: 100 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20190313155146_remove_reject_content_purpose_supergroup_from_subscriber_list.rb: -------------------------------------------------------------------------------- 1 | class RemoveRejectContentPurposeSupergroupFromSubscriberList < ActiveRecord::Migration[5.2] 2 | def change 3 | remove_column :subscriber_lists, :reject_content_purpose_supergroup, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20190412121520_add_facet_group_link_to_eu_exit_subscriber_lists.rb: -------------------------------------------------------------------------------- 1 | class AddFacetGroupLinkToEuExitSubscriberLists < ActiveRecord::Migration[5.2] 2 | def change 3 | SubscriberList.where("slug LIKE 'find-eu-exit-guidance-for-your-business%'").each do |list| 4 | list.links = list.links.merge(facet_groups: { any: %W(52435175-82ed-4a04-adef-74c0199d0f46) }) 5 | list.save! 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20190503114015_remove_content_purpose_supergroup_from_subscriber_list.rb: -------------------------------------------------------------------------------- 1 | class RemoveContentPurposeSupergroupFromSubscriberList < ActiveRecord::Migration[5.2] 2 | def change 3 | remove_column :subscriber_lists, :content_purpose_supergroup, :string, limit: 100 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20190522114020_add_type_to_subscriber_list.rb: -------------------------------------------------------------------------------- 1 | class AddTypeToSubscriberList < ActiveRecord::Migration[5.2] 2 | def change 3 | add_column :subscriber_lists, :type, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20190618111941_remove_type_from_subscriber_list.rb: -------------------------------------------------------------------------------- 1 | class RemoveTypeFromSubscriberList < ActiveRecord::Migration[5.2] 2 | def change 3 | remove_column :subscriber_lists, :type, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20190717121233_add_index_to_subscription_ended_at.rb: -------------------------------------------------------------------------------- 1 | class AddIndexToSubscriptionEndedAt < ActiveRecord::Migration[5.2] 2 | disable_ddl_transaction! 3 | 4 | def change 5 | add_index :subscriptions, :ended_at, algorithm: :concurrently 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20190814163542_create_messages.rb: -------------------------------------------------------------------------------- 1 | class CreateMessages < ActiveRecord::Migration[5.2] 2 | def change 3 | create_table :messages, id: :uuid, default: "uuid_generate_v4()" do |t| 4 | t.text :sender_message_id 5 | t.text :title, null: false 6 | t.text :url 7 | t.text :body, null: false 8 | t.json :links, default: {}, null: false 9 | t.json :tags, default: {}, null: false 10 | t.string :document_type 11 | t.string :email_document_supertype 12 | t.string :government_document_supertype 13 | t.datetime :processed_at 14 | t.string :signon_user_uid 15 | t.string :govuk_request_id, null: false 16 | t.integer :priority, default: 0, null: false 17 | t.timestamps 18 | 19 | t.index :sender_message_id, unique: true 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /db/migrate/20190815150119_create_matched_messages.rb: -------------------------------------------------------------------------------- 1 | class CreateMatchedMessages < ActiveRecord::Migration[5.2] 2 | def change 3 | create_table :matched_messages do |t| 4 | t.references :message, null: false, type: :uuid, foreign_key: { on_delete: :cascade } 5 | t.references :subscriber_list, null: false, foreign_key: { on_delete: :cascade } 6 | t.timestamps 7 | 8 | t.index %i(message_id subscriber_list_id), unique: true 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20190815191552_add_message_to_subscription_contents.rb: -------------------------------------------------------------------------------- 1 | class AddMessageToSubscriptionContents < ActiveRecord::Migration[5.2] 2 | def change 3 | add_reference :subscription_contents, 4 | :message, 5 | type: :uuid, 6 | foreign_key: { on_delete: :restrict, validate: false }, 7 | index: false 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20190815191553_validate_message_to_subscription_contents.rb: -------------------------------------------------------------------------------- 1 | class ValidateMessageToSubscriptionContents < ActiveRecord::Migration[5.2] 2 | def changes 3 | validate_foreign_key :subscription_contents, :messages 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20190815192452_subscription_contents_accept_nil_content_changes.rb: -------------------------------------------------------------------------------- 1 | class SubscriptionContentsAcceptNilContentChanges < ActiveRecord::Migration[5.2] 2 | def change 3 | change_column_null :subscription_contents, :content_change_id, true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20190815192913_unique_index_on_subscription_contents_messages.rb: -------------------------------------------------------------------------------- 1 | class UniqueIndexOnSubscriptionContentsMessages < ActiveRecord::Migration[5.2] 2 | disable_ddl_transaction! 3 | 4 | def change 5 | add_index :subscription_contents, 6 | %i(subscription_id message_id), 7 | unique: true, 8 | algorithm: :concurrently 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20190823114916_add_url_to_subscriber_lists.rb: -------------------------------------------------------------------------------- 1 | class AddUrlToSubscriberLists < ActiveRecord::Migration[5.2] 2 | def change 3 | add_column :subscriber_lists, :url, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20190829070710_add_description_to_subscriber_lists.rb: -------------------------------------------------------------------------------- 1 | class AddDescriptionToSubscriberLists < ActiveRecord::Migration[5.2] 2 | def change 3 | add_column :subscriber_lists, :description, :string, null: false, default: "" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20190902130018_add_tags_links_digest_to_subscriber_lists.rb: -------------------------------------------------------------------------------- 1 | class AddTagsLinksDigestToSubscriberLists < ActiveRecord::Migration[5.2] 2 | def change 3 | add_column :subscriber_lists, :tags_digest, :string 4 | add_column :subscriber_lists, :links_digest, :string 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20190902133711_add_tags_links_digest_to_existing_subscriber_lists.rb: -------------------------------------------------------------------------------- 1 | class AddTagsLinksDigestToExistingSubscriberLists < ActiveRecord::Migration[5.2] 2 | disable_ddl_transaction! 3 | 4 | def change 5 | SubscriberList.where("tags_digest IS NULL AND links_digest IS NULL").find_each do |list| 6 | list.tags_digest = HashDigest.new(list.tags).generate 7 | list.links_digest = HashDigest.new(list.links).generate 8 | list.save! 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20190903101929_add_criteria_rules_to_messages.rb: -------------------------------------------------------------------------------- 1 | class AddCriteriaRulesToMessages < ActiveRecord::Migration[5.2] 2 | def change 3 | add_column :messages, :criteria_rules, :json 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20190904182908_remove_superfluous_message_fields.rb: -------------------------------------------------------------------------------- 1 | class RemoveSuperfluousMessageFields < ActiveRecord::Migration[5.2] 2 | def change 3 | change_table :messages, bulk: true do |t| 4 | t.remove :links, 5 | :tags, 6 | :document_type, 7 | :email_document_supertype, 8 | :government_document_supertype 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20190905162914_add_indexes_to_subscriber_list_digests.rb: -------------------------------------------------------------------------------- 1 | class AddIndexesToSubscriberListDigests < ActiveRecord::Migration[5.2] 2 | disable_ddl_transaction! 3 | 4 | def change 5 | add_index :subscriber_lists, 6 | :tags_digest, 7 | algorithm: :concurrently 8 | add_index :subscriber_lists, 9 | :links_digest, 10 | algorithm: :concurrently 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/20190911112938_reduce_slug_lengths.rb: -------------------------------------------------------------------------------- 1 | class ReduceSlugLengths < ActiveRecord::Migration[5.2] 2 | disable_ddl_transaction! 3 | 4 | def up 5 | SubscriberList.where("LENGTH(slug) > 255").find_each do |list| 6 | list.update!(slug: slugify(list.title)) 7 | end 8 | end 9 | 10 | def down 11 | raise ActiveRecord::IrreversibleMigration 12 | end 13 | 14 | def slugify(title) 15 | slug = title.parameterize.truncate(255, omission: "", separator: "-") 16 | 17 | while SubscriberList.where(slug: slug).exists? 18 | slug = title.parameterize.truncate(244, omission: "", separator: "-") 19 | slug += "-#{SecureRandom.hex(5)}" 20 | end 21 | 22 | slug 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /db/migrate/20190911115859_remove_limits_on_subscriber_lists.rb: -------------------------------------------------------------------------------- 1 | class RemoveLimitsOnSubscriberLists < ActiveRecord::Migration[5.2] 2 | def change 3 | change_column :subscriber_lists, :title, :string, limit: nil 4 | change_column :subscriber_lists, :slug, :string, limit: nil 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20190912161623_amend_brexit_result_descriptions.rb: -------------------------------------------------------------------------------- 1 | class AmendBrexitResultDescriptions < ActiveRecord::Migration[5.2] 2 | def up 3 | SubscriberList.connection.execute(" 4 | UPDATE subscriber_lists 5 | SET description = REGEXP_REPLACE( 6 | description, 7 | '\\[.*\\]\\((.*)\\).*', 8 | '[You can view a copy of your results on GOV.UK.](\\1)' 9 | ) 10 | WHERE description like '%You can view a copy of your Brexit tool results%' 11 | ") 12 | end 13 | 14 | def down 15 | raise ActiveRecord::IrreversibleMigration 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /db/migrate/20190913102634_add_group_id_to_subscriber_lists.rb: -------------------------------------------------------------------------------- 1 | class AddGroupIdToSubscriberLists < ActiveRecord::Migration[5.2] 2 | def change 3 | add_column :subscriber_lists, :group_id, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20191011142014_add_partial_index_on_subscription_contents.rb: -------------------------------------------------------------------------------- 1 | class AddPartialIndexOnSubscriptionContents < ActiveRecord::Migration[5.2] 2 | def change 3 | add_index( 4 | :subscription_contents, 5 | :subscription_id, 6 | where: "email_id IS NULL", 7 | name: "partial_index_subscription_contents_on_subscription_id", 8 | ) 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20191014131300_remove_partial_index_on_subscription_contents.rb: -------------------------------------------------------------------------------- 1 | class RemovePartialIndexOnSubscriptionContents < ActiveRecord::Migration[5.2] 2 | def change 3 | remove_index( 4 | :subscription_contents, 5 | name: "partial_index_subscription_contents_on_subscription_id", 6 | ) 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20191016072945_add_descriptions_to_travel_advice.rb: -------------------------------------------------------------------------------- 1 | class AddDescriptionsToTravelAdvice < ActiveRecord::Migration[5.2] 2 | COUNTRIES = %w( 3 | austria 4 | belgium 5 | bulgaria 6 | croatia 7 | cyprus 8 | czech-republic 9 | denmark 10 | estonia 11 | finland 12 | france 13 | germany 14 | greece 15 | hungary 16 | iceland 17 | ireland 18 | italy 19 | latvia 20 | liechtenstein 21 | lithuania 22 | luxembourg 23 | malta 24 | netherlands 25 | norway 26 | poland 27 | portugal 28 | romania 29 | slovakia 30 | slovenia 31 | spain 32 | sweden 33 | switzerland 34 | ).freeze 35 | 36 | DESCRIPTION = "Find out about the changes to [travelling to Europe after Brexit](https://www.gov.uk/visit-europe-brexit).".freeze 37 | 38 | def slugs 39 | COUNTRIES.map { |country| "#{country}-travel-advice" } 40 | end 41 | 42 | def up 43 | updated_count = SubscriberList.where(slug: slugs) 44 | .update_all(description: DESCRIPTION) 45 | raise "Not all travel advice updated." unless updated_count == COUNTRIES.count 46 | end 47 | 48 | def down 49 | SubscriberList.where(slug: slugs).update_all(description: "") 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /db/migrate/20200123161117_update_brexit_subscriber_list_to_transition.rb: -------------------------------------------------------------------------------- 1 | class UpdateBrexitSubscriberListToTransition < ActiveRecord::Migration[5.2] 2 | def up 3 | subscriber_list = SubscriberList.find_by_slug("brexit-p") 4 | return if subscriber_list.nil? 5 | 6 | subscriber_list.slug = "transition-p" 7 | subscriber_list.title = "Transition" 8 | subscriber_list.save! 9 | end 10 | 11 | def down 12 | subscriber_list = SubscriberList.find_by_slug("transition-p") 13 | return if subscriber_list.nil? 14 | 15 | subscriber_list.slug = "brexit-p" 16 | subscriber_list.title = "Brexit" 17 | subscriber_list.save! 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /db/migrate/20200203103706_update_brexit_checker_titles.rb: -------------------------------------------------------------------------------- 1 | class UpdateBrexitCheckerTitles < ActiveRecord::Migration[5.2] 2 | OLD_TITLE = "How to prepare for a no deal Brexit".freeze 3 | NEW_TITLE = "Get ready for 2021".freeze 4 | 5 | def up 6 | SubscriberList.where(title: OLD_TITLE).update_all(title: NEW_TITLE) 7 | end 8 | 9 | def down 10 | SubscriberList.where(title: NEW_TITLE).update_all(title: OLD_TITLE) 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /db/migrate/20200212134444_remove_descriptions_from_travel_advice.rb: -------------------------------------------------------------------------------- 1 | class RemoveDescriptionsFromTravelAdvice < ActiveRecord::Migration[5.2] 2 | DESCRIPTION = "Find out about the changes to [travelling to Europe after Brexit](https://www.gov.uk/visit-europe-brexit).".freeze 3 | 4 | def change 5 | SubscriberList.where(description: DESCRIPTION).update_all(description: "") 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20200310111954_rename_brexit_to_transition.rb: -------------------------------------------------------------------------------- 1 | class RenameBrexitToTransition < ActiveRecord::Migration[6.0] 2 | def up 3 | lists = SubscriberList.where("title like ?", "%Brexit%") 4 | lists.each do |list| 5 | list.title.gsub!("Brexit", "Transition") 6 | list.save! 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20200406094740_rename_coronavirus_topical_event_sub.rb: -------------------------------------------------------------------------------- 1 | class RenameCoronavirusTopicalEventSub < ActiveRecord::Migration[6.0] 2 | NEW_TITLE = "Coronavirus (COVID-19)".freeze 3 | 4 | def up 5 | lists = SubscriberList.where("title like ?", "%Coronavirus (COVID-19): UK government response%") 6 | lists.each do |list| 7 | list.title = NEW_TITLE 8 | list.save! 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20200730170816_drop_failure_reason_from_emails.rb: -------------------------------------------------------------------------------- 1 | class DropFailureReasonFromEmails < ActiveRecord::Migration[6.0] 2 | def change 3 | remove_index :emails, :failure_reason 4 | remove_column :emails, :failure_reason, :integer 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20200730172117_migrate_delivery_attempt_statuses.rb: -------------------------------------------------------------------------------- 1 | class MigrateDeliveryAttemptStatuses < ActiveRecord::Migration[6.0] 2 | class DeliveryAttempt < ApplicationRecord; end 3 | 4 | def up 5 | # we've changed from an enum of: 6 | # enum status: { sending: 0, delivered: 1, permanent_failure: 2, temporary_failure: 3, technical_failure: 4, internal_failure: 5 } 7 | # 8 | # to: 9 | # enum status: { sending: 0, delivered: 1, undeliverable_failure: 3, provider_communication_failure: 4 } 10 | # 11 | # This updates all records that fall outside these parameters 12 | DeliveryAttempt.where(status: 2).update_all(status: 3) 13 | DeliveryAttempt.where(status: 5).update_all(status: 4) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /db/migrate/20200804143030_remove_marked_as_spam_from_email.rb: -------------------------------------------------------------------------------- 1 | class RemoveMarkedAsSpamFromEmail < ActiveRecord::Migration[6.0] 2 | def change 3 | remove_column :emails, :marked_as_spam, :boolean 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20200810121128_remove_subscriber_count_from_digest_run.rb: -------------------------------------------------------------------------------- 1 | class RemoveSubscriberCountFromDigestRun < ActiveRecord::Migration[6.0] 2 | def up 3 | remove_column :digest_runs, :subscriber_count 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20200817151547_add_processed_at_to_digest_runs.rb: -------------------------------------------------------------------------------- 1 | class AddProcessedAtToDigestRuns < ActiveRecord::Migration[6.0] 2 | def change 3 | add_column :digest_runs, :processed_at, :datetime 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20200818150135_unique_index_on_digest_run_subscribers.rb: -------------------------------------------------------------------------------- 1 | class UniqueIndexOnDigestRunSubscribers < ActiveRecord::Migration[6.0] 2 | disable_ddl_transaction! 3 | 4 | def change 5 | add_index :digest_run_subscribers, 6 | %i[digest_run_id subscriber_id], 7 | unique: true, 8 | algorithm: :concurrently 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /db/migrate/20200818172715_rename_completed_at_digest_run_subscriber.rb: -------------------------------------------------------------------------------- 1 | class RenameCompletedAtDigestRunSubscriber < ActiveRecord::Migration[6.0] 2 | def change 3 | rename_column :digest_run_subscribers, :completed_at, :processed_at 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20200903094629_add_sent_at_to_emails.rb: -------------------------------------------------------------------------------- 1 | class AddSentAtToEmails < ActiveRecord::Migration[6.0] 2 | def change 3 | add_column :emails, :sent_at, :datetime 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20200916095316_add_unique_index_for_digest_runs.rb: -------------------------------------------------------------------------------- 1 | class AddUniqueIndexForDigestRuns < ActiveRecord::Migration[6.0] 2 | disable_ddl_transaction! 3 | 4 | def change 5 | add_index :digest_runs, %i[date range], unique: true, algorithm: :concurrently 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20200916163844_remove_sent_at_and_completed_at_from_delivery_attempts.rb: -------------------------------------------------------------------------------- 1 | class RemoveSentAtAndCompletedAtFromDeliveryAttempts < ActiveRecord::Migration[6.0] 2 | def up 3 | change_table :delivery_attempts, bulk: true do |t| 4 | t.remove :sent_at, :completed_at 5 | end 6 | end 7 | 8 | def down 9 | change_table :delivery_attempts, bulk: true do |t| 10 | t.column :sent_at, :datetime 11 | t.column :completed_at, :datetime 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /db/migrate/20200916164443_remove_finished_sending_at_from_emails.rb: -------------------------------------------------------------------------------- 1 | class RemoveFinishedSendingAtFromEmails < ActiveRecord::Migration[6.0] 2 | def change 3 | remove_index :emails, :finished_sending_at 4 | remove_column :emails, :finished_sending_at, :datetime 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20201016164726_add_foreign_key_indexes_for_subscriber_id.rb: -------------------------------------------------------------------------------- 1 | class AddForeignKeyIndexesForSubscriberId < ActiveRecord::Migration[6.0] 2 | disable_ddl_transaction! 3 | 4 | def change 5 | add_index :emails, :subscriber_id, algorithm: :concurrently 6 | add_index :digest_run_subscribers, :subscriber_id, algorithm: :concurrently 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20201019110929_add_index_for_digest_run_id_on_digest_run_subscribers.rb: -------------------------------------------------------------------------------- 1 | class AddIndexForDigestRunIdOnDigestRunSubscribers < ActiveRecord::Migration[6.0] 2 | disable_ddl_transaction! 3 | 4 | def change 5 | add_index :digest_run_subscribers, :digest_run_id, algorithm: :concurrently 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20201019112513_add_index_for_message_id_on_subscription_contents.rb: -------------------------------------------------------------------------------- 1 | class AddIndexForMessageIdOnSubscriptionContents < ActiveRecord::Migration[6.0] 2 | disable_ddl_transaction! 3 | 4 | def change 5 | add_index :subscription_contents, :message_id, algorithm: :concurrently 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20201019154701_delete_lingering_subscription_contents.rb: -------------------------------------------------------------------------------- 1 | class DeleteLingeringSubscriptionContents < ActiveRecord::Migration[6.0] 2 | class SubscriptionContent < ApplicationRecord; end 3 | 4 | def up 5 | SubscriptionContent.where("created_at < '2020-06-23'").delete_all 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20201020153450_change_foreign_key_constraint_for_emails.rb: -------------------------------------------------------------------------------- 1 | class ChangeForeignKeyConstraintForEmails < ActiveRecord::Migration[6.0] 2 | def change 3 | remove_foreign_key :emails, :subscribers 4 | add_foreign_key :emails, :subscribers, on_delete: :restrict 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20201021153802_drop_delivery_attempts_table.rb: -------------------------------------------------------------------------------- 1 | class DropDeliveryAttemptsTable < ActiveRecord::Migration[6.0] 2 | def up 3 | drop_table :delivery_attempts 4 | end 5 | 6 | def down 7 | raise ActiveRecord::IrreversibleMigration 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20201110163036_remove_deactivated_at_from_subscribers.rb: -------------------------------------------------------------------------------- 1 | class RemoveDeactivatedAtFromSubscribers < ActiveRecord::Migration[6.0] 2 | def change 3 | remove_column :subscribers, :deactivated_at, :datetime 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20201130145943_remove_email_archived_at.rb: -------------------------------------------------------------------------------- 1 | class RemoveEmailArchivedAt < ActiveRecord::Migration[6.0] 2 | def change 3 | remove_column :emails, :archived_at, :datetime 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20201217084600_remove_group_id.rb: -------------------------------------------------------------------------------- 1 | class RemoveGroupId < ActiveRecord::Migration[6.0] 2 | def change 3 | remove_column :subscriber_lists, :group_id, type: :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20201218095119_remove_description_field.rb: -------------------------------------------------------------------------------- 1 | class RemoveDescriptionField < ActiveRecord::Migration[6.0] 2 | def change 3 | remove_column :subscriber_lists, :description, type: :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20210104090701_remove_message_url.rb: -------------------------------------------------------------------------------- 1 | class RemoveMessageUrl < ActiveRecord::Migration[6.0] 2 | def change 3 | remove_column :messages, :url, :string 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20210309090225_update_brexit_data_subscriber_list_criteria.rb: -------------------------------------------------------------------------------- 1 | class UpdateBrexitDataSubscriberListCriteria < ActiveRecord::Migration[6.1] 2 | CSV_FILE = Rails.root.join("db/migrate/data/subscriber-list-criteria-2021-03-08.csv").freeze 3 | 4 | def up 5 | update_criteria("new_criteria") 6 | end 7 | 8 | def down 9 | update_criteria("matching_criteria") 10 | end 11 | 12 | private 13 | 14 | def update_criteria(criteria_field_name) 15 | CSV.foreach(CSV_FILE, headers: true) do |row| 16 | criteria = JSON.parse(row[criteria_field_name]).to_h 17 | tags = criteria["tags"] 18 | 19 | subscriber_list = SubscriberList.find_by(slug: row["slug"]) 20 | subscriber_list.update!(tags:) if subscriber_list 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /db/migrate/20210608161920_add_content_id_to_subscriber_lists.rb: -------------------------------------------------------------------------------- 1 | class AddContentIdToSubscriberLists < ActiveRecord::Migration[6.1] 2 | def change 3 | add_column :subscriber_lists, :content_id, :uuid 4 | add_index :subscriber_lists, :content_id 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20210629083707_add_govuk_account_id_to_subscribers.rb: -------------------------------------------------------------------------------- 1 | class AddGovukAccountIdToSubscribers < ActiveRecord::Migration[6.1] 2 | def change 3 | add_column :subscribers, :govuk_account_id, :string 4 | add_index :subscribers, :govuk_account_id 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20220105173124_use_text_for_long_subscriber_lists_columns.rb: -------------------------------------------------------------------------------- 1 | class UseTextForLongSubscriberListsColumns < ActiveRecord::Migration[6.1] 2 | def up 3 | change_table :subscriber_lists, bulk: true do |t| 4 | t.change :title, :text 5 | t.change :url, :text 6 | end 7 | end 8 | 9 | def down 10 | change_table :subscriber_lists, bulk: true do |t| 11 | t.change :title, :string 12 | t.change :url, :string 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /db/migrate/20220105174025_use_text_for_long_emails_columns.rb: -------------------------------------------------------------------------------- 1 | class UseTextForLongEmailsColumns < ActiveRecord::Migration[6.1] 2 | def up 3 | change_column :emails, :subject, :text 4 | end 5 | 6 | def down 7 | change_column :emails, :subject, :string 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20220117130900_add_omit_footer_unsubscribe_link_to_messages.rb: -------------------------------------------------------------------------------- 1 | class AddOmitFooterUnsubscribeLinkToMessages < ActiveRecord::Migration[6.1] 2 | def change 3 | add_column :messages, :omit_footer_unsubscribe_link, :boolean, default: false, null: false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20220119090527_add_override_subscription_frequency_to_immediate_to_message.rb: -------------------------------------------------------------------------------- 1 | class AddOverrideSubscriptionFrequencyToImmediateToMessage < ActiveRecord::Migration[6.1] 2 | def change 3 | add_column :messages, :override_subscription_frequency_to_immediate, :boolean, default: false, null: false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20220121161623_add_description_column_to_subscriber_lists.rb: -------------------------------------------------------------------------------- 1 | class AddDescriptionColumnToSubscriberLists < ActiveRecord::Migration[6.1] 2 | def change 3 | add_column :subscriber_lists, :description, :text 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20231102140714_add_content_id_to_email.rb: -------------------------------------------------------------------------------- 1 | class AddContentIdToEmail < ActiveRecord::Migration[7.1] 2 | def change 3 | add_column :emails, :content_id, :uuid 4 | add_index :emails, :content_id 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20231102150419_update_emails_add_notify_status_and_id_index.rb: -------------------------------------------------------------------------------- 1 | class UpdateEmailsAddNotifyStatusAndIdIndex < ActiveRecord::Migration[7.1] 2 | def change 3 | add_column :emails, :notify_status, :string 4 | add_index :emails, :notify_status 5 | add_index :emails, :id 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /db/migrate/20240411100041_add_last_audited_at_to_subscriber_lists.rb: -------------------------------------------------------------------------------- 1 | class AddLastAuditedAtToSubscriberLists < ActiveRecord::Migration[7.1] 2 | def change 3 | add_column :subscriber_lists, :last_audited_at, :datetime, default: nil, null: true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20240513131857_add_last_alerted_at.rb: -------------------------------------------------------------------------------- 1 | class AddLastAlertedAt < ActiveRecord::Migration[7.1] 2 | def change 3 | add_column :subscriber_lists, :last_alerted_at, :datetime, default: nil, null: true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20240522152228_add_subscription_id_to_email.rb: -------------------------------------------------------------------------------- 1 | class AddSubscriptionIdToEmail < ActiveRecord::Migration[7.1] 2 | def change 3 | add_column :emails, :subscription_id, :uuid, default: nil, null: true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/seeds.rb: -------------------------------------------------------------------------------- 1 | return if User.where(name: "Test user").present? 2 | 3 | gds_organisation_id = "af07d5a5-df63-4ddc-9383-6a666845ebe9" 4 | 5 | User.create!( 6 | name: "Test user", 7 | permissions: %w[signin status_updates internal_app], 8 | organisation_content_id: gds_organisation_id, 9 | ) 10 | -------------------------------------------------------------------------------- /docs/adr/adr-002/digests.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alphagov/email-alert-api/21a9b1a91e5a4c4fd5bb37e10eb05513f65fccfd/docs/adr/adr-002/digests.png -------------------------------------------------------------------------------- /docs/adr/adr-005-record-architecture-decisions.md: -------------------------------------------------------------------------------- 1 | # 5. Record architecture decisions 2 | 3 | Date: 2020-10-21 4 | 5 | ## Context 6 | 7 | While we have already recorded architectural decisions for this project, there is no guidance to follow for writing new ones. Recently we found that creating a new ADR was [hindered](https://github.com/alphagov/email-alert-api/pull/1441#discussion_r508729384) by wanting to be consistent with the structure of previous ADRs. 8 | 9 | ## Decision 10 | 11 | We will continue to use Architecture Decision Records, as described by Michael Nygard in this article: http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions. 12 | 13 | We will adopt a structure for future ADRs that matches this document - the exemplar in [our Rails app conventions](https://docs.publishing.service.gov.uk/manual/conventions-for-rails-applications.html#documenting-your-decisions). We will not change how the files are named, to avoid breaking links that were never expected to change. 14 | 15 | ## Status 16 | 17 | Accepted 18 | 19 | ## Consequences 20 | 21 | ADRs preceding this one will have an inconsistent format to the ones that follow this. 22 | -------------------------------------------------------------------------------- /docs/env-vars.md: -------------------------------------------------------------------------------- 1 | # ENV vars 2 | 3 | ### `GOVUK_NOTIFY_RECIPIENTS` 4 | 5 | This environment variable determines whether emails will be attempted to be 6 | sent to Notify. Emails that aren't send to Notify are written to a log file. 7 | This makes this setting useful in non-production environments where you may 8 | want to send no, or only a few emails, to Notify. 9 | 10 | When this is set to `*` all emails are sent to Notify, this is the expected 11 | configuration for a production environment. 12 | 13 | In other environments this can be set as a comma separated list of email 14 | addresses to specify the recipients who should have their emails sent to 15 | Notify. For example, `GOVUK_NOTIFY_RECIPIENTS=test-1@example.com,test-2@example.com`. 16 | Emails that are sent to other recipients will not be sent and will instead 17 | be written to the log file. 18 | 19 | If this environment variable is not set then no emails will be sent to Notify 20 | and all will be written to the log file. 21 | -------------------------------------------------------------------------------- /docs/load-test-email-alert-api.md: -------------------------------------------------------------------------------- 1 | # Load test Email Alert API 2 | 3 | You may wish to load test Email Alert API to get a realistic idea of how the 4 | system performs when it has a large quantity of emails to create and send. 5 | This can be useful to provide data on where the system may have performance 6 | bottlenecks. 7 | 8 | To perform a load test you will need: 9 | 10 | - A mechanism to artificially create a quantity of work for Email Alert API to 11 | do - we previously had a number of [rake tasks][] to allow this; 12 | - An approach to simulate the delay of an actual request to Notify - we 13 | previously used a `Kernel.sleep(0.1)` to apply this. 14 | 15 | When performing the test you should inform the `#govuk-2ndline` 16 | channel as they may see alerts during it. 17 | 18 | [rake tasks]: https://github.com/alphagov/email-alert-api/pull/1494 19 | [clear-queues]: https://github.com/alphagov/email-alert-api/blob/17d54964063256a6769189bc5fd6d4cf61a9d40f/lib/tasks/load_testing.rake#L18-L21 20 | -------------------------------------------------------------------------------- /docs/sidekiq-web.md: -------------------------------------------------------------------------------- 1 | ## Viewing the Sidekiq UI for Email Alert API 2 | 3 | We have access to the Sidekiq UI but because Email Alert API doesn't have a 4 | frontend we have to use port forwarding to see it in our live environments. 5 | 6 | You'll need to have access to our EKS clusters before you can follow these 7 | instructions. There's [documentation here](https://docs.publishing.service.gov.uk/kubernetes/get-started/access-eks-cluster/#access-a-cluster-that-you-have-accessed-before) on how to do that. This means that 8 | you'll need full production access before you can view the Sidekiq UI. 9 | 10 | To view the UI run: 11 | 12 | ``` 13 | kubectl -n apps port-forward deployment/email-alert-api 8080:8080 14 | ``` 15 | 16 | and then navigate to localhost:8080/sidekiq 17 | -------------------------------------------------------------------------------- /lib/callable.rb: -------------------------------------------------------------------------------- 1 | # This mixin is to provide the boilerplate for classes that implement a single 2 | # public class method of `.call` - typically used by objects that are entirely 3 | # to perform a business transaction. 4 | module Callable 5 | extend ActiveSupport::Concern 6 | 7 | included do 8 | def self.call(...) 9 | new(...).call 10 | end 11 | 12 | private_class_method :new 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/hash_digest.rb: -------------------------------------------------------------------------------- 1 | require "digest" 2 | 3 | class HashDigest 4 | def initialize(hash) 5 | @original_hash = hash.deep_symbolize_keys 6 | end 7 | 8 | def generate 9 | return nil if original_hash.empty? 10 | 11 | Digest::SHA256.hexdigest(sort_hash(original_hash).to_s) 12 | end 13 | 14 | private 15 | 16 | attr_reader :original_hash 17 | 18 | # We sort the hash because the order of keys and values shouldn't matter. 19 | # You should get the same digest for a hash regardless of structure. 20 | def sort_hash(hash) 21 | value_sorted_hash = hash.transform_values do |value| 22 | case value 23 | when Hash 24 | sort_hash(value) 25 | when Array 26 | value.compact.sort 27 | else 28 | value 29 | end 30 | end 31 | 32 | value_sorted_hash.sort 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/notifications_from_notify.rb: -------------------------------------------------------------------------------- 1 | class NotificationsFromNotify 2 | def initialize 3 | @client = Notifications::Client.new(Rails.application.credentials.notify_api_key) 4 | end 5 | 6 | def self.call(*args) 7 | new.call(*args) 8 | end 9 | 10 | def call(reference) 11 | puts "Query Notify for emails with the reference #{reference}" 12 | 13 | response = client.get_notifications( 14 | template_type: "email", 15 | reference:, 16 | ) 17 | 18 | if response.is_a?(Notifications::Client::NotificationsCollection) 19 | if response.collection.count.zero? 20 | puts "No results found, empty collection returned" 21 | else 22 | response.collection.each do |notification| 23 | puts <<~TEXT 24 | ------------------------------------------- 25 | Notification ID: #{notification.id} 26 | Status: #{notification.status} 27 | created_at: #{notification.created_at} 28 | sent_at: #{notification.sent_at} 29 | completed_at: #{notification.completed_at} 30 | TEXT 31 | end 32 | end 33 | else 34 | puts "No results found" 35 | end 36 | rescue Notifications::Client::RequestError => e 37 | puts "Returns request error #{e.code}, message: #{e.message}" 38 | end 39 | 40 | private 41 | 42 | attr_reader :client 43 | end 44 | -------------------------------------------------------------------------------- /lib/reports/concerns/notification_stats.rb: -------------------------------------------------------------------------------- 1 | module Reports::Concerns::NotificationStats 2 | def list_names_array(lists) 3 | lists.map { |l| "#{l.title} (#{l.subscriptions.active.count} active subscribers)" } 4 | end 5 | 6 | def list_stats_array(lists) 7 | total_subs = lists.sum { |l| l.subscriptions.active.count } 8 | immediately_subs = lists.sum { |l| l.subscriptions.active.immediately.count } 9 | daily_subs = lists.sum { |l| l.subscriptions.active.daily.count } 10 | weekly_subs = lists.sum { |l| l.subscriptions.active.weekly.count } 11 | 12 | [ 13 | "notified immediately: #{immediately_subs}", 14 | "notified next day: #{daily_subs}", 15 | "notified at weekend: #{weekly_subs}", 16 | "notified total: #{total_subs}", 17 | ] 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/reports/finder_statistics_report.rb: -------------------------------------------------------------------------------- 1 | require "reports/concerns/notification_stats" 2 | 3 | class Reports::FinderStatisticsReport 4 | include Reports::Concerns::NotificationStats 5 | 6 | attr_reader :govuk_path 7 | 8 | def initialize(govuk_path) 9 | @govuk_path = govuk_path 10 | end 11 | 12 | def call 13 | lists = SubscriberListsForFinderQuery.new(govuk_path:).call 14 | 15 | output_string = "\nLists created from this finder\n" 16 | 17 | list_names_array(lists).each { |ln| output_string += " - #{ln}\n" } 18 | 19 | output_string += "\nResulting in:\n" 20 | 21 | list_stats_array(lists).each { |ls| output_string += " - #{ls}\n" } 22 | 23 | output_string 24 | rescue SubscriberListsForFinderQuery::NotAFinderError 25 | ["This item is not a finder, so isn't suitable for this report."] 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/reports/historical_content_change_statistics_report.rb: -------------------------------------------------------------------------------- 1 | require "reports/concerns/notification_stats" 2 | 3 | class Reports::HistoricalContentChangeStatisticsReport 4 | include Reports::Concerns::NotificationStats 5 | 6 | attr_reader :govuk_path 7 | 8 | def initialize(govuk_path) 9 | @govuk_path = govuk_path 10 | end 11 | 12 | def call 13 | content_changes = ContentChange.where(base_path: govuk_path).order(:public_updated_at) 14 | 15 | if content_changes.any? 16 | output_string = "#{content_changes.count} content changes registered for #{govuk_path}.\n\n" 17 | 18 | content_changes.each do |cc| 19 | output_string += "Content change on #{cc.public_updated_at}:\n" 20 | 21 | lists = SubscriberListQuery.new(content_id: cc.content_id, tags: cc.tags, links: cc.links, document_type: cc.document_type, email_document_supertype: cc.email_document_supertype, government_document_supertype: cc.government_document_supertype).lists 22 | 23 | list_stats_array(lists).each { |ls| output_string += " - #{ls}\n" } 24 | end 25 | 26 | output_string 27 | else 28 | "No content changes registered for path: #{govuk_path}" 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/reports/matched_content_changes_report.rb: -------------------------------------------------------------------------------- 1 | class Reports::MatchedContentChangesReport 2 | OUTPUT_ATTRIBUTES = { 3 | created_at: :content_change, 4 | base_path: :content_change, 5 | change_note: :content_change, 6 | document_type: :content_change, 7 | publishing_app: :content_change, 8 | priority: :content_change, 9 | title: :subscriber_list, 10 | slug: :subscriber_list, 11 | }.freeze 12 | 13 | def call(start_time: nil, end_time: nil) 14 | start_time = start_time ? Time.zone.parse(start_time) : 1.week.ago 15 | end_time = end_time ? Time.zone.parse(end_time) : Time.zone.now 16 | 17 | query = MatchedContentChange 18 | .includes(:subscriber_list, :content_change) # for efficient "row.content_change" 19 | .joins(:content_change) # for the next query 20 | .where("content_changes.created_at": start_time..end_time) 21 | .order("matched_content_changes.created_at desc") 22 | 23 | CSV.generate do |csv| 24 | csv << OUTPUT_ATTRIBUTES.keys 25 | 26 | query.each do |row| 27 | csv << OUTPUT_ATTRIBUTES.map do |sub_field, field| 28 | row.public_send(field).public_send(sub_field) 29 | end 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/reports/potentially_dead_lists_report.rb: -------------------------------------------------------------------------------- 1 | class Reports::PotentiallyDeadListsReport 2 | def call 3 | distinct_list_sql = Arel.sql("distinct subscriber_list_id") 4 | 5 | recent_subscriptions = Subscription 6 | .where("created_at > ?", 1.year.ago) 7 | .pluck(distinct_list_sql) 8 | 9 | all_changes = MatchedContentChange.pluck(distinct_list_sql) + 10 | MatchedMessage.pluck(distinct_list_sql) 11 | 12 | potentially_dead_slugs = SubscriberList 13 | .where(id: Subscription.active.pluck(distinct_list_sql)) 14 | .where.not(id: recent_subscriptions | all_changes) 15 | .pluck(:slug) 16 | 17 | Reports::SubscriberListsReport.new( 18 | Date.yesterday.to_s, slugs: potentially_dead_slugs.join(",") 19 | ).call 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/reports/subscriber_list_subscriber_count_report.rb: -------------------------------------------------------------------------------- 1 | class Reports::SubscriberListSubscriberCountReport 2 | class BadDateError < StandardError; end 3 | 4 | attr_reader :url, :active_on_date 5 | 6 | def initialize(url, active_on_date = nil) 7 | @url = url 8 | @active_on_date = active_on_date || Time.zone.now.end_of_day 9 | end 10 | 11 | def call 12 | list = SubscriberList.find_by_url(url) 13 | if list 14 | count = list.subscriptions 15 | .active_on(formatted_date) 16 | .count 17 | 18 | "Subscriber list for #{url} had #{count} subscribers on #{formatted_date}." 19 | else 20 | "Subscriber list cannot be found with URL: #{url}" 21 | end 22 | rescue ArgumentError 23 | "Cannot parse active_on_date, is this a valid ISO8601 date?: #{active_on_date}" 24 | end 25 | 26 | private 27 | 28 | def formatted_date 29 | return active_on_date if active_on_date.instance_of?(ActiveSupport::TimeWithZone) 30 | 31 | Time.zone.strptime(active_on_date, "%F").end_of_day 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/search_alert_list.rb: -------------------------------------------------------------------------------- 1 | module SearchAlertList 2 | MIN_AGE = 1.hour 3 | MAX_AGE = 2.days 4 | MAX_RESULTS = 50 5 | 6 | def get_alert_content_items(document_type:) 7 | results = GdsApi.search.search(count: MAX_RESULTS, fields: "content_id,link,public_timestamp", filter_format: document_type, order: "-public_timestamp") 8 | 9 | output = results["results"].map do |result| 10 | publish_time = Time.zone.parse(result["public_timestamp"]) 11 | if publish_time.between?(Time.zone.now - MAX_AGE, Time.zone.now - MIN_AGE) 12 | { content_id: result["content_id"], valid_from: publish_time, url: result["link"] } 13 | end 14 | end 15 | 16 | output.compact 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/services.rb: -------------------------------------------------------------------------------- 1 | module Services 2 | def self.rate_limiter 3 | @rate_limiter ||= Ratelimit.new("email-alert-api:deliveries") 4 | end 5 | 6 | def self.accounts_emails 7 | @accounts_emails ||= 8 | File.readlines(Rails.root.join("config/bulk_email/email_addresses.txt"), chomp: true) 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/symbolize_json.rb: -------------------------------------------------------------------------------- 1 | module SymbolizeJSON 2 | def self.included(model) 3 | model.columns.each do |column| 4 | next unless column.sql_type == "json" 5 | 6 | define_method(column.name) do 7 | SymbolizeJSON.symbolize(self[column.name]) 8 | end 9 | end 10 | end 11 | 12 | def self.symbolize(value) 13 | case value 14 | when Array 15 | value.map { |element| symbolize(element) } 16 | when Hash 17 | value.deep_symbolize_keys 18 | else 19 | value 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/tasks/alert_listeners.rake: -------------------------------------------------------------------------------- 1 | ALERT_SLUGS = %w[ 2 | medical-device-alerts-drug-alerts-field-safety-notice-national-patient-safety-alert-and-device-safety-information 3 | travel-advice-for-all-countries-travel-advice 4 | ].freeze 5 | 6 | namespace :alert_listeners do 7 | desc "Verify or create medical/travel alert listener" 8 | task verify_or_create: :environment do |_t, _args| 9 | address = ENV["ALERT_LISTENER_EMAIL_ACCOUNT"] 10 | abort("Can't create listener: ALERT_LISTENER_EMAIL_ACCOUNT env var missing!") unless address 11 | subscriber_lists = SubscriberList.where(slug: ALERT_SLUGS) 12 | abort("Can't create listener: one or more subscriber_lists missing") if subscriber_lists.count != ALERT_SLUGS.count 13 | 14 | puts("Checking that #{address} subscriber exists and is subscribed to Medical Alert and Travel Advice lists") 15 | subscriber = Subscriber.find_or_create_by!(address:) { puts("Subscriber record missing: created") } 16 | 17 | subscriber_lists.each do |subscriber_list| 18 | Subscription.find_or_create_by( 19 | subscriber:, 20 | subscriber_list:, 21 | frequency: "immediately", 22 | source: "support_task", 23 | ) { puts("Subscription record for #{subscriber_list.slug} missing: created") } 24 | end 25 | puts("Subscription ready") 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/tasks/lint.rake: -------------------------------------------------------------------------------- 1 | desc "Run RuboCop" 2 | task lint: :environment do 3 | sh "bundle exec rubocop --format clang" 4 | end 5 | -------------------------------------------------------------------------------- /log/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alphagov/email-alert-api/21a9b1a91e5a4c4fd5bb37e10eb05513f65fccfd/log/.gitkeep -------------------------------------------------------------------------------- /spec/README.md: -------------------------------------------------------------------------------- 1 | #Test folder layout 2 | 3 | Within spec, the folders that reflect the structure in `app` are used for unit 4 | tests (see definition below). In addition to these folders there is a `lib` 5 | folder for unit tests of code within the root `lib` directory. 6 | 7 | In addition to these folder there are `integration` and `feature` folders. 8 | See their README docs for definitions. 9 | 10 | ## Unit tests ## 11 | 12 | The purpose of these tests is to check that single units of the system do the 13 | right thing in isolation. This ensures individual units of the system behave 14 | correctly, before they are combined with other things. 15 | 16 | These tests should: 17 | 18 | - be fairly exhaustive and cover the majority of code paths 19 | 20 | These tests should not: 21 | 22 | - know intimate details about implementation details of the unit being tested 23 | 24 | It is ok for these tests to: 25 | 26 | - stub out methods not part of the unit being tested 27 | - assert that calls have been made to other units 28 | 29 | If you need to test multiple units work together, consider writing an 30 | integration test instead. See `spec/integration/README.md`. 31 | -------------------------------------------------------------------------------- /spec/builders/linked_account_email_builder_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe LinkedAccountEmailBuilder do 2 | describe ".call" do 3 | let(:subscriber) { build(:subscriber) } 4 | 5 | subject(:email) { described_class.call(subscriber:) } 6 | 7 | it "lists active subscriptions in the body" do 8 | active1 = create(:subscription, subscriber:) 9 | active2 = create(:subscription, subscriber:) 10 | 11 | expect(email.body).to include(active1.subscriber_list.title) 12 | expect(email.body).to include(active2.subscriber_list.title) 13 | end 14 | 15 | it "does not list ended subscriptions in the body" do 16 | inactive = create(:subscription, :ended, subscriber:) 17 | 18 | expect(email.body).not_to include(inactive.subscriber_list.title) 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/builders/subscriber_auth_email_builder_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe SubscriberAuthEmailBuilder do 2 | describe ".call" do 3 | let(:subscriber) { create(:subscriber) } 4 | 5 | subject(:email) do 6 | described_class.call( 7 | subscriber:, 8 | destination: "/destination", 9 | token: "secret", 10 | ) 11 | end 12 | 13 | before do 14 | allow(PublicUrls).to receive(:url_for) 15 | .with(base_path: "/destination", token: "secret") 16 | .and_return("auth_url") 17 | end 18 | 19 | it "creates an email" do 20 | expect(email.subject).to eq("Change your GOV.UK email preferences") 21 | expect(email.subscriber_id).to eq(subscriber.id) 22 | 23 | expect(email.body).to eq( 24 | <<~BODY, 25 | # Click the link to confirm your email address 26 | 27 | # [Yes, I want to change my GOV.UK email preferences](auth_url) 28 | 29 | This link will stop working after 7 days. 30 | 31 | If you did not request this email, you can ignore it. 32 | 33 | Thanks 34 | GOV.UK emails 35 | BODY 36 | ) 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /spec/features/README.md: -------------------------------------------------------------------------------- 1 | ## Feature tests ## 2 | 3 | The purpose of these tests is to check email-alert-api behaves correctly from 4 | the perspective of someone using the API. This ensures we're testing the system 5 | in a way representative of how someone will actually use it. 6 | 7 | These tests should: 8 | 9 | - interact with the api by calling its HTTP endpoints 10 | - use the responses from these endpoints to make assertions about behaviour 11 | - assert that calls have been made to external systems (e.g. Notify) 12 | 13 | These tests should not: 14 | 15 | - read/write directly from/to the database 16 | - know intimate details about implementation details of the app 17 | - e.g. stub out methods or assert that specific methods have been called 18 | 19 | If you need to test smaller boundaries of the system, consider writing an 20 | integration test instead. See `spec/integration/README.md`. 21 | -------------------------------------------------------------------------------- /spec/features/login_verify_email_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe "Login verify email", type: :request do 2 | include TokenHelpers 3 | 4 | let(:address) { "test@example.com" } 5 | let!(:subscriber) { create(:subscriber, address:) } 6 | let(:destination) { "/authenticate" } 7 | 8 | scenario "successful auth token" do 9 | login_with_internal_app 10 | 11 | post "/subscribers/auth-token", 12 | params: { 13 | address:, 14 | destination:, 15 | } 16 | 17 | email_data = expect_an_email_was_sent( 18 | address: "test@example.com", 19 | subject: "Change your GOV.UK email preferences", 20 | ) 21 | 22 | expect(response.status).to be 201 23 | 24 | body = email_data.dig(:personalisation, :body) 25 | expect(body).to include("http://www.dev.gov.uk#{destination}?token=") 26 | 27 | expect(decrypt_token_from_link(body)).to eq( 28 | "subscriber_id" => subscriber.id, 29 | ) 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/features/sending_email_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe "Sending an email", type: :request do 2 | scenario "sending an email for a content change" do 3 | login_with_internal_app 4 | 5 | subscriber_list = create_subscriber_list 6 | subscribe_to_subscriber_list(subscriber_list[:id]) 7 | create_content_change 8 | 9 | email_data = expect_an_email_was_sent( 10 | subject: "Update from GOV.UK for: Title", 11 | address: "test@test.com", 12 | ) 13 | 14 | body = email_data.dig(:personalisation, :body) 15 | expect(body).to include("Description") 16 | expect(body).to include("gov.uk/base-path") 17 | expect(body).to include("Change note") 18 | expect(body).to include("12:00am, 1 January 2017") 19 | expect(body).to include("[Unsubscribe](http://www.dev.gov.uk/email/unsubscribe") 20 | expect(body).to include("gov.uk/email/manage/authenticate") 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/features/status_update_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe "Status updates", type: :request do 2 | before do 3 | login_with(%w[internal_app status_updates]) 4 | subscriber_list = create_subscriber_list 5 | subscribe_to_subscriber_list(subscriber_list[:id]) 6 | create_content_change 7 | @email_data = expect_an_email_was_sent 8 | end 9 | 10 | scenario "successful delivery" do 11 | reference = @email_data.fetch(:reference) 12 | send_status_update(reference:, expected_status: 204) 13 | send_status_update(reference: nil, expected_status: 400) 14 | end 15 | 16 | scenario "permanent failure" do 17 | send_status_update(status: "permanent-failure", 18 | to: @email_data.fetch(:email_address), 19 | expected_status: 204) 20 | 21 | clear_any_requests_that_have_been_recorded! 22 | 3.times { create_content_change } 23 | expect_an_email_was_not_sent 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/integration/README.md: -------------------------------------------------------------------------------- 1 | ## Integration tests ## 2 | 3 | The purpose of these tests is to check that units of email-alert-api work 4 | together when combined into higher-level code. This ensures objects/classes 5 | integrate together correctly. 6 | 7 | These tests should: 8 | 9 | - exercise code of more than one object/class 10 | 11 | These tests should not: 12 | 13 | - exhaustively test all code paths (save that for unit tests) 14 | - know intimate details about implementation details of the components being tested 15 | - e.g. stub out methods or assert that specific methods have been called 16 | 17 | It is ok for these tests to: 18 | 19 | - stub out methods of components not part of the integration test 20 | - assert that calls have been made to components not part of the integration test 21 | 22 | If you need to test an entire journey through the system, consider writing a 23 | feature test instead. See `spec/features/README.md`. 24 | 25 | If you need to test a single component exhaustively, consider writing a unit 26 | test instead. See `spec/README.md`. 27 | -------------------------------------------------------------------------------- /spec/lib/metrics_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe Metrics do 2 | before do 3 | allow(GovukStatsd).to receive(:count) 4 | end 5 | 6 | describe ".content_change_emails" do 7 | it "sends stats for a batch of content change emails" do 8 | content_change = build(:content_change, publishing_app: "app", document_type: "type") 9 | expect(GovukStatsd).to receive(:count).with("content_change_emails.publishing_app.app.immediate", 1) 10 | expect(GovukStatsd).to receive(:count).with("content_change_emails.document_type.type.immediate", 1) 11 | described_class.content_change_emails(content_change, 1) 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /spec/lib/reports/finder_statistics_report_spec.rb: -------------------------------------------------------------------------------- 1 | require "gds_api/test_helpers/content_store" 2 | 3 | RSpec.describe Reports::FinderStatisticsReport do 4 | include GdsApi::TestHelpers::ContentStore 5 | 6 | let(:govuk_path) { "/cma-cases" } 7 | 8 | let(:expected) do 9 | <<~OUTPUT 10 | 11 | Lists created from this finder 12 | - Example List (0 active subscribers) 13 | 14 | Resulting in: 15 | - notified immediately: 0 16 | - notified next day: 0 17 | - notified at weekend: 0 18 | - notified total: 0 19 | OUTPUT 20 | end 21 | 22 | before do 23 | content_item = content_item_for_base_path(govuk_path).merge( 24 | "document_type" => "finder", 25 | "links" => { "email_alert_signup" => { "withdrawn" => false } }, 26 | "details" => { "filter" => { "format" => "cma_case" } }, 27 | ) 28 | stub_content_store_has_item(govuk_path, content_item) 29 | create(:subscriber_list, title: "Example List", tags: { format: { any: %w[cma_case] } }) 30 | end 31 | 32 | it "returns data around active lists for the given date" do 33 | expect(described_class.new(govuk_path).call).to eq expected 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /spec/lib/reports/matched_content_changes_report_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe Reports::MatchedContentChangesReport do 2 | describe "#call" do 3 | it "outputs a CSV of matched content changes" do 4 | subscriber_list = create :subscriber_list 5 | match = create(:matched_content_change, subscriber_list:) 6 | expect(described_class.new.call).to eq report_for([{ match: }]) 7 | end 8 | 9 | it "allows specifying the date range to report" do 10 | subscriber_list = create :subscriber_list 11 | create(:matched_content_change, subscriber_list:) 12 | output = described_class.new.call(start_time: 1.day.from_now.to_s, end_time: 2.days.from_now.to_s) 13 | expect(output).to eq report_for([]) 14 | end 15 | 16 | def report_for(rows) 17 | headers = described_class::OUTPUT_ATTRIBUTES.keys.join(",") 18 | 19 | rows = [headers] + rows.map do |row| 20 | "#{row[:match].content_change.created_at}," \ 21 | "government/base_path,change note,document type,publishing app,normal," \ 22 | "#{row[:match].subscriber_list.title}," \ 23 | "#{row[:match].subscriber_list.slug}" \ 24 | end 25 | 26 | "#{rows.join("\n")}\n" 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/lib/reports/potentially_dead_lists_report_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe Reports::PotentiallyDeadListsReport do 2 | it "delegates to the subscriber list report" do 3 | inactive_list = create(:subscriber_list, 4 | created_at: 2.years.ago) 5 | 6 | create(:subscription, 7 | subscriber_list: inactive_list, 8 | created_at: 13.months.ago) 9 | 10 | # active lists 11 | create(:subscription, created_at: 11.months.ago) 12 | create(:matched_content_change) 13 | create(:matched_message) 14 | 15 | # archivable list 16 | create(:subscriber_list) 17 | 18 | expect(Reports::SubscriberListsReport).to receive(:new) 19 | .with(Date.yesterday.to_s, slugs: inactive_list.slug) 20 | .and_call_original 21 | 22 | described_class.new.call 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/models/digest_run_subscriber_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe DigestRunSubscriber do 2 | describe ".populate" do 3 | let(:digest_run) { create(:digest_run) } 4 | 5 | it "inserts digest run subscribers" do 6 | subscribers = create_list(:subscriber, 2) 7 | expect { described_class.populate(digest_run, subscribers.map(&:id)) } 8 | .to change { described_class.count }.by(2) 9 | end 10 | 11 | it "sets the appropriate attributes" do 12 | freeze_time do 13 | subscriber = create(:subscriber) 14 | described_class.populate(digest_run, [subscriber.id]) 15 | expect(described_class.last).to have_attributes( 16 | digest_run_id: digest_run.id, 17 | subscriber_id: subscriber.id, 18 | created_at: Time.zone.now, 19 | updated_at: Time.zone.now, 20 | ) 21 | end 22 | end 23 | 24 | it "returns an array of ids" do 25 | subscriber = create(:subscriber) 26 | ids = described_class.populate(digest_run, [subscriber.id]) 27 | expect(ids).to eq([described_class.last.id]) 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/models/email_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe Email do 2 | describe ".timed_bulk_insert" do 3 | let(:records) do 4 | 3.times.map do |i| 5 | { 6 | subject: "subject #{i}", 7 | body: "body #{i}", 8 | address: "#{i}@example.com", 9 | created_at: Time.zone.now, 10 | updated_at: Time.zone.now, 11 | } 12 | end 13 | end 14 | 15 | context "when we're inserting a full batch of emails" do 16 | it "times the insert" do 17 | expect(Metrics).to receive(:email_bulk_insert).and_call_original 18 | expect(described_class).to receive(:insert_all!).with(records) 19 | described_class.timed_bulk_insert(records, 3) 20 | end 21 | end 22 | 23 | context "when we're not inserting a full batch of emails" do 24 | it "doesn't time the insert" do 25 | expect(Metrics).not_to receive(:email_bulk_insert) 26 | expect(described_class).to receive(:insert_all!).with(records) 27 | described_class.timed_bulk_insert(records, 5) 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/models/matched_content_change_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe MatchedContentChange do 2 | context "validations" do 3 | subject { build(:matched_content_change) } 4 | 5 | it "is valid for the default factory" do 6 | expect(subject).to be_valid 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/models/matched_message_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe MatchedMessage do 2 | it "is valid for the default factory" do 3 | expect(build(:matched_message)).to be_valid 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/models/user_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe User, type: :model do 2 | it_behaves_like "a gds-sso user class" 3 | end 4 | -------------------------------------------------------------------------------- /spec/presenters/bulk_email_body_presenter_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe BulkEmailBodyPresenter do 2 | describe ".call" do 3 | it "substitutes the list URL in the body" do 4 | subscriber_list = build(:subscriber_list, url: "/url") 5 | body = "something [link](%LISTURL%)." 6 | 7 | allow(PublicUrls).to receive(:url_for) 8 | .with( 9 | base_path: "/url", 10 | utm_campaign: "govuk-notifications-bulk", 11 | utm_source: subscriber_list.slug, 12 | ) 13 | .and_return("domain/url") 14 | 15 | result = described_class.call(body, subscriber_list) 16 | expect(result).to include("something [link](domain/url).") 17 | end 18 | 19 | it "copes when the subscriber list has no URL" do 20 | subscriber_list = build(:subscriber_list) 21 | body = "something [link](%LISTURL%)." 22 | 23 | result = described_class.call(body, subscriber_list) 24 | expect(result).to include("something [link]().") 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/presenters/message_presenter_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe MessagePresenter do 2 | describe ".call" do 3 | around do |example| 4 | ClimateControl.modify(GOVUK_APP_DOMAIN: "gov.uk") { example.run } 5 | end 6 | 7 | it "returns a presenter message" do 8 | message = create( 9 | :message, 10 | body: "Some information\nfor a user", 11 | ) 12 | 13 | expected = <<~MESSAGE 14 | Some information 15 | for a user 16 | MESSAGE 17 | 18 | expect(described_class.call(message)).to eq(expected.strip) 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/services/auth_token_generator_service_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe AuthTokenGeneratorService do 2 | include TokenHelpers 3 | 4 | describe ".call" do 5 | it "can be decoded" do 6 | token = described_class.call("token") 7 | expect(decrypt_and_verify_token(token)).to eq("token") 8 | end 9 | 10 | it "expires in one week" do 11 | token = described_class.call("token") 12 | 13 | travel_to(6.days.from_now) do 14 | expect(decrypt_and_verify_token(token)).to_not be_nil 15 | end 16 | 17 | travel_to(8.days.from_now) do 18 | expect(decrypt_and_verify_token(token)).to be_nil 19 | end 20 | end 21 | end 22 | 23 | describe ".crypt" do 24 | it "caches the encryptor for performance" do 25 | expect(described_class.crypt).to be_a ActiveSupport::MessageEncryptor 26 | expect(described_class.crypt).to equal(described_class.crypt) 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/services/matched_content_change_generation_service_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe MatchedContentChangeGenerationService do 2 | let(:content_change) do 3 | create(:content_change, tags: { tribunal_decision_categories: %w[transfer-of-undertakings] }) 4 | end 5 | 6 | let!(:subscriber_list) do 7 | create(:subscriber_list, tags: { tribunal_decision_categories: { any: %w[transfer-of-undertakings] } }) 8 | end 9 | 10 | describe ".call" do 11 | it "creates a MatchedContentChange" do 12 | expect { described_class.call(content_change) } 13 | .to change { MatchedContentChange.count }.by(1) 14 | end 15 | 16 | it "copes when there aren't any subscriber lists for the content change" do 17 | no_match_content_change = create(:content_change) 18 | expect { described_class.call(no_match_content_change) } 19 | .to_not(change { MatchedContentChange.count }) 20 | end 21 | 22 | it "copes and does nothing when the MatchedContentChange records already exists" do 23 | MatchedContentChange.create!(content_change:, 24 | subscriber_list:) 25 | 26 | expect { described_class.call(content_change) } 27 | .to_not(change { MatchedContentChange.count }) 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/services/matched_message_generation_service_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe MatchedMessageGenerationService do 2 | let(:message) do 3 | create( 4 | :message, 5 | criteria_rules: [ 6 | { type: "tag", key: "tribunal_decision_categories", value: "transfer-of-undertakings" }, 7 | ], 8 | ) 9 | end 10 | 11 | let!(:subscriber_list) do 12 | create(:subscriber_list, tags: { tribunal_decision_categories: { any: %w[transfer-of-undertakings] } }) 13 | end 14 | 15 | describe ".call" do 16 | it "creates a MatchedMessage" do 17 | expect { described_class.call(message) } 18 | .to change { MatchedMessage.count }.by(1) 19 | end 20 | 21 | it "copes when there aren't any subscriber lists for the message" do 22 | no_match_message = create(:message) 23 | expect { described_class.call(no_match_message) } 24 | .to_not(change { MatchedMessage.count }) 25 | end 26 | 27 | it "copes and does nothing when the MatchedMessage records already exists" do 28 | MatchedMessage.create!(message:, subscriber_list:) 29 | 30 | expect { described_class.call(message) } 31 | .to_not(change { MatchedMessage.count }) 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/services/send_email_service/send_pseudo_email_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe SendEmailService::SendPseudoEmail do 2 | describe ".call" do 3 | let(:email) { create(:email) } 4 | 5 | it "writes the email details to the Rails log file" do 6 | expect(Rails.logger).to receive(:info).with(/Logging email/) 7 | described_class.call(email) 8 | end 9 | 10 | it "marks the email as sent" do 11 | freeze_time do 12 | expect { described_class.call(email) } 13 | .to change { email.reload.status }.to("sent") 14 | .and change { email.reload.sent_at }.to(Time.zone.now) 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/services/unsubscribe_all_service_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe UnsubscribeAllService do 2 | describe ".call" do 3 | let(:subscriber) { create(:subscriber) } 4 | 5 | before do 6 | create_list(:subscription, 2, subscriber:) 7 | end 8 | 9 | it "ends the active subscriptions" do 10 | described_class.call(subscriber, :unsubscribed) 11 | expect(subscriber.active_subscriptions.count).to eq 0 12 | end 13 | 14 | it "records how many subscriptions have been ended" do 15 | expect(Metrics).to receive(:unsubscribed).with(:unsubscribed, 2) 16 | described_class.call(subscriber, :unsubscribed) 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/services/update_last_alerted_at_subscriber_list_service_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe UpdateLastAlertedAtSubscriberListService do 2 | describe ".call" do 3 | let(:associated_subscription) { create(:subscription) } 4 | let(:associated_content_change) { create(:content_change) } 5 | 6 | it "updates the last_alerted_at date for the associated_subscription subscriber_list" do 7 | create( 8 | :matched_content_change, 9 | subscriber_list: associated_subscription.subscriber_list, 10 | content_change: associated_content_change, 11 | ) 12 | 13 | expect { 14 | described_class.call(associated_content_change) 15 | }.to change { associated_subscription.subscriber_list.reload.last_alerted_at }.from(nil).to(be_present) 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/support/authentication_helpers.rb: -------------------------------------------------------------------------------- 1 | module AuthenticationHelpers 2 | def login_with_internal_app 3 | login_with("internal_app") 4 | end 5 | 6 | def login_with_signin 7 | login_with("signin") 8 | end 9 | 10 | def login_with_status_updates 11 | login_with("status_updates") 12 | end 13 | 14 | def login_with(permissions) 15 | login_as(create(:user, permissions: Array(permissions))) 16 | end 17 | 18 | def without_login(&block) 19 | ClimateControl.modify(GDS_SSO_MOCK_INVALID: "1", &block) 20 | end 21 | 22 | def login_as(user) 23 | GDS::SSO.test_user = user 24 | end 25 | 26 | def logout 27 | GDS::SSO.test_user = nil 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/support/search_alert_list_helpers.rb: -------------------------------------------------------------------------------- 1 | module SearchAlertListHelpers 2 | def medical_safety_alert_search_body(content_id:, public_timestamp:) 3 | { 4 | results: [ 5 | { 6 | content_id:, 7 | link: "/drug-device-alerts/test", 8 | public_timestamp:, 9 | index: "govuk", 10 | es_score: nil, 11 | model_score: nil, 12 | original_rank: nil, 13 | combined_score: nil, 14 | _id: "/drug-device-alerts/test", 15 | elasticsearch_type: "medical_safety_alert", 16 | document_type: "medical_safety_alert", 17 | }, 18 | ], 19 | total: 1, 20 | start: 0, 21 | aggregates: {}, 22 | suggested_queries: [], 23 | suggested_autocomplete: [], 24 | es_cluster: "A", 25 | reranked: false, 26 | } 27 | end 28 | 29 | def stub_medical_safety_alert_query(content_id:, age:) 30 | stub_request(:get, "http://search-api.dev.gov.uk/search.json?count=50&fields=content_id,link,public_timestamp&filter_format=medical_safety_alert&order=-public_timestamp") 31 | .to_return(status: 200, body: medical_safety_alert_search_body(content_id:, public_timestamp: (Time.zone.now - age).strftime("%Y-%m-%dT%H:%M:%SZ")).to_json, headers: {}) 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/support/token_helpers.rb: -------------------------------------------------------------------------------- 1 | module TokenHelpers 2 | def decrypt_token_from_link(body) 3 | token = URI.decode_www_form_component( 4 | body.match(/token=([^&)]+)/)[1], 5 | ) 6 | 7 | decrypt_and_verify_token(token) 8 | end 9 | 10 | def decrypt_and_verify_token(data) 11 | cipher = AuthTokenGeneratorService::CIPHER 12 | len = ActiveSupport::MessageEncryptor.key_len(cipher) 13 | 14 | secret = Rails.application.credentials.email_alert_auth_token 15 | key = ActiveSupport::KeyGenerator.new(secret, hash_digest_class: OpenSSL::Digest::SHA256).generate_key("", len) 16 | 17 | options = AuthTokenGeneratorService::OPTIONS 18 | crypt = ActiveSupport::MessageEncryptor.new(key, **options) 19 | crypt.decrypt_and_verify(data) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/validators/email_address_validator_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe EmailAddressValidator do 2 | let(:record_class) do 3 | Class.new do 4 | include ActiveModel::Validations 5 | include ActiveModel::Model 6 | 7 | attr_accessor :email 8 | 9 | validates :email, email_address: true 10 | end 11 | end 12 | 13 | it "is valid when a valid email is provided" do 14 | expect(record_class.new(email: "test@example.com")).to be_valid 15 | end 16 | 17 | it "is invalid when an invalid email is provided" do 18 | record = record_class.new(email: "bad email") 19 | expect(record).to be_invalid 20 | expect(record.errors[:email]).to match(["is not an email address"]) 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/validators/links_validator_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe LinksValidator do 2 | let(:record_class) do 3 | Class.new do 4 | include ActiveModel::Validations 5 | include ActiveModel::Model 6 | 7 | attr_accessor :links 8 | 9 | validates :links, links: true 10 | end 11 | end 12 | 13 | it "is valid when links match a UUID formatting" do 14 | record = record_class.new(links: { 15 | commodity_type: { any: %w[f3bbdec2-0e62-4520-a7fd-6ffd5d36e03a] }, 16 | }) 17 | 18 | expect(record).to be_valid 19 | end 20 | 21 | it "is invalid when links with other formats are provided" do 22 | record = record_class.new(links: { 23 | organisations: { any: %w[dogs cats] }, 24 | countries: { any: %w[dogs cats] }, 25 | foo: { any: %w([dogs] !cats) }, 26 | people: { any: %w[\u0000] }, 27 | taxon_tree: { any: ">" }, 28 | }) 29 | 30 | expect(record).to be_invalid 31 | expect(record.errors[:links]).to match([ 32 | "foo, people, and taxon_tree has a value with an invalid format.", 33 | ]) 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /spec/validators/root_relative_url_validator_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe RootRelativeUrlValidator do 2 | let(:record_class) do 3 | Class.new do 4 | include ActiveModel::Validations 5 | include ActiveModel::Model 6 | 7 | attr_accessor :url 8 | 9 | validates :url, root_relative_url: true 10 | end 11 | end 12 | 13 | it "is valid for an absolute path" do 14 | expect(record_class.new(url: "/path")).to be_valid 15 | end 16 | 17 | it "is invalid for a relative path" do 18 | expect(record_class.new(url: "path")).to be_invalid 19 | end 20 | 21 | it "is invalid when a protocol-relative url is provided" do 22 | expect(record_class.new(url: "//example.com/test")).to be_invalid 23 | end 24 | 25 | it "is invalid for an invalid URI" do 26 | expect(record_class.new(url: "bad uri")).to be_invalid 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/validators/uuid_validator_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe UuidValidator do 2 | let(:record_class) do 3 | Class.new do 4 | include ActiveModel::Validations 5 | include ActiveModel::Model 6 | 7 | attr_accessor :uuid 8 | 9 | validates :uuid, uuid: true 10 | end 11 | end 12 | 13 | it "is valid for a correctly formatted UUID" do 14 | expect(record_class.new(uuid: SecureRandom.uuid)).to be_valid 15 | end 16 | 17 | it "is invalid for a incorrectly formatted UUID" do 18 | expect(record_class.new(uuid: "not a UUID")).to be_invalid 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/workers/daily_digest_initiator_worker_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe DailyDigestInitiatorWorker do 2 | describe ".perform" do 3 | it "calls the daily digest initiator service" do 4 | expect(DigestInitiatorService).to receive(:call) 5 | .with(date: Date.current, range: Frequency::DAILY) 6 | 7 | subject.perform 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/workers/email_deletion_worker_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe EmailDeletionWorker do 2 | describe "#perform" do 3 | def perform 4 | described_class.new.perform 5 | end 6 | 7 | context "when there are no emails to delete" do 8 | before { create(:email) } 9 | 10 | it "doesn't change the number of Email records" do 11 | expect { perform }.not_to(change { Email.count }) 12 | end 13 | end 14 | 15 | context "when there are emails to delete" do 16 | before { create(:deleteable_email) } 17 | 18 | it "deletes the Email records" do 19 | expect { perform }.to change { Email.count }.by(-1) 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/workers/metrics_collection_worker/content_change_exporter_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe MetricsCollectionWorker::ContentChangeExporter do 2 | describe ".call" do 3 | let(:statsd) { double } 4 | 5 | before do 6 | create(:content_change, created_at: 122.minutes.ago) 7 | allow(GovukStatsd).to receive(:gauge) 8 | end 9 | 10 | it "records total number of unprocessed content changes over 120 minutes old" do 11 | expect(GovukStatsd).to receive(:gauge).with("content_changes.unprocessed_total", 1) 12 | described_class.call 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/workers/metrics_collection_worker/digest_run_exporter_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe MetricsCollectionWorker::DigestRunExporter do 2 | describe ".call" do 3 | it "records number of unprocessed digest runs over 2 hours old (critical)" do 4 | # Digest runs must be created after 8am to validate 5 | travel_to("10:00") do 6 | create(:digest_run, created_at: 2.days.ago, date: 2.days.ago) 7 | create(:digest_run, created_at: 21.minutes.ago, date: Time.zone.today) 8 | expect(GovukStatsd).to receive(:gauge).with("digest_runs.critical_total", 1) 9 | described_class.call 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/workers/metrics_collection_worker/message_exporter_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe MetricsCollectionWorker::MessageExporter do 2 | describe ".call" do 3 | let(:statsd) { double } 4 | 5 | before do 6 | 3.times { create(:message, created_at: 122.minutes.ago) } 7 | allow(GovukStatsd).to receive(:gauge) 8 | end 9 | 10 | it "records total number of unprocessed messages over 120 minutes old" do 11 | expect(GovukStatsd).to receive(:gauge).with("messages.unprocessed_total", 3) 12 | described_class.call 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/workers/metrics_collection_worker_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe MetricsCollectionWorker do 2 | describe ".perform" do 3 | it "delegates to collect metrics" do 4 | expect(MetricsCollectionWorker::ContentChangeExporter).to receive(:call) 5 | expect(MetricsCollectionWorker::DigestRunExporter).to receive(:call) 6 | expect(MetricsCollectionWorker::MessageExporter).to receive(:call) 7 | 8 | subject.perform 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/workers/recover_lost_jobs_worker/missing_digest_runs_check_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe RecoverLostJobsWorker::MissingDigestRunsCheck do 2 | describe "#call" do 3 | it "can create missing work for the week" do 4 | tuesday = Time.zone.parse("2017-01-10 10:30") 5 | travel_to tuesday 6 | 7 | subject.call 8 | expect(DigestRun.where(range: :daily).where("date > ?", 7.days.ago).count).to eq 7 9 | expect(DigestRun.where(range: :weekly).where("date > ?", 7.days.ago).count).to eq 1 10 | end 11 | 12 | it "does not create duplicate work" do 13 | tuesday = Time.zone.parse("2017-01-10 10:30") 14 | travel_to tuesday 15 | 16 | create(:digest_run, range: :daily, date: 2.days.ago) 17 | create(:digest_run, range: :weekly, date: 3.days.ago) 18 | 19 | subject.call 20 | expect(DigestRun.where(date: 2.days.ago, range: :daily).count).to eq 1 21 | expect(DigestRun.where(date: 3.days.ago, range: :weekly).count).to eq 1 22 | end 23 | 24 | it "does not create work prematurely" do 25 | early_saturday = Time.zone.parse("2017-01-07 05:30") 26 | travel_to early_saturday 27 | subject.call 28 | expect(DigestRun.where(date: Date.current).count).to eq 0 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/workers/recover_lost_jobs_worker/old_pending_emails_check_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe RecoverLostJobsWorker::OldPendingEmailsCheck do 2 | describe "#call" do 3 | it "recovers pending emails over an hour old" do 4 | email = create(:email, created_at: 4.hours.ago) 5 | expect(SendEmailWorker) 6 | .to receive(:perform_async_in_queue) 7 | .with(email.id, queue: :send_email_immediate) 8 | 9 | subject.call 10 | end 11 | 12 | it "does not recover recent pending emails" do 13 | create(:email, created_at: 2.hours.ago) 14 | expect(SendEmailWorker).to_not receive(:perform_async_in_queue) 15 | subject.call 16 | end 17 | 18 | it "does not recover emails that aren't pending" do 19 | create(:email, created_at: 4.hours.ago, status: :sent) 20 | expect(ProcessContentChangeWorker).not_to receive(:perform_async_in_queue) 21 | subject.call 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/workers/recover_lost_jobs_worker_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe RecoverLostJobsWorker do 2 | describe "#perform" do 3 | it "delegates recovery" do 4 | expect_any_instance_of(RecoverLostJobsWorker::UnprocessedCheck).to receive(:call) 5 | expect_any_instance_of(RecoverLostJobsWorker::MissingDigestRunsCheck).to receive(:call) 6 | expect_any_instance_of(RecoverLostJobsWorker::OldPendingEmailsCheck).to receive(:call) 7 | 8 | subject.perform 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/workers/weekly_digest_initiator_worker_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe WeeklyDigestInitiatorWorker do 2 | describe ".perform" do 3 | it "calls the weekly digest initiator service" do 4 | expect(DigestInitiatorService).to receive(:call) 5 | .with(date: Date.current, range: Frequency::WEEKLY) 6 | 7 | subject.perform 8 | end 9 | end 10 | end 11 | --------------------------------------------------------------------------------