From 60d01ed394a85abcff0f6977f39ef556263e2a6c Mon Sep 17 00:00:00 2001 From: Matthew Somerville Date: Thu, 19 Dec 2024 15:24:50 +0000 Subject: [PATCH] [Bexley] Garden subscription flow, front to payment. TODO lots more probably. It does not know until you have picked DD/CC that DD is cheaper for the first bin. Perhaps can be implemented as a 5GBP discount when you click the DD option? --- perllib/FixMyStreet/App/Controller/Waste.pm | 2 +- perllib/FixMyStreet/Cobrand/Bexley.pm | 5 +- perllib/FixMyStreet/Cobrand/Bexley/Garden.pm | 61 +++ t/app/controller/waste_bexley_garden.t | 380 +++++++++++++++++++ 4 files changed, 445 insertions(+), 3 deletions(-) create mode 100644 perllib/FixMyStreet/Cobrand/Bexley/Garden.pm create mode 100644 t/app/controller/waste_bexley_garden.t diff --git a/perllib/FixMyStreet/App/Controller/Waste.pm b/perllib/FixMyStreet/App/Controller/Waste.pm index 67ca3885f5b..346251af3ff 100644 --- a/perllib/FixMyStreet/App/Controller/Waste.pm +++ b/perllib/FixMyStreet/App/Controller/Waste.pm @@ -1429,7 +1429,7 @@ sub setup_garden_sub_params : Private { my $service_id; if (my $service = $c->cobrand->garden_current_subscription) { $service_id = $service->{service_id}; - } else { + } elsif ($c->cobrand->can('garden_service_id')) { # XXX TODO Does Bexley need its own? Or is this actually Echo only? $service_id = $c->cobrand->garden_service_id; } $c->set_param('email_renewal_reminders_opt_in', $data->{email_renewal_reminders} eq 'Yes' ? 'Y' : 'N') if $data->{email_renewal_reminders}; diff --git a/perllib/FixMyStreet/Cobrand/Bexley.pm b/perllib/FixMyStreet/Cobrand/Bexley.pm index cc152f2ded8..6bbed2fb570 100644 --- a/perllib/FixMyStreet/Cobrand/Bexley.pm +++ b/perllib/FixMyStreet/Cobrand/Bexley.pm @@ -18,8 +18,9 @@ use warnings; use Time::Piece; use DateTime; use Moo; -with 'FixMyStreet::Roles::Open311Multi'; -with 'FixMyStreet::Cobrand::Bexley::Waste'; +with 'FixMyStreet::Roles::Open311Multi', + 'FixMyStreet::Cobrand::Bexley::Garden', + 'FixMyStreet::Cobrand::Bexley::Waste'; sub council_area_id { 2494 } sub council_area { 'Bexley' } diff --git a/perllib/FixMyStreet/Cobrand/Bexley/Garden.pm b/perllib/FixMyStreet/Cobrand/Bexley/Garden.pm new file mode 100644 index 00000000000..9c092257a9b --- /dev/null +++ b/perllib/FixMyStreet/Cobrand/Bexley/Garden.pm @@ -0,0 +1,61 @@ +=head1 NAME + +FixMyStreet::Cobrand::Bexley::Garden - code specific to Bexley WasteWorks GGW + +=cut + +package FixMyStreet::Cobrand::Bexley::Garden; + +use Moo::Role; +with 'FixMyStreet::Roles::Cobrand::SCP', + 'FixMyStreet::Roles::Cobrand::Paye'; + +sub garden_service_name { 'garden waste collection service' } + +# TODO No current subscription look up here +# +sub garden_current_subscription { } + +=item * You can order a maximum of five bins + +=cut + +sub waste_garden_maximum { 5 } + +=item * Garden waste has different price for the first bin + +=cut + +around garden_waste_cost_pa => sub { + my ($orig, $self, $bin_count) = @_; + $bin_count ||= 1; + my $per_bin_cost = $self->garden_waste_subsequent_cost_pa; + my $first_cost = $self->garden_waste_first_cost_pa; + my $cost = $per_bin_cost * ($bin_count-1) + $first_cost; + return $cost; +}; + +# XXX DD Direct Debit TODO +around garden_waste_first_cost_pa => sub { + my ($orig, $self) = @_; + return $self->_get_cost('ggw_cost_first_cc'); +}; + +around garden_waste_subsequent_cost_pa => sub { + my ($orig, $self) = @_; + return $self->_get_cost('ggw_cost_other'); +}; + +# TODO Check +sub waste_cc_payment_sale_ref { + my ($self, $p) = @_; + return "GGW" . $p->get_extra_field_value('uprn'); +} + +# TODO Check +sub waste_cc_payment_line_item_ref { + my ($self, $p) = @_; + return $p->id; +} + +1; diff --git a/t/app/controller/waste_bexley_garden.t b/t/app/controller/waste_bexley_garden.t new file mode 100644 index 00000000000..d6741244638 --- /dev/null +++ b/t/app/controller/waste_bexley_garden.t @@ -0,0 +1,380 @@ +use Test::MockModule; +use Test::MockTime qw(:all); +use FixMyStreet::TestMech; +use FixMyStreet::Script::Reports; +use List::MoreUtils qw(firstidx); + +FixMyStreet::App->log->disable('info'); +END { FixMyStreet::App->log->enable('info'); } + +my $mech = FixMyStreet::TestMech->new; + +my $body = $mech->create_body_ok(2494, 'Bexley', { cobrand => 'bexley' }); +my $user = $mech->create_user_ok('test@example.net', name => 'Normal User'); +my $staff_user = $mech->create_user_ok('staff@example.org', from_body => $body, name => 'Staff User'); +$staff_user->user_body_permissions->create({ body => $body, permission_type => 'contribute_as_anonymous_user' }); +$staff_user->user_body_permissions->create({ body => $body, permission_type => 'contribute_as_another_user' }); +$staff_user->user_body_permissions->create({ body => $body, permission_type => 'report_mark_private' }); + +sub create_contact { + my ($params, @extra) = @_; + my $contact = $mech->create_contact_ok(body => $body, %$params, group => ['Waste'], extra => { type => 'waste' }); + $contact->set_extra_fields( + { code => 'uprn', required => 1, automated => 'hidden_field' }, + { code => 'property_id', required => 1, automated => 'hidden_field' }, + @extra, + ); + $contact->update; +} + +create_contact({ category => 'Garden Subscription', email => 'garden@example.com'}, + { code => 'current_containers', required => 1, automated => 'hidden_field' }, + { code => 'new_containers', required => 1, automated => 'hidden_field' }, + { code => 'payment', required => 1, automated => 'hidden_field' }, + { code => 'payment_method', required => 1, automated => 'hidden_field' }, +); + +my $whitespace_mock = Test::MockModule->new('Integrations::Whitespace'); +sub default_mocks { + # These are overridden for some tests + $whitespace_mock->mock('GetSiteCollections', sub { }); + $whitespace_mock->mock( + 'GetCollectionByUprnAndDate', + sub { + my ( $self, $property_id, $from_date ) = @_; + return []; + } + ); + $whitespace_mock->mock( 'GetInCabLogsByUsrn', sub { }); + $whitespace_mock->mock( 'GetInCabLogsByUprn', sub { }); + $whitespace_mock->mock( 'GetSiteInfo', sub { { + AccountSiteID => 1, + AccountSiteUPRN => 10001, + Site => { + SiteShortAddress => ', 1, THE AVENUE, DA1 3NP', + SiteLatitude => 51.466707, + SiteLongitude => 0.181108, + }, + } }); + $whitespace_mock->mock( 'GetAccountSiteID', sub {}); + $whitespace_mock->mock( 'GetSiteWorksheets', sub {}); + $whitespace_mock->mock( 'GetWorksheetDetailServiceItems', sub { }); +}; + +default_mocks(); + +FixMyStreet::override_config { + ALLOWED_COBRANDS => 'bexley', + MAPIT_URL => 'http://mapit.uk/', + COBRAND_FEATURES => { + waste => { bexley => 1 }, + whitespace => { bexley => { + url => 'https://example.net/', + } }, + payment_gateway => { bexley => { + ggw_cost_first_cc => 7500, + ggw_cost_first_dd => 7000, + ggw_cost_other => 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 { + my $sent_params = {}; + my $call_params = {}; + + my $pay = Test::MockModule->new('Integrations::SCP'); + $pay->mock(call => sub { + my $self = shift; + my $method = shift; + $call_params = { @_ }; + }); + $pay->mock(pay => sub { + my $self = shift; + $sent_params = shift; + $pay->original('pay')->($self, $sent_params); + return { + transactionState => 'IN_PROGRESS', + scpReference => '12345', + invokeResult => { + status => 'SUCCESS', + redirectUrl => 'http://example.org/faq' + } + }; + }); + $pay->mock(query => sub { + my $self = shift; + $sent_params = shift; + return { + transactionState => 'COMPLETE', + paymentResult => { + status => 'SUCCESS', + paymentDetails => { + paymentHeader => { + uniqueTranId => 54321 + } + } + } + }; + }); + + my $paye = Test::MockModule->new('Integrations::Paye'); + $paye->mock(call => sub { + my $self = shift; + my $method = shift; + $call_params = { @_ }; + }); + $paye->mock(pay => sub { + my $self = shift; + $sent_params = shift; + $paye->original('pay')->($self, $sent_params); + return { + transactionState => 'InProgress', + apnReference => '4ab5f886-de7d-4f5b-bbd8-42151a5deb82', + requestId => '21355', + invokeResult => { + status => 'Success', + redirectUrl => 'http://paye.example.org/faq', + } + } + }); + $paye->mock(query => sub { + my $self = shift; + $sent_params = shift; + return { + transactionState => 'Complete', + paymentResult => { + status => 'Success', + paymentDetails => { + authDetails => { + authCode => 'authCode', + uniqueAuthId => 54321, + }, + payments => { + paymentSummary => { + continuousAuditNumber => 'CAN', + } + } + } + } + }; + }); + + subtest 'check subscription link present' => sub { + $mech->get_ok('/waste/10001'); + $mech->content_contains('Subscribe to garden waste collection service', 'Subscribe link present if expired'); + }; + + subtest 'check new sub bin limits' => sub { + $mech->get_ok('/waste/10001/garden'); + $mech->submit_form_ok({ form_number => 1 }); + $mech->submit_form_ok({ with_fields => { existing => 'yes' } }); + $mech->content_contains('Please specify how many bins you already have'); + $mech->submit_form_ok({ with_fields => { existing => 'no' } }); + my $form = $mech->form_with_fields( qw(current_bins bins_wanted payment_method) ); + ok $form, "form found"; + is $mech->value('current_bins'), 0, "current bins is set to 0"; + + $mech->get_ok('/waste/10001/garden'); + $mech->submit_form_ok({ form_number => 1 }); + $mech->submit_form_ok({ with_fields => { existing => 'yes', existing_number => 1 } }); + $form = $mech->form_with_fields( qw(current_bins bins_wanted payment_method) ); + ok $form, "form found"; + $mech->content_like(qr#Total to pay now: £]*>75.00#, "initial cost set correctly"); + is $mech->value('current_bins'), 1, "current bins is set to 1"; + }; + + subtest 'check new sub credit card payment' => sub { + my $test = { + month => '01', + pounds_cost => '130.00', + pence_cost => '13000' + }; + set_fixed_time("2021-$test->{month}-09T17:00:00Z"); + $mech->get_ok('/waste/10001/garden'); + $mech->submit_form_ok({ form_number => 1 }); + $mech->submit_form_ok({ with_fields => { existing => 'no' } }); + $mech->content_like(qr#Total to pay now: £]*>0.00#, "initial cost set to zero"); + $mech->submit_form_ok({ with_fields => { + current_bins => 0, + bins_wanted => 2, + payment_method => 'credit_card', + name => 'Test McTest', + email => 'test@example.net' + } }); + $mech->content_contains('Test McTest'); + $mech->content_contains('£' . $test->{pounds_cost}); + $mech->content_contains('2 bins'); + $mech->submit_form_ok({ with_fields => { goto => 'details' } }); + $mech->content_contains('' . $test->{pounds_cost}); + $mech->content_contains('' . $test->{pounds_cost}); + $mech->submit_form_ok({ with_fields => { + current_bins => 0, + bins_wanted => 2, + payment_method => 'credit_card', + name => 'Test McTest', + email => 'test@example.net' + } }); + $mech->content_contains('Continue to payment', 'Waste features text_for_waste_payment not used for non-staff payment'); + $mech->waste_submit_check({ with_fields => { tandc => 1 } }); + + my ( $token, $new_report, $report_id ) = get_report_from_redirect( $sent_params->{returnUrl} ); + + is $sent_params->{items}[0]{amount}, $test->{pence_cost}, 'correct amount used'; + check_extra_data_pre_confirm($new_report, new_bin_type => 1, new_quantity => 2); + + $mech->get_ok("/waste/pay_complete/$report_id/$token?STATUS=9&PAYID=54321"); + + check_extra_data_post_confirm($new_report); + + $mech->content_like(qr#/waste/10001">Show upcoming#, "contains link to bin page"); + + FixMyStreet::Script::Reports::send(); + my @emails = $mech->get_email; + my $body = $mech->get_text_body_from_email($emails[1]); + TODO: { + local $TODO = 'Quantity not yet read in _garden_data.html'; + like $body, qr/Number of bin subscriptions: 2/; + } + like $body, qr/Bins to be delivered: 2/; + like $body, qr/Total:.*?$test->{pounds_cost}/; + $mech->clear_emails_ok; + }; + + set_fixed_time('2023-01-09T17:00:00Z'); # Set a date when garden service full price for most tests + + subtest 'check new sub credit card payment with no bins required' => sub { + $mech->get_ok('/waste/10001/garden'); + $mech->submit_form_ok({ form_number => 1 }); + $mech->submit_form_ok({ with_fields => { existing => 'no' } }); + $mech->submit_form_ok({ with_fields => { + current_bins => 1, + bins_wanted => 1, + payment_method => 'credit_card', + name => 'Test McTest', + email => 'test@example.net' + } }); + $mech->content_contains('Test McTest'); + $mech->content_contains('£75.00'); + $mech->waste_submit_check({ with_fields => { tandc => 1 } }); + + my ( $token, $new_report, $report_id ) = get_report_from_redirect( $sent_params->{returnUrl} ); + + is $sent_params->{items}[0]{amount}, 7500, 'correct amount used'; + check_extra_data_pre_confirm($new_report, new_bins => 0); + + $mech->get_ok("/waste/pay_complete/$report_id/$token?STATUS=9&PAYID=54321"); + check_extra_data_post_confirm($new_report); + + $mech->clear_emails_ok; + FixMyStreet::Script::Reports::send(); + my @emails = $mech->get_email; + my $body = $mech->get_text_body_from_email($emails[1]); + TODO: { + local $TODO = 'Quantity not yet read in _garden_data.html'; + like $body, qr/Number of bin subscriptions: 1/; + } + unlike $body, qr/Bins to be delivered/; + like $body, qr/Total:.*?75.00/; + }; + + subtest 'check new sub credit card payment with one less bin required' => sub { + $mech->get_ok('/waste/10001/garden'); + $mech->submit_form_ok({ form_number => 1 }); + $mech->submit_form_ok({ with_fields => { existing => 'no' } }); + $mech->submit_form_ok({ with_fields => { + current_bins => 2, + bins_wanted => 1, + payment_method => 'credit_card', + name => 'Test McTest', + email => 'test@example.net' + } }); + $mech->content_contains('Test McTest'); + $mech->content_contains('£75.00'); + $mech->waste_submit_check({ with_fields => { tandc => 1 } }); + + my ( $token, $new_report, $report_id ) = get_report_from_redirect( $sent_params->{returnUrl} ); + + is $sent_params->{items}[0]{amount}, 7500, 'correct amount used'; + check_extra_data_pre_confirm($new_report, new_bins => 0); + + $mech->get_ok("/waste/pay_complete/$report_id/$token?STATUS=9&PAYID=54321"); + check_extra_data_post_confirm($new_report); + + $mech->clear_emails_ok; + FixMyStreet::Script::Reports::send(); + my @emails = $mech->get_email; + my $body = $mech->get_text_body_from_email($emails[1]); + TODO: { + local $TODO = 'Quantity not yet read in _garden_data.html'; + like $body, qr/Number of bin subscriptions: 1/; + } + like $body, qr/Bins to be removed: 1/; + like $body, qr/Total:.*?75.00/; + }; +}; + +sub get_report_from_redirect { + my $url = shift; + + my ($report_id, $token) = ( $url =~ m#/(\d+)/([^/]+)$# ); + my $new_report = FixMyStreet::DB->resultset('Problem')->find( { + id => $report_id, + }); + + return undef unless $new_report->get_extra_metadata('redirect_id') eq $token; + return ($token, $new_report, $report_id); +} + +sub check_extra_data_pre_confirm { + my $report = shift; + my %params = ( + type => 'New', + state => 'unconfirmed', + quantity => 1, + new_bins => 1, + action => 1, + bin_type => 1, + payment_method => 'credit_card', + new_quantity => '', + new_bin_type => '', + ref_type => 'scp', + category => 'Garden Subscription', + @_ + ); + $report->discard_changes; + is $report->category, $params{category}, 'correct category on report'; + is $report->title, "Garden Subscription - $params{type}", 'correct title on report'; + is $report->get_extra_field_value('payment_method'), $params{payment_method}, 'correct payment method on report'; + TODO: { + local $TODO = 'Fields (not these values) not yet set'; + is $report->get_extra_field_value('Paid_Collection_Container_Quantity'), $params{quantity}, 'correct bin count'; + is $report->get_extra_field_value('Paid_Collection_Container_Type'), $params{bin_type}, 'correct bin type'; + is $report->get_extra_field_value('Container_Quantity'), $params{new_quantity}, 'correct bin count'; + is $report->get_extra_field_value('Container_Type'), $params{new_bin_type}, 'correct bin type'; + } + is $report->state, $params{state}, 'report state correct'; + if ($params{state} eq 'unconfirmed') { + if ($params{ref_type} eq 'apn') { + is $report->get_extra_metadata('apnReference'), '4ab5f886-de7d-4f5b-bbd8-42151a5deb82', 'correct scp reference on report'; + } else { + is $report->get_extra_metadata('scpReference'), '12345', 'correct scp reference on report'; + } + } +} + +sub check_extra_data_post_confirm { + my $report = shift; + $report->discard_changes; + is $report->state, 'confirmed', 'report confirmed'; + is $report->get_extra_field_value('LastPayMethod'), 2, 'correct echo payment method field'; + is $report->get_extra_field_value('PaymentCode'), '54321', 'correct echo payment reference field'; + is $report->get_extra_metadata('payment_reference'), '54321', 'correct payment reference on report'; +} + +done_testing;