From 6029fd1a4e816c957e9f330a62c00f007a3c1e17 Mon Sep 17 00:00:00 2001 From: Victoria Mihell-Hale Date: Thu, 16 Jan 2025 22:21:59 +0000 Subject: [PATCH] [Bexley][WW] Cancel GGW subscription --- perllib/FixMyStreet/App/Controller/Waste.pm | 4 +- .../App/Form/Waste/Garden/Cancel.pm | 1 + .../App/Form/Waste/Garden/Cancel/Bexley.pm | 38 +++++ perllib/FixMyStreet/Cobrand/Bexley/Garden.pm | 93 +++++++++++- perllib/FixMyStreet/Cobrand/Bexley/Waste.pm | 5 +- perllib/Integrations/Agile.pm | 78 ++++++++++ t/app/controller/waste_bexley.t | 9 +- .../waste_bexley_container_requests.t | 4 + t/app/controller/waste_bexley_garden.t | 141 ++++++++++++++++-- 9 files changed, 356 insertions(+), 17 deletions(-) create mode 100644 perllib/FixMyStreet/App/Form/Waste/Garden/Cancel/Bexley.pm create mode 100644 perllib/Integrations/Agile.pm diff --git a/perllib/FixMyStreet/App/Controller/Waste.pm b/perllib/FixMyStreet/App/Controller/Waste.pm index aeb2a4a0ebf..fa97724bc69 100644 --- a/perllib/FixMyStreet/App/Controller/Waste.pm +++ b/perllib/FixMyStreet/App/Controller/Waste.pm @@ -1280,7 +1280,9 @@ sub garden_cancel : Chained('garden_setup') : Args(0) { $c->forward('check_if_staff_can_pay', [ $payment_method ]); $c->stash->{first_page} = 'intro'; - $c->stash->{form_class} = 'FixMyStreet::App::Form::Waste::Garden::Cancel'; + $c->stash->{form_class} + = $c->cobrand->call_hook('waste_cancel_form_class') + || 'FixMyStreet::App::Form::Waste::Garden::Cancel'; $c->forward('form'); } diff --git a/perllib/FixMyStreet/App/Form/Waste/Garden/Cancel.pm b/perllib/FixMyStreet/App/Form/Waste/Garden/Cancel.pm index 65447b80e25..71912143877 100644 --- a/perllib/FixMyStreet/App/Form/Waste/Garden/Cancel.pm +++ b/perllib/FixMyStreet/App/Form/Waste/Garden/Cancel.pm @@ -34,6 +34,7 @@ has_field confirm => ( option_label => 'I confirm I wish to cancel my subscription', required => 1, label => "Confirm", + order => 998, ); has_field submit => ( diff --git a/perllib/FixMyStreet/App/Form/Waste/Garden/Cancel/Bexley.pm b/perllib/FixMyStreet/App/Form/Waste/Garden/Cancel/Bexley.pm new file mode 100644 index 00000000000..faab311ded6 --- /dev/null +++ b/perllib/FixMyStreet/App/Form/Waste/Garden/Cancel/Bexley.pm @@ -0,0 +1,38 @@ +package FixMyStreet::App::Form::Waste::Garden::Cancel::Bexley; + +use utf8; +use HTML::FormHandler::Moose; +extends 'FixMyStreet::App::Form::Waste::Garden::Cancel'; + +has_field reason => ( + type => 'Select', + widget => 'RadioGroup', + required => 1, + label => 'Reason for cancellation', + messages => { required => 'Please select a reason' }, +); + +sub options_reason { + my $form = shift; + + my @options = ( + 'Price', + 'Service Issues', + 'Moving Out of Borough', + 'Other', + ); + return map { { label => $_, value => $_ } } @options; +} + +has_field reason_further_details => ( + required => 1, + type => 'Text', + widget => 'Textarea', + label => + "If you selected 'Other', please provide further details (up to 250 characters)", + required_when => { reason => 'Other' }, + maxlength => 250, + messages => { required => 'Please provide further details' }, +); + +1; diff --git a/perllib/FixMyStreet/Cobrand/Bexley/Garden.pm b/perllib/FixMyStreet/Cobrand/Bexley/Garden.pm index 46a623ece76..c766fa8bd24 100644 --- a/perllib/FixMyStreet/Cobrand/Bexley/Garden.pm +++ b/perllib/FixMyStreet/Cobrand/Bexley/Garden.pm @@ -6,15 +6,102 @@ FixMyStreet::Cobrand::Bexley::Garden - code specific to Bexley WasteWorks GGW package FixMyStreet::Cobrand::Bexley::Garden; +use DateTime::Format::Strptime; +use Integrations::Agile; +use FixMyStreet::App::Form::Waste::Garden::Cancel::Bexley; + use Moo::Role; with 'FixMyStreet::Roles::Cobrand::SCP', 'FixMyStreet::Roles::Cobrand::Paye'; +has agile => ( + is => 'lazy', + default => sub { + my $self = shift; + my $cfg = $self->feature('agile'); + return Integrations::Agile->new(%$cfg); + }, +); + sub garden_service_name { 'garden waste collection service' } -# TODO No current subscription look up here -# -sub garden_current_subscription { undef } +sub garden_service_ids { + return [ 'GA-140', 'GA-240' ]; +} + +sub garden_current_subscription { + my $self = shift; + + my $current = $self->{c}->stash->{property}{garden_current_subscription}; + return $current if $current; + + my $uprn = $self->{c}->stash->{property}{uprn}; + return undef unless $uprn; + +# TODO Fetch active subscription from DB for UPRN +# (get_original_sub() in Controller/Waste.pm needs to handle Bexley UPRN). +# Could be more than one customer, so match against email. +# Could be more than one contract, so match against reference. + + my $results = $self->agile->CustomerSearch($uprn); + return undef unless $results && $results->{Customers}; + my $customer = $results->{Customers}[0]; + return undef unless $customer && $customer->{ServiceContracts}; + my $contract = $customer->{ServiceContracts}[0]; + return unless $contract; + + my $parser + = DateTime::Format::Strptime->new( pattern => '%d/%m/%Y %H:%M' ); + my $end_date = $parser->parse_datetime( $contract->{EndDate} ); + + # Agile says there is a subscription; now get service data from + # Whitespace + my $services = $self->{c}->stash->{services}; + for ( @{ $self->garden_service_ids } ) { + if ( my $srv = $services->{$_} ) { + $srv->{customer_external_ref} + = $customer->{CustomerExternalReference}; + $srv->{end_date} = $end_date; + return $srv; + } + } + + return { + agile_only => 1, + customer_external_ref => $customer->{CustomerExternalReference}, + end_date => $end_date, + }; +} + +# TODO This is a placeholder +sub get_current_garden_bins { 1 } + +sub waste_cancel_asks_staff_for_user_details { 1 } + +sub waste_cancel_form_class { + 'FixMyStreet::App::Form::Waste::Garden::Cancel::Bexley'; +} + +sub waste_garden_sub_params { + my ( $self, $data, $type ) = @_; + + my $c = $self->{c}; + + if ( $data->{category} eq 'Cancel Garden Subscription' ) { + my $srv = $self->garden_current_subscription; + + my $parser = DateTime::Format::Strptime->new( pattern => '%d/%m/%Y' ); + my $due_date_str = $parser->format_datetime( $srv->{end_date} ); + + my $reason = $data->{reason}; + $reason .= ': ' . $data->{reason_further_details} + if $data->{reason_further_details}; + + $c->set_param( 'customer_external_ref', $srv->{customer_external_ref} ); + $c->set_param( 'due_date', $due_date_str ); + $c->set_param( 'reason', $reason ); + } +} =item * You can order a maximum of five bins diff --git a/perllib/FixMyStreet/Cobrand/Bexley/Waste.pm b/perllib/FixMyStreet/Cobrand/Bexley/Waste.pm index 44b9cee4afd..5e91492b2d9 100644 --- a/perllib/FixMyStreet/Cobrand/Bexley/Waste.pm +++ b/perllib/FixMyStreet/Cobrand/Bexley/Waste.pm @@ -459,6 +459,9 @@ sub bin_services_for_address { ]; } + $property->{garden_current_subscription} + = $self->garden_current_subscription; + @site_services_filtered = $self->_remove_service_if_assisted_exists(@site_services_filtered); @site_services_filtered = $self->service_sort(@site_services_filtered); @@ -1246,7 +1249,7 @@ sub in_cab_logs_reason_prefixes { 'Clear Sacks' => ['MDR-SACK', 'CW-SACK'], 'Paper & Card' => ['PA-1100', 'PA-1280', 'PA-140', 'PA-240', 'PA-55', 'PA-660', 'PA-940', 'PC-180', 'PC-55'], 'Food' => ['FO-140', 'FO-23'], - 'Garden' => ['GA-140', 'GA-240'], + 'Garden' => ['GA-140', 'GA-240'], # TODO Call Garden.pm->garden_service_ids to make sure these IDs are consistent 'Plastics & Glass' => ['PG-1100', 'PG-1280', 'PG-240', 'PG-360', 'PG-55', 'PG-660', 'PG-940', 'PL-1100', 'PL-1280', 'PL-140', 'PL-55', 'PL-660', 'PL-940'], 'Glass' => ['GL-1100', 'GL-1280', 'GL-55', 'GL-660'], 'Refuse' => ['RES-1100', 'RES-1280', 'RES-140', 'RES-180', 'RES-240', 'RES-660', 'RES-720', 'RES-940', 'RES-CHAM', 'RES-DBIN', 'RES-SACK'], diff --git a/perllib/Integrations/Agile.pm b/perllib/Integrations/Agile.pm new file mode 100644 index 00000000000..ff76321dfab --- /dev/null +++ b/perllib/Integrations/Agile.pm @@ -0,0 +1,78 @@ +=head1 NAME + +Integrations::Agile - Agile Applications API integration + +=head1 DESCRIPTION + +This module provides an interface to the Agile Applications API + +=cut + +package Integrations::Agile; + +use strict; +use warnings; + +use HTTP::Request; +use JSON::MaybeXS; +use LWP::UserAgent; +use Moo; +use URI; + +has url => ( is => 'ro' ); + +# TODO Logging + +sub call { + my ( $self, %args ) = @_; + + my $action = $args{action}; + my $controller = $args{controller}; + my $data = $args{data}; + my $method = 'POST'; + + my $body = { + Method => $method, + Controller => $controller, + Action => $action, + Data => $data, + }; + my $body_json = encode_json($body); + + my $uri = URI->new( $self->{url} ); + + my $req = HTTP::Request->new( $method, $uri ); + $req->content_type('application/json; charset=UTF-8'); + $req->content($body_json); + + my $ua = LWP::UserAgent->new; + my $res = $ua->request($req); + + if ( $res->is_success ) { + return decode_json( $res->content ); + } else { + die $res->content; + } +} + +sub IsAddressFree { + my ( $self, $uprn ) = @_; + + return $self->call( + action => 'isaddressfree', + controller => 'customer', + data => { UPRN => $uprn }, + ); +} + +sub CustomerSearch { + my ( $self, $uprn ) = @_; + + return $self->call( + action => 'search', + controller => 'customer', + data => { ServiceContractUPRN => $uprn }, + ); +} + +1; diff --git a/t/app/controller/waste_bexley.t b/t/app/controller/waste_bexley.t index a88ad75d04d..512a6c60d91 100644 --- a/t/app/controller/waste_bexley.t +++ b/t/app/controller/waste_bexley.t @@ -21,6 +21,9 @@ my $mock_waste = Test::MockModule->new('BexleyAddresses'); # We don't actually read from the file, so just put anything that is a valid path $mock_waste->mock( 'database_file', '/' ); +my $agile_mock = Test::MockModule->new('Integrations::Agile'); +$agile_mock->mock( 'CustomerSearch', sub { {} } ); + my $dbi_mock = Test::MockModule->new('DBI'); $dbi_mock->mock( 'connect', sub { my $dbh = Test::MockObject->new; @@ -268,9 +271,9 @@ $existing_missed_collection_report2->add_to_comments( FixMyStreet::override_config { ALLOWED_COBRANDS => 'bexley', MAPIT_URL => 'http://mapit.uk/', - COBRAND_FEATURES => { whitespace => { bexley => { - url => 'http://example.org/', - } }, + COBRAND_FEATURES => { + whitespace => { bexley => { url => 'http://example.org/' } }, + agile => { bexley => { url => 'test' } }, waste => { bexley => 1 }, waste_calendar_links => { bexley => { 'Wk-1' => 'PDF 1', 'Wk-2' => 'PDF 2'} }, }, diff --git a/t/app/controller/waste_bexley_container_requests.t b/t/app/controller/waste_bexley_container_requests.t index a343e6b1834..c2b980d9024 100644 --- a/t/app/controller/waste_bexley_container_requests.t +++ b/t/app/controller/waste_bexley_container_requests.t @@ -22,6 +22,9 @@ $dbi_mock->mock( 'connect', sub { return $dbh; } ); +my $agile_mock = Test::MockModule->new('Integrations::Agile'); +$agile_mock->mock( 'CustomerSearch', sub { {} } ); + my $mech = FixMyStreet::TestMech->new; my $cobrand = FixMyStreet::Cobrand::Bexley->new; @@ -674,6 +677,7 @@ FixMyStreet::override_config { COBRAND_FEATURES => { waste => { bexley => 1 }, whitespace => { bexley => { url => 'http://example.org/' } }, + agile => { bexley => { url => 'test' } }, }, }, sub { my $whitespace_mock = Test::MockModule->new('Integrations::Whitespace'); diff --git a/t/app/controller/waste_bexley_garden.t b/t/app/controller/waste_bexley_garden.t index ebadd0c9d30..2f4b12bcbb7 100644 --- a/t/app/controller/waste_bexley_garden.t +++ b/t/app/controller/waste_bexley_garden.t @@ -18,6 +18,9 @@ $dbi_mock->mock( 'connect', sub { return $dbh; } ); +my $agile_mock = Test::MockModule->new('Integrations::Agile'); +$agile_mock->mock( 'CustomerSearch', sub { {} } ); + my $mech = FixMyStreet::TestMech->new; my $body = $mech->create_body_ok(2494, 'Bexley', { cobrand => 'bexley' }); @@ -44,6 +47,14 @@ create_contact({ category => 'Garden Subscription', email => 'garden@example.com { code => 'payment', required => 1, automated => 'hidden_field' }, { code => 'payment_method', required => 1, automated => 'hidden_field' }, ); +create_contact( + { category => 'Cancel Garden Subscription', email => 'garden_cancel@example.com' }, + { code => 'customer_external_ref', required => 1, automated => 'hidden_field' }, + { code => 'due_date', required => 1, automated => 'hidden_field' }, + { code => 'reason', required => 1, automated => 'hidden_field' }, + { code => 'uprn', required => 1, automated => 'hidden_field' }, + { code => 'fixmystreet_id', required => 1, automated => 'server' }, +); my $whitespace_mock = Test::MockModule->new('Integrations::Whitespace'); sub default_mocks { @@ -82,16 +93,17 @@ FixMyStreet::override_config { whitespace => { bexley => { url => 'https://example.net/', } }, + agile => { bexley => { url => 'test' } }, payment_gateway => { bexley => { - ggw_cost_first => 7500, - ggw_cost => 5500, - cc_url => 'http://example.org/cc_submit', - scpID => 1234, - hmac_id => 1234, - hmac => 1234, - paye_siteID => 1234, - paye_hmac_id => 1234, - paye_hmac => 1234, + ggw_cost_first => 7500, + ggw_cost => 5500, + cc_url => 'http://example.org/cc_submit', + scpID => 1234, + hmac_id => 1234, + hmac => 1234, + paye_siteID => 1234, + paye_hmac_id => 1234, + paye_hmac => 1234, } }, }, }, sub { @@ -327,6 +339,117 @@ FixMyStreet::override_config { like $body, qr/Bins to be removed: 1/; like $body, qr/Total:.*?75.00/; }; + + subtest 'cancel garden subscription' => sub { + set_fixed_time('2024-02-01T00:00:00'); + + $agile_mock->mock( 'CustomerSearch', sub { { + Customers => [ + { + CustomerExternalReference => 'CUSTOMER_123', + ServiceContracts => [ + { + EndDate => '12/12/2025 12:21', + }, + ], + }, + ], + } } ); + + $mech->log_in_ok( $user->email ); + + subtest 'with Agile data only' => sub { + $mech->get_ok('/waste/10001'); + like $mech->text, qr/Sorry, we are unable to find any rubbish and recycling collections/; + + $mech->get_ok('/waste/10001/garden_cancel'); + like $mech->text, qr/Cancel your garden waste subscription/; + + $mech->submit_form_ok( + { with_fields => { + reason => 'Other', + reason_further_details => 'Burnt all my leaves', + confirm => 1, + }, + } + ); + like $mech->text, qr/Your subscription has been cancelled/, + 'form submitted OK'; + + my $report + = FixMyStreet::DB->resultset('Problem')->order_by('-id') + ->first; + + is $report->get_extra_field_value('customer_external_ref'), + 'CUSTOMER_123'; + is $report->get_extra_field_value('due_date'), + '12/12/2025'; + is $report->get_extra_field_value('reason'), + 'Other: Burnt all my leaves'; + + $mech->clear_emails_ok; + FixMyStreet::Script::Reports::send(); + + my @emails = $mech->get_email; + my $body = $mech->get_text_body_from_email($emails[1]); + like $body, qr/You have cancelled your garden waste collection service/; + }; + + subtest 'with Whitespace data' => sub { + $whitespace_mock->mock( + 'GetSiteCollections', + sub { + [ { SiteServiceID => 1, + ServiceItemDescription => 'Garden waste', + ServiceItemName => 'GA-140', # Garden 140 ltr Bin + ServiceName => 'Brown Wheelie Bin', + NextCollectionDate => '2024-02-07T00:00:00', + SiteServiceValidFrom => '2024-01-01T00:00:00', + SiteServiceValidTo => '0001-01-01T00:00:00', + + RoundSchedule => 'RND-1 Mon', + } + ]; + } + ); + + $mech->get_ok('/waste/10001'); + like $mech->content, qr/waste-service-subtitle.*Garden waste/s; + + $mech->get_ok('/waste/10001/garden_cancel'); + like $mech->text, qr/Cancel your garden waste subscription/; + + $mech->submit_form_ok( + { with_fields => { + reason => 'Other', + reason_further_details => 'Burnt all my leaves', + confirm => 1, + }, + } + ); + like $mech->text, qr/Your subscription has been cancelled/, + 'form submitted OK'; + + my $report + = FixMyStreet::DB->resultset('Problem')->order_by('-id') + ->first; + + is $report->get_extra_field_value('customer_external_ref'), + 'CUSTOMER_123'; + is $report->get_extra_field_value('due_date'), + '12/12/2025'; + is $report->get_extra_field_value('reason'), + 'Other: Burnt all my leaves'; + + $mech->clear_emails_ok; + FixMyStreet::Script::Reports::send(); + + my @emails = $mech->get_email; + my $body = $mech->get_text_body_from_email($emails[1]); + like $body, qr/You have cancelled your garden waste collection service/; + }; + + }; }; sub get_report_from_redirect {