From 4d2cc2fd93434602e2b24fffc655c5481a63ade4 Mon Sep 17 00:00:00 2001 From: Jess Robinson Date: Sun, 7 Dec 2025 13:22:14 +0000 Subject: [PATCH 1/6] WIP: Donor tier added, membership form updated --- cpanfile | 5 +- lib/AccessSystem/Form/Person.pm | 109 ++++----- lib/AccessSystem/Schema/Result/Allowed.pm | 3 +- lib/AccessSystem/Schema/Result/Person.pm | 92 ++++++- lib/AccessSystem/Schema/ResultSet/Person.pm | 21 +- root/src/forms/person.tt | 8 +- root/src/main.js | 5 + t/ResultSetPerson.t | 251 +++++++++++++++++--- 8 files changed, 383 insertions(+), 111 deletions(-) diff --git a/cpanfile b/cpanfile index af672bb..33484bc 100644 --- a/cpanfile +++ b/cpanfile @@ -29,7 +29,7 @@ requires 'JSON'; requires 'MIME::Base64'; requires 'CatalystX::AuthenCookie'; requires 'Catalyst::Plugin::StackTrace'; -requires 'DBD::Pg'; +requires 'DBD::Pg', 3.18.0; requires 'DateTime::Format::Pg'; requires 'List::Util'; requires 'Mojo::Base'; @@ -44,3 +44,6 @@ requires 'Email::Sender::Transport::SMTP'; requires 'DBIx::Class::EasyFixture'; requires 'Time::HiRes'; requires 'Date::Holidays::GB'; +requires 'Test::More'; +requires 'Test::Exception'; +requires 'DBIx::Class::InflateColumn::Boolean'; diff --git a/lib/AccessSystem/Form/Person.pm b/lib/AccessSystem/Form/Person.pm index bd88a0c..12bfe92 100644 --- a/lib/AccessSystem/Form/Person.pm +++ b/lib/AccessSystem/Form/Person.pm @@ -146,7 +146,35 @@ has_field email => field_add_defaults { required => 'Please enter an email we can use to contact you', }, unique_message => "That email address is already registered, did you do submit twice?", - help_string => 'An email address we can contact you on. Your membership payment details will be sent to it.', + help_string => 'An email address we can contact you on. Your membership/donor payment details will be sent to it.', +}; + +has_field address => field_add_defaults { + type => 'TextArea', + label => 'Address *', + required => 1, + rows => 6, + maxlength => 1024, + wrapper_attr => { id => 'field-address', }, + tags => { no_errors => 1 }, + messages => { + required => 'Please enter your full street address', + }, + help_string => 'As it would appear on an envelope, required by the gov for the membership register.', +}; + +# Display 3 tier choices as big buttons? +has_field tier => field_add_defaults { + type => 'Select', + required => 1, + active_column => 'in_use', + label_column => 'display_name_and_price', + widget => 'RadioGroup', + messages => { + required => 'Please choose a payment tier', + }, + wrapper_attr => { id => 'field-tier', class => 'payment' }, + help_string => 'Not resident in Swindon, and a member of a space elsewhere?
Access weekends and Wednesday nights only
Access 24/7 365 days a year
Full access, sponsoring member
Send donations only, not a member of the company', }; has_field opt_in => field_add_defaults { @@ -175,7 +203,7 @@ has_field dob => field_add_defaults { end_date => DateTime->now->clone()->subtract(years => 17)->ymd, start_date => DateTime->now->clone()->subtract(years => 120)->ymd, required => 1, - wrapper_attr => { id => 'field-dob', class => 'payment' }, + wrapper_attr => { id => 'field-dob', class => 'payment donor_hide' }, tags => { no_errors => 1 }, messages => { required => 'Please enter your date of birth', @@ -184,39 +212,19 @@ has_field dob => field_add_defaults { help_string => 'YYYY-MM, only Year/Month accuracy is required. We use this to ensure that you are old enough to be an adult member, or if you are eligible for senior (65) concessions.', }; -has_field address => field_add_defaults { - type => 'TextArea', - label => 'Address *', - required => 1, - rows => 6, - maxlength => 1024, - wrapper_attr => { id => 'field-address', }, - tags => { no_errors => 1 }, - messages => { - required => 'Please enter your full street address', - }, - help_string => 'As it would appear on an envelope, for the membership register.', -}; - -# has_field 'vehicles' => field_add_defaults { -# type => 'Repeatable', -# }; - # has_field 'vehicles.plate_reg' => field_add_defaults { has_field 'plate_reg' => field_add_defaults { type => 'Text', label => 'Car Reg Plate', required => 0, maxlength => 7, - wrapper_attr => { id => 'field-plate-reg', }, + wrapper_attr => { id => 'field-plate-reg', class => 'donor_hide'}, tags => { no_errors => 1 }, messages => { required => 'Please enter your car licence plate string (max 7 chars)', }, - help_string => 'The car park has number plate recognition, enter your plate for us to automatically add you to the system. If not entered you will need to manually check in every visit.', + help_string => '(Optional) Car park has no parking rules but this may change (again).', }; -# has_field 'vehicles.person' => ( type => 'Hidden' ); -# has_field 'vehicles.person_id' => ( type => 'PrimaryKey' ); has_field how_found_us => field_add_defaults { type => 'Select', @@ -240,6 +248,7 @@ has_field associated_button => ( type => 'Button', value => 'Change/Show Associated Accounts', label => '', + wrapper_attr => { class => 'donor_hide' }, element_attr => { style => 'background-color: #eabf83;' }, ); @@ -255,17 +264,17 @@ has_field github_user => field_add_defaults { help_string => '(Optional) A github username, this will allow us to give you access to our code repositories and wiki.', }; -has_field telegram_username => field_add_defaults { - type => 'Text', - required => 0, - maxlength => 255, - wrapper_attr => { id => 'field-telegram-username', class => 'associated associated_hide', style => "display:none"}, - tags => { no_errors => 1 }, - messages => { - required => 'Please enter a telegram screen name', - }, - help_string => '(Optional) Your Telegram username, used to tag you by a bot (for inductions etc)', -}; +# has_field telegram_username => field_add_defaults { +# type => 'Text', +# required => 0, +# maxlength => 255, +# wrapper_attr => { id => 'field-telegram-username', class => 'associated associated_hide', style => "display:none"}, +# tags => { no_errors => 1 }, +# messages => { +# required => 'Please enter a telegram screen name', +# }, +# help_string => '(Optional) Your Telegram username, used to tag you by a bot (for inductions etc)', +# }; has_field google_id => field_add_defaults { type => 'Text', @@ -279,24 +288,11 @@ has_field google_id => field_add_defaults { help_string => '(Optional) Your google account email address, (even if the same as your usual email address) - this will be used for access to our Google Drive documents.', }; -# Display 3 tier choices as big buttons? -has_field tier => field_add_defaults { - type => 'Select', - required => 1, - active_column => 'in_use', - label_column => 'display_name_and_price', - widget => 'RadioGroup', - messages => { - required => 'Please choose a payment tier', - }, - wrapper_attr => { id => 'field-tier', class => 'payment' }, - help_string => 'Not resident in Swindon, and a member of a space elsewhere?
Access weekends and Wednesday nights only
Access 24/7 365 days a year
Full access, sponsoring member', -}; - has_field payment_button => ( type => 'Button', value => 'Change/Show More Payment Options', label => '', + wrapper_attr => { class => 'donor_hide' }, element_attr => { style => 'background-color: #eabf83;' }, ); @@ -321,15 +317,6 @@ has_field concessionary_rate_override => field_add_defaults { help_string => 'Do you qualify for our reduced payment rate? Choose what best matches your situation, please show documentation to a director to prove your status.', }; -# has_field member_of_other_hackspace => field_add_defaults { -# type => 'Checkbox', -# required => 0, -# wrapper_attr => { id => 'field-member-of-other-hackspace', class => 'payment payment_hide', style => "display:none" }, -# tags => { no_errors => 1}, -# label => 'I am mainly a member of another hackspace/makerspace', -# help_string => 'Just visiting or only in Swindon part of the year? If you are a member of another hackspace somewhere, you can join us for only £5/month.', -# }; - has_field payment_override => field_add_defaults { type => 'Money', # currency_symbol => '£', @@ -348,6 +335,7 @@ has_field door_colour => field_add_defaults { required => 0, label => 'Entry Colour', default => 'green', + wrapper_attr => { class => 'donor_hide' }, options => [ { value => 'green', label => 'Green', attributes => { class => 'door_colour' }, checked => 1 }, { value => 'white', label => 'White', attributes => { class => 'door_colour' } }, @@ -364,6 +352,7 @@ has_field door_colour => field_add_defaults { has_field voucher_code => field_add_defaults { type => 'Text', required => 0, + wrapper_attr => { class => 'donor_hide' }, label => 'Voucher Code', wrapper_attr => { id => 'field-voucher-code', class => 'payment_hide', style => "display:none" }, help_string => 'If you have a voucher, enter the code, or location you got it, here.', @@ -374,7 +363,7 @@ has_field membership_guide => field_add_defaults { type => 'Checkbox', required => 1, label => 'I have read and agree to comply with the Membership Guide *', - wrapper_attr => { id => 'field-membership-guide', }, + wrapper_attr => { id => 'field-membership-guide', class => 'donor_hide'}, tags => { no_errors => 1 }, messages => { required => 'Please read and agree to the Membership Guide', @@ -386,7 +375,7 @@ has_field health_and_safety => field_add_defaults { type => 'Checkbox', required => 1, label => 'I have read and agree to comply with the Health & Safety Policy *', - wrapper_attr => { id => 'field-health-safety', }, + wrapper_attr => { id => 'field-health-safety', class => 'donor_hide' }, tags => { no_errors => 1 }, messages => { required => 'Please read and agree to the Health & Safety Policy', diff --git a/lib/AccessSystem/Schema/Result/Allowed.pm b/lib/AccessSystem/Schema/Result/Allowed.pm index fe704dc..7acc781 100644 --- a/lib/AccessSystem/Schema/Result/Allowed.pm +++ b/lib/AccessSystem/Schema/Result/Allowed.pm @@ -5,7 +5,7 @@ use warnings; use base 'DBIx::Class::Core'; -__PACKAGE__->load_components(qw/InflateColumn::DateTime TimeStamp UUIDColumns/); +__PACKAGE__->load_components(qw/InflateColumn::DateTime TimeStamp UUIDColumns InflateColumn::Boolean/); __PACKAGE__->table('allowed'); @@ -28,6 +28,7 @@ __PACKAGE__->add_columns( pending_acceptance => { data_type => 'boolean', default_value => 'true', + is_boolean => 1, }, accepted_on => { data_type => 'datetime', diff --git a/lib/AccessSystem/Schema/Result/Person.pm b/lib/AccessSystem/Schema/Result/Person.pm index 9c27c1a..2ee95b7 100644 --- a/lib/AccessSystem/Schema/Result/Person.pm +++ b/lib/AccessSystem/Schema/Result/Person.pm @@ -248,6 +248,8 @@ sub update { sub is_valid { my ($self, $date) = @_; + return 0 if($self->tier->name =~ /donation/i); + $date = DateTime->now(); my $dtf = $self->result_source->schema->storage->datetime_parser; @@ -297,6 +299,10 @@ sub normal_dues { return 0 if $self->parent; + if ($self->tier->name =~ /donation/i) { + return 0; + } + my $dues = $self->tier ? $self->tier->price : 2500; if($self->tier && $self->tier->concessions_allowed && $self->concessionary_rate) { @@ -488,13 +494,13 @@ sub create_payment { # if no valid date yet, but we now make a payment (first ever), # email member to notify them + my $now = DateTime->now; my $valid_date = $self->valid_until; - if($valid_date && $valid_date->clone->subtract(days => $OVERLAP_DAYS) > DateTime->now) { + if($valid_date && $valid_date->clone->subtract(days => $OVERLAP_DAYS) > $now) { warn "Member " . $self->bank_ref . " not about to expire.\n"; return 1; } - my $now = DateTime->now; # Figure out what sort of payment this is, if valid_until is # empty, then its a first payment or renewal payment - use the # payment date. @@ -505,15 +511,72 @@ sub create_payment { # first payment ever # this is the start of their voucher if($self->voucher_code) { - $self->update({ voucher_start => DateTime->now()}); + $self->update({ voucher_start => $now}); } } # work this out after voucher setting cos it changes the dues - if($self->balance_p < $self->dues) { + + # And so does this bit! + # If Donor tier and balance is now enough and only donor-ified after 1 month ago - revert + # (one month else they'll keep flipping in/out of donor) + if($self->is_donor) { + my $old_data = $self->confirmations->find({ token => 'old_tier'}); + my $when = $dt_parser->parse_datetime($old_data->storage->{changed_on}); + my $old_tier = $old_data->storage->{tier_id}; + my $one_month_ago = $now->clone->subtract(months => 1); + if($when > $one_month_ago) { + if($current_bal >= $schema->resultset('Person')->get_dummy_dues( + $old_tier, + $self->dob(), + $self->concessionary_rate_override(), + $old_data->storage->{fees} + )) { + # Topped up, undo donorification: + $self->update({ tier_id => $old_tier }); + # set payment amount to min tier fee: + $self->update({ payment_override => $self->normal_dues }); + } + } + } + + if($current_bal < $self->dues) { + # Expanded for fees update 2025: + # If previously valid, and payment (intention) is less than dues (which have changed!) + # and actually expired (mid overlap), convert to donor-tier, fees to 0, store old tier/fees + # this should happens before the reminder_email (which is not sent to donors) + if($valid_date + && $valid_date->clone()->subtract(days => int($OVERLAP_DAYS / 2)) < $now + && $current_bal > 0 + && $self->payment_override < $self->normal_dues + && !$self->is_donor) { + my $old_tier_id = $self->tier_id; + my $old_fees = $self->payment_override; + my $min_dues = $self->dues; + my $token = 'old_tier'; # findable! + $schema->txn_do(sub { + my $confirm = $self->confirmations->create({ + token => $token, + storage => { + tier_id => $old_tier_id, + fees => $old_fees, + changed_on => $dt_parser->format_datetime($now), + } + }); + $self->update({ + tier_id => 6, + payment_override => 0, + }); + # Send an email (force renew): + $self->create_communication('Your Swindon Makerspace membership is now Donor Only', 'move_to_donation_tier', { current_balance => $current_bal, min_dues => $min_dues}, 1); + }); + warn "Member " . $self->bank_ref . " not covered fees, converted to Donor Tier"; + return; + } + # Normal fail - intended to pay enough for tier but.. didnt? (Presumably stopped paying) warn "Member " . $self->bank_ref . " balance not enough for another month.\n"; # Remind when actually expired, 5 days into the overlap - if($valid_date && - $valid_date->clone()->subtract(days => 5) < $now + if($valid_date && !$self->is_donor + && $valid_date->clone()->subtract(days => 5) < $now && $valid_date >= $now->clone()->subtract(months => 1)) { # has (or is about to) expire # this will only send once! @@ -551,6 +614,10 @@ sub create_payment { my $r_email = $self->communications_rs->find({type => 'reminder_email'}); $r_email->delete if $r_email; } + + # Donor tier members should not make "payments" + return if $self->is_donor; + # Only add $OVERLAP extra days if a first or renewal payment - these # ensure member remains valid if standing order is not an # exact month due to weekends and bank holidays @@ -708,6 +775,19 @@ sub create_induction_email { return ($comm, $confirm); } +sub send_membership_fee_warning { + my ($self) = @_; + + my $last_payment = $self->last_payment; + if ($last_payment && $last_payment->amount_p < $self->dues) { + my $last_warning = $self->create_communication('Makerspace Membership Fee changed, please update your payment', 'membership_fees_change', { last_pay => $last_payment }); + if($last_warning->created_on > DateTime->now->subtract(minutes => 10)) { + return 1; + } + } + return 0; +} + sub create_communication { my ($self, $subject, $type, $tt_vars, $force) = @_; $force ||= 0; diff --git a/lib/AccessSystem/Schema/ResultSet/Person.pm b/lib/AccessSystem/Schema/ResultSet/Person.pm index b3e1e02..d4a10cf 100644 --- a/lib/AccessSystem/Schema/ResultSet/Person.pm +++ b/lib/AccessSystem/Schema/ResultSet/Person.pm @@ -148,12 +148,29 @@ sub allowed_to_thing { } } if($deps_ok) { + # Jan 2025 - new membrership fees - email if underpaying + my $beep = 0; + if ($person->send_membership_fee_warning()) { + $beep = 1; + } return { - person => $person, - thing => $person->allowed->first->tool, + person => $person, + beep => $beep, + message => 'Fees have changed. Check email.', + thing => $person->allowed->first->tool, }; } } else { + if ($person->is_donor) { + # Fetch stored old-tier info + # force send ? + $person->create_communication('Makerspace upgrade Donor Tier to Membership', 'donation_access_denied', {}, 1); + return { + person => $person, + error => "No access for donors", + colour => 0x22, + }; + } return { error => "Membership expired/unpaid", colour => 0x22, diff --git a/root/src/forms/person.tt b/root/src/forms/person.tt index 848f432..56c4571 100644 --- a/root/src/forms/person.tt +++ b/root/src/forms/person.tt @@ -1,19 +1,19 @@ [% WRAPPER wrapper.tt %] [% IF !parent %] -

Sign up as a Swindon Makerspace member

+

Sign up as a Swindon Makerspace member or donor

[% ELSE %]

Add child for [% parent.name %]

[% END %]

- Swindon Makerspace members pay a monthly fee (concessions available) to access the Makerspace facility 24 hours a day, 7 days a week. Only paid-up members may use the regulated equipment (after a short induction). + Swindon Makerspace members pay a monthly fee (concessions available) to access the Makerspace facility 24 hours a day, 7 days a week. Only paid-up members may use the regulated equipment (after a short induction). Donors (Donation Only) are explicitly not members, they do not get a vote or access to the facility.

- Paying Members must be adults (18+), adults may bring supervised children at no further cost. + Paying Members/Donors must be adults (18+), adults may bring supervised children at no further cost.

Consult our Privacy Policy for further detail on how your data is used.

-

Full details available in the Membership Guide +

Full details of Membership available in the Membership Guide

[% form.render() %] [% END %] diff --git a/root/src/main.js b/root/src/main.js index 5a57361..a07bc12 100644 --- a/root/src/main.js +++ b/root/src/main.js @@ -34,6 +34,11 @@ if (jQuery('#tier\\.3').is(':checked')) { jQuery('.door_colour').prop('disabled', false); } else { + if (jQuery('#tier\\.6').is(':checked')) { + jQuery('.donor_hide').hide(); + } else { + jQuery('.donor_hide').show(); + } jQuery('.door_colour').prop('checked', false); jQuery('#door_colour\\.0').prop('checked', true); jQuery('.door_colour').prop('disabled', true); diff --git a/t/ResultSetPerson.t b/t/ResultSetPerson.t index 069941c..198b5ce 100644 --- a/t/ResultSetPerson.t +++ b/t/ResultSetPerson.t @@ -4,6 +4,7 @@ use strict; use warnings; use Test::More; +use Test::Exception; use DateTime; use AccessSystem::Schema; @@ -11,49 +12,225 @@ use AccessSystem::Schema; use lib 't/lib'; use AccessSystem::Fixtures; +my $overlap_days = 14; +$ENV{CATALYST_HOME} = '.'; + my $testdb="test_db.db"; unlink $testdb if(-e $testdb); my $schema = AccessSystem::Schema->connect("dbi:SQLite:$testdb"); -$schema->deploy(); -# my $fixtures = AccessSystem::Fixtures->new( { schema => $schema } ); -# $fixtures->load('standard_member'); - -# tiers: -AccessSystem::Fixtures::create_tiers($schema); - -# Fobs + acccess -my $test9 = AccessSystem::Fixtures::create_person($schema); - -my $no_token = $schema->resultset('Person')->allowed_to_thing('12345678', 'blahblahblah'); -like($no_token->{error}, qr/not recognised/, 'No such member with that token'); -$test9->create_related('tokens', { id => '12345678', type => 'test token' }); - -my $no_thing = $schema->resultset('Person')->allowed_to_thing('12345678', 'blahblahblah'); -like($no_thing->{error}, qr/not recognised/, 'No such missing thing'); - -my $thing = $schema->resultset('Tool')->create({ name => 'test thing', assigned_ip => '10.0.0.1', requires_induction => 1, team => 'Who knows' }); - -my $no_allowed = $schema->resultset('Person')->allowed_to_thing('12345678', $thing->id); -like($no_allowed->{error}, qr/not inducted/, 'Person cannot use the thing'); - -$test9->create_related('allowed', { tool => $thing, is_admin => 0 }); - -my $no_pay = $schema->resultset('Person')->allowed_to_thing('12345678', $thing->id); -like($no_pay->{error}, qr/Pay up please/, 'Member hasnt paid'); - -$test9->create_related('payments', { paid_on_date => DateTime->now, expires_on_date => DateTime->now->add(months => 1, days => 14), amount_p => $test9->dues }); - -my $no_confirm = $schema->resultset('Person')->allowed_to_thing('12345678', $thing->id); -like($no_confirm->{error}, qr/Induction not confirmed/, 'Member hasnt confirmed induction'); - -my $allowed = $test9->allowed->find({tool_id => $thing->id }); -$allowed->update({ pending_acceptance => 'false' }); -my $good = $schema->resultset('Person')->allowed_to_thing('12345678', $thing->id); -ok(!$good->{error}, 'Allowed to use thing now'); +{ + $| = 1; + + $schema->deploy(); + # my $fixtures = AccessSystem::Fixtures->new( { schema => $schema } ); + # $fixtures->load('standard_member'); + + # tiers: + AccessSystem::Fixtures::create_tiers($schema); + + ## Fobs + acccess, verify normal access with person, token, tool + induction + my $test9 = AccessSystem::Fixtures::create_person($schema); + + my $no_token = $schema->resultset('Person')->allowed_to_thing('12345678', 'blahblahblah'); + like($no_token->{error}, qr/not recognised/, 'No such member with that token'); + $test9->create_related('tokens', { id => '12345678', type => 'test token' }); + + my $no_thing = $schema->resultset('Person')->allowed_to_thing('12345678', 'blahblahblah'); + like($no_thing->{error}, qr/not recognised/, 'No such missing thing'); + + my $thing = $schema->resultset('Tool')->create({ name => 'test thing', assigned_ip => '10.0.0.1', requires_induction => 1, team => 'Who knows' }); + + my $no_allowed = $schema->resultset('Person')->allowed_to_thing('12345678', $thing->id); + like($no_allowed->{error}, qr{accepted/inducted}, 'Person cannot use the thing'); + + $test9->create_related('allowed', { tool => $thing, is_admin => 0 }); + + my $no_pay = $schema->resultset('Person')->allowed_to_thing('12345678', $thing->id); + like($no_pay->{error}, qr/Pay up please/, 'Member hasnt paid'); + + $test9->create_related('payments', { paid_on_date => DateTime->now, expires_on_date => DateTime->now->add(months => 1, days => 14), amount_p => $test9->dues }); + + my $no_confirm = $schema->resultset('Person')->allowed_to_thing('12345678', $thing->id); + like($no_confirm->{error}, qr/Induction not confirmed/, 'Member hasnt confirmed induction'); + + my $allowed = $test9->allowed->find({tool_id => $thing->id }); + $allowed->update({ pending_acceptance => 0 }); + + my $good = $schema->resultset('Person')->allowed_to_thing('12345678', $thing->id); + ok(!$good->{error}, 'Allowed to use thing now'); + + unlink($testdb); +} + +{ + $| = 1; + + ## Test fees - Person, with tier, with matching payment override, create transaction, create payment/fees + + # Deploy fresh DB: + $schema->deploy(); + + # tiers: + AccessSystem::Fixtures::create_tiers($schema); + + ## sends correct emails? + # standard tier (3), cost 2500 + # "thing" to test access against + my $payment_amount = 2500; + my $comms_count = 0; + my $testee = AccessSystem::Fixtures::create_person($schema, payment => $payment_amount); + $testee->create_related('tokens', { id => '12345678', type => 'test token' }); + # The Door so that Result::Person::update_door_access works + my $thing = $schema->resultset('Tool')->create({ name => 'The Door', assigned_ip => '10.0.0.1', requires_induction => 1, team => 'Who knows' }); + my $allowed = $testee->create_related('allowed', { tool => $thing, is_admin => 0}); + $allowed->discard_changes(); + $allowed->update({ pending_acceptance => 0 }); + $allowed->discard_changes(); + + # simulate (under)payment by creating a transaction for payment amount + my $now = DateTime->now(); + $testee->import_transaction( + { + dtposted => $now->clone->subtract(hours => 5), + trnamt => ($payment_amount - 500) / 100, + }); + is($testee->balance_p, $payment_amount - 500, 'Imported underpay transaction'); + # (no emails, does not create a payment) + lives_ok(sub { $testee->create_payment($overlap_days); }, 'Attempted create_payment without dying'); + is($testee->balance_p, $payment_amount - 500, 'Balance remains the same'); + is($testee->communications_rs->count, $comms_count, 'No communications yet'); + + # simulate corrected payment by adding a transaction that makes it up to correct amount + $testee->import_transaction( + { + dtposted => $now->clone->subtract(hours => 4), + trnamt => 500 / 100, + }); + lives_ok( sub { $testee->create_payment($overlap_days); }, '2nd create payment, should actually create one without dying'); + is($testee->balance_p, 0, 'Balance now empty'); + + my $good = $schema->resultset('Person')->allowed_to_thing('12345678', $thing->id); + ok(!$good->{error}, 'Can access Door'); + + # (has started email, creates a payment) + is($testee->payments_rs->count, 1, 'Has a payment'); + is($testee->communications_rs->count, ++$comms_count, 'Sent a communication'); + my $comms = $testee->communications_rs->find({'type' => 'first_payment'}); + ok($comms, 'Created a first_payment email'); + + # change valid_date so that its over 5 days ago (attempt to + # resolve payments will cause a membership reminder) + my $existing_payment = $testee->payments_rs->first; + $existing_payment->update({ expires_on_date => $now->clone->subtract(days => 6)}); + # attempt to pay - should send reminder_email + # sleep cos the timestamp is part of the primary key (seconds resolution!) + sleep 1; + lives_ok( sub { $testee->create_payment($overlap_days); }, 'Run create payment with an expir(ing) member without dying'); + is($testee->communications_rs->count, ++$comms_count, 'Sent a communication'); + $comms = $testee->communications_rs->find({'type' => 'reminder_email'}); + ok($comms, 'Created a reminder_email email'); + + # simulate full transaction, attempt to pay, should send 'rejoined' email and remove 'reminder_email' + $testee->import_transaction( + { + dtposted => $now->clone->subtract(hours => 3), + trnamt => 2500 / 100, + }); + sleep 1; + lives_ok( sub { $testee->create_payment($overlap_days); }, 'Run create payment with normal payment without dying'); + is($testee->communications_rs->count, $comms_count, 'Same communication count (deleted one)'); + $comms = $testee->communications_rs->find({'type' => 'rejoin_payment'}); + ok($comms, 'Created a rejoin_payment email'); + + ## Tier fees change, test what happens! + # update testee to have a 20% higher fee: + # store current tier id: + my $old_tier_id = $testee->tier_id; + my $tier_fee = $testee->tier->price; + $testee->tier->update({ price => $tier_fee * 1.2 }); + ok($testee->tier->price > $tier_fee, 'Tier is now a higher fee'); + $payment_amount = $testee->payment_override; + diag("Payment Amount: $payment_amount"); + ok($payment_amount < $testee->tier->price, 'Member thinks they are paying less, still'); + ok($testee->dues > $payment_amount, 'System disagrees (lower payment_override ignored)'); + is($testee->balance_p, 0, 'Should start at 0 to do this test'); + + # reset expiry dates + $existing_payment = $testee->last_payment; + # Expires in 10 days, should add new payment if available + # NB If this gets to 2 days it will also send a reminder_email? + # but not change to donor yet as not entirely expired: + $existing_payment->update({ expires_on_date => $now->clone->add(days => 10) }); + # try to access Door - should send email! (last payment was less than dues) + # sleep - comms emails also have timestamps in the PK + sleep 1; + $good = $schema->resultset('Person')->allowed_to_thing('12345678', $thing->id); + ok(!$good->{error}, 'No Door error'); + is($testee->communications_rs->count, ++$comms_count, 'Sent a communication'); + $comms = $testee->communications_rs->find({'type' => 'membership_fees_change'}); + ok($comms, 'Sent a membership_fees_change email'); + + # pay the old amount: + $testee->import_transaction( + { + dtposted => $now->clone->subtract(hours => 2), + trnamt => $payment_amount / 100, + }); + # No emails.. normal "under balance" status until actually expired + # Aka access allowed for whole of final month.. + my $payment_count = $testee->payments->count; + sleep 1; + lives_ok( sub { $testee->create_payment($overlap_days); }, 'Run create payment with old fees without dying'); + is($testee->balance_p, $payment_amount, 'Balance unused'); + is($testee->payments->count, $payment_count, 'Not made a new payment'); + is($testee->communications_rs->count, $comms_count, 'No new communication'); + $testee->discard_changes(); + is($testee->tier_id, $old_tier_id, 'Tier not changed'); + + # Now "expire" the last payment: + $existing_payment->update({ expires_on_date => $now->clone->subtract(days => 1) }); + # and attempt to make a new payment with the lower balance: + sleep 1; + lives_ok( sub { $testee->create_payment($overlap_days); }, 'Run create payment with old payment, expired member, without dying'); + # still no payment: + is($testee->balance_p, $payment_amount, 'Balance unused'); + is($testee->payments->count, $payment_count, 'Not made a new payment'); + + # and a new email: + is($testee->communications_rs->count, ++$comms_count, 'Sent a communication'); + $comms = $testee->communications_rs->find({'type' => 'move_to_donation_tier'}); + ok($comms, 'Sent a move_to_donation_tier email'); + # changed tier: + $testee->discard_changes(); + isnt($testee->tier_id, $old_tier_id, 'Changed tier id'); + # should be in confirmations: + my $conf = $testee->confirmations->find({ token => 'old_tier' }); + ok($conf, 'Stored old tier settings'); + + # top up missing amount + # old tier price: + my $old_tier = $schema->resultset('Tier')->find({ id => $old_tier_id }); + $testee->import_transaction( + { + dtposted => $now->clone->subtract(hours => 1), + trnamt => ($old_tier->price - $payment_amount) / 100, + }); + + # retry payment, should revert tier!? + sleep 1; + lives_ok( sub { $testee->create_payment($overlap_days); }, 'Run create payment with new fees without dying'); + is($testee->balance_p, 0, 'Balance empty'); + is($testee->payments->count, $payment_count+1, 'Made a new fees payment'); + is($testee->tier_id, $old_tier_id, 'Reset tier id'); + ok($testee->payment_override >= $testee->tier->price, 'Corrected payment_override'); + + # Bob's my uncle? + + unlink($testdb); +} -unlink($testdb); done_testing; From fa7e5327ccafa412b079f526ea94703a3a7ca2ce Mon Sep 17 00:00:00 2001 From: Jess Robinson Date: Sun, 14 Dec 2025 14:17:04 +0000 Subject: [PATCH 2/6] Verify and payments checking for new fees / donor tier --- docs/diagrams/bank_import_activity.png | Bin 0 -> 14859 bytes docs/diagrams/bank_import_activity.txt | 30 +++++++++ docs/diagrams/extend_membership_activity.png | Bin 0 -> 48392 bytes docs/diagrams/extend_membership_activity.txt | 63 +++++++++++++++++++ lib/AccessSystem/API/Controller/Root.pm | 15 +++-- lib/AccessSystem/Schema/Result/Person.pm | 20 ++++-- lib/AccessSystem/Schema/ResultSet/Person.pm | 16 ++++- t/ResultSetPerson.t | 10 +++ 8 files changed, 142 insertions(+), 12 deletions(-) create mode 100644 docs/diagrams/bank_import_activity.png create mode 100644 docs/diagrams/bank_import_activity.txt create mode 100644 docs/diagrams/extend_membership_activity.png create mode 100644 docs/diagrams/extend_membership_activity.txt diff --git a/docs/diagrams/bank_import_activity.png b/docs/diagrams/bank_import_activity.png new file mode 100644 index 0000000000000000000000000000000000000000..91595f0e1b8f057770681bb438f489dc1d668614 GIT binary patch literal 14859 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 0) then (check name) + if (Name contains sm) then (find member) + if (Numerical id is a Person id) then (import) + if (Already imported - \nmatch on date/amount/person?) then (no) + :Insert new transaction into DB; + else (yes) + stop + endif + else (no) + stop + endif + else (no) + stop + endif + else (no) + stop + endif +endwhile +stop +@enduml diff --git a/docs/diagrams/extend_membership_activity.png b/docs/diagrams/extend_membership_activity.png new file mode 100644 index 0000000000000000000000000000000000000000..a55fe4434b16f5ebaf8285451b9086a307cf30d8 GIT binary patch literal 48392 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 + endif + ->continue; + :Get current balance (sum of all Transactions); + if ((NEW) Member is donor) then (yes) + :Load previous tier information; + if (Made Donor less than 1 month ago and\nCurrent balance enough for old tier) then (yes) + :Reset tier to old tier; + endif + endif + if (Current balance < fees) then (yes) + if ((NEW) Has previously paid anything and\nActually expired and\nBalance is > 0 and\nNot donor tier and\nChosen amount less than default tier fee) then (yes) + :Store current tier and payment amount; + :Switch tier to donor and payment amount to 0; + :Send email announcing donor switch; + break + else (no) + :Log "balance not enough"; + break + endif + if (Expiry date and\nExpired between 5 days ago and 1 month ago) then (yes) + :Send "reminder_email" email; + endif + endif + if (Expiry date) then (no) + :Send email "first_payment"; + endif + ->continue; + if (Expiry date and\nExpiry date is before now) then (yes) + :Force send email "rejoin_payment"; + :Delete any previous "reminder_email" communication; + endif + if (No Expiry date or\n Expiry date is before now) then (yes) + :Set Expiry date to today + :Extra days is $overlap days; + endif + ->continue; + :Enable Door access (in case of returning pre-covid member); + :Transaction Amount = membership fees + :Calculate new Expiry date (old one + 1 month + overlap days); + if (Current balance > fee * 12 * 0.9) then (yes) + :Recalulcate new Expiry date (old one + 12 months + overlap days); + :Transaction Amount = membership fees * 12 * 0.9; + endif + ->no; + :Create negative Transaction for Transaction Amount; + :Create Dues entry (Amount, paid on, expiry date); + endif +endwhile +:Repeat if more members; +stop +@enduml diff --git a/lib/AccessSystem/API/Controller/Root.pm b/lib/AccessSystem/API/Controller/Root.pm index 8c34c9b..3814df8 100644 --- a/lib/AccessSystem/API/Controller/Root.pm +++ b/lib/AccessSystem/API/Controller/Root.pm @@ -291,11 +291,13 @@ sub verify: Chained('base') :PathPart('verify') :Args(0) { person => { name => $result->{person}->name }, inductor => $result->{person}->allowed->first->is_admin, access => 1, + beep => $result->{beep} || 0, cache => $result->{person}->tier->restrictions->{'times'} ? 0 : 1, colour => $result->{person}->door_colour_to_code || 0x01, } ); } elsif($result) { + # Found Tool and Person but not allowed in some way: $c->stash( json => { access => 0, @@ -943,14 +945,15 @@ sub get_dues: Chained('base'): PathPart('get_dues'): Args(0) { my $tier = $c->req->params->{tier} || 3; $c->log->debug(Data::Dumper::Dumper($c->req->params)); + my $dummy_dues = $c->model('AccessDB::Person')->get_dummy_dues($tier, $dob, $concession); # $c->log->debug("Vals: $dob $concession $other_hackspace Result: ", $new_person->dues); - my $new_person = $c->model('AccessDB::Person')->new_result({}); - $new_person->tier_id($tier); - $new_person->dob($dob) if $dob; - $new_person->concessionary_rate_override($concession); + # my $new_person = $c->model('AccessDB::Person')->new_result({}); + # $new_person->tier_id($tier); + # $new_person->dob($dob) if $dob; + # $new_person->concessionary_rate_override($concession); - $c->log->debug("Vals: $dob $concession Result: ", $new_person->dues); - $c->response->body($new_person->dues / 100); + $c->log->debug("Vals: $dob $concession Result: ", $dummy_dues); + $c->response->body($dummy_dues / 100); } sub register: Chained('base'): PathPart('register'): Args(0) { diff --git a/lib/AccessSystem/Schema/Result/Person.pm b/lib/AccessSystem/Schema/Result/Person.pm index 2ee95b7..c141ae7 100644 --- a/lib/AccessSystem/Schema/Result/Person.pm +++ b/lib/AccessSystem/Schema/Result/Person.pm @@ -245,10 +245,18 @@ sub update { return $self->next::method(@_); } +sub is_donor { + my ($self) = @_; + + if ($self->tier && $self->tier->name =~ /donation/i) { + return 1; + } +} + sub is_valid { my ($self, $date) = @_; - return 0 if($self->tier->name =~ /donation/i); + return 0 if($self->is_donor); $date = DateTime->now(); @@ -299,7 +307,7 @@ sub normal_dues { return 0 if $self->parent; - if ($self->tier->name =~ /donation/i) { + if ($self->is_donor) { return 0; } @@ -485,6 +493,7 @@ If the balance is >= 12*$monthly*0.1, then make a year payment. sub create_payment { my ($self, $OVERLAP_DAYS) = @_; my $schema = $self->result_source->schema; + my $dt_parser = $schema->storage->datetime_parser; ## minor(?) side effects: @@ -514,6 +523,7 @@ sub create_payment { $self->update({ voucher_start => $now}); } } + my $current_bal = $self->balance_p; # work this out after voucher setting cos it changes the dues # And so does this bit! @@ -607,8 +617,8 @@ sub create_payment { } if($valid_date && $valid_date < $now) { - # renewed payments - $self->create_communication('Your Swindon Makerspace membership has restarted', 'rejoin_payment'); + # renewed payments - force this one, should happen everytime + $self->create_communication('Your Swindon Makerspace membership has restarted', 'rejoin_payment', {}, 1); # rejoined, so remove any "reminder" email, so that if they # subsequently stop paying again, they get a new reminder (!) my $r_email = $self->communications_rs->find({type => 'reminder_email'}); @@ -630,7 +640,7 @@ sub create_payment { my $payment_size = $self->dues; $self->update_door_access(); my $expires_on = $valid_date->clone->add(months => 1, %extra_days); - if($self->balance_p >= $self->dues * 12 * 0.9) { + if($current_bal >= $self->dues * 12 * 0.9) { # Special case, they paid for a year in advance (we assume!) $expires_on = $valid_date->clone->add(years => 1, %extra_days); $payment_size = $self->dues * 12 * 0.9; diff --git a/lib/AccessSystem/Schema/ResultSet/Person.pm b/lib/AccessSystem/Schema/ResultSet/Person.pm index d4a10cf..218f8ce 100644 --- a/lib/AccessSystem/Schema/ResultSet/Person.pm +++ b/lib/AccessSystem/Schema/ResultSet/Person.pm @@ -129,6 +129,7 @@ sub allowed_to_thing { } if (!$r_allow) { return { + person => $person, error => 'No access for Weekend Member', colour => 0x21, }; @@ -172,6 +173,7 @@ sub allowed_to_thing { }; } return { + person => $person, error => "Membership expired/unpaid", colour => 0x22, }; @@ -211,7 +213,7 @@ sub allowed_to_thing { } } else { return { - error => sprintf("%s not accepted. See email", $thing_rs->first->name), + error => sprintf("%s not accepted/inducted. See email", $thing_rs->first->name), person => $person, thing => $thing_rs->first, colour => 0x24, @@ -296,6 +298,18 @@ sub update_member_register { } } +sub get_dummy_dues { + my ($self, $tier_id, $dob, $conc, $fee_override) = @_; + + my $new_person = $self->new_result({}); + $new_person->tier_id($tier_id); + $new_person->dob($dob) if $dob; + $new_person->concessionary_rate_override($conc); + $new_person->payment_override($fee_override) if $fee_override; + + return $new_person->dues; +} + sub get_person_from_hash { my ($self, $hash) = @_; diff --git a/t/ResultSetPerson.t b/t/ResultSetPerson.t index 198b5ce..df5cf39 100644 --- a/t/ResultSetPerson.t +++ b/t/ResultSetPerson.t @@ -37,8 +37,13 @@ my $schema = AccessSystem::Schema->connect("dbi:SQLite:$testdb"); like($no_token->{error}, qr/not recognised/, 'No such member with that token'); $test9->create_related('tokens', { id => '12345678', type => 'test token' }); +<<<<<<< HEAD my $no_thing = $schema->resultset('Person')->allowed_to_thing('12345678', 'blahblahblah'); like($no_thing->{error}, qr/not recognised/, 'No such missing thing'); +======= +my $no_allowed = $schema->resultset('Person')->allowed_to_thing('12345678', $thing->id); +like($no_allowed->{error}, qr{accepted/inducted}, 'Person cannot use the thing'); +>>>>>>> 60aad4e (Verify and payments checking for new fees / donor tier) my $thing = $schema->resultset('Tool')->create({ name => 'test thing', assigned_ip => '10.0.0.1', requires_induction => 1, team => 'Who knows' }); @@ -50,7 +55,12 @@ my $schema = AccessSystem::Schema->connect("dbi:SQLite:$testdb"); my $no_pay = $schema->resultset('Person')->allowed_to_thing('12345678', $thing->id); like($no_pay->{error}, qr/Pay up please/, 'Member hasnt paid'); +<<<<<<< HEAD $test9->create_related('payments', { paid_on_date => DateTime->now, expires_on_date => DateTime->now->add(months => 1, days => 14), amount_p => $test9->dues }); +======= +my $allowed = $test9->allowed->find({tool_id => $thing->id }); +$allowed->update({ pending_acceptance => 0 }); +>>>>>>> 60aad4e (Verify and payments checking for new fees / donor tier) my $no_confirm = $schema->resultset('Person')->allowed_to_thing('12345678', $thing->id); like($no_confirm->{error}, qr/Induction not confirmed/, 'Member hasnt confirmed induction'); From 8f60f5e10b315907c00df4dd99fd439ed8d1493c Mon Sep 17 00:00:00 2001 From: AnotherMatt92 <108900488+AnotherMatt92@users.noreply.github.com> Date: Sun, 7 Dec 2025 13:33:45 +0000 Subject: [PATCH 3/6] emails related to new donation tier and new membership rates added 3 new email templates donation_access_denied - members on the donation tier should recieve this email when they are denied access membership_fees_change - when access the space after the first time that the mebership fees have changed highlighting how to update their payments move_to_donation - when membership is moved to donation due to lack of funds they should recieve this email --- .../donation_access_denied.html | 23 +++++++++++++++ .../donation_access_denied.txt | 19 +++++++++++++ .../membership_fees_change.html | 26 +++++++++++++++++ .../membership_fees_change.txt | 23 +++++++++++++++ .../move_to_dontation_tier.html | 28 +++++++++++++++++++ .../move_to_dontation_tier.txt | 22 +++++++++++++++ 6 files changed, 141 insertions(+) create mode 100644 root/src/emails/donation_access_denied/donation_access_denied.html create mode 100644 root/src/emails/donation_access_denied/donation_access_denied.txt create mode 100644 root/src/emails/membership_fees_change/membership_fees_change.html create mode 100644 root/src/emails/membership_fees_change/membership_fees_change.txt create mode 100644 root/src/emails/move_to_dontation_tier/move_to_dontation_tier.html create mode 100644 root/src/emails/move_to_dontation_tier/move_to_dontation_tier.txt diff --git a/root/src/emails/donation_access_denied/donation_access_denied.html b/root/src/emails/donation_access_denied/donation_access_denied.html new file mode 100644 index 0000000..58c23da --- /dev/null +++ b/root/src/emails/donation_access_denied/donation_access_denied.html @@ -0,0 +1,23 @@ +[% WRAPPER layout.html.tt %] + +

Dear [% member.name %],

+ +

You recently tried to access the Makerspace however your membership is currently set to donation which does not have access

+

This is likely due to our membership fees changing and you have not updated your standing order to reflect this

+ +

To activate your membership again and gain access to the Makerspace please make sure your payments are as follows

+

+

    +
  • Monthly fee: £[% member.oldtier.dues / 100 | format('%.2f') %]/month
  • +
  • To: Swindon Makerspace CIC
  • +
  • Bank: Barclays
  • +
  • Sort Code: 20-84-58
  • +
  • Account: 83789160
  • +
  • Ref: [% member.bank_ref %]
  • +
+

+ +

Any issues please contact us either on Telegram or email us at info@swindon-makerspace.org

+

Regards,

+ +

Swindon Makerspace

\ No newline at end of file diff --git a/root/src/emails/donation_access_denied/donation_access_denied.txt b/root/src/emails/donation_access_denied/donation_access_denied.txt new file mode 100644 index 0000000..0211a93 --- /dev/null +++ b/root/src/emails/donation_access_denied/donation_access_denied.txt @@ -0,0 +1,19 @@ +Dear [% member.name %], + +You recently tried to access the Makerspace however your membership is currently set to donation which does not have access +This is likely due to our membership fees changing and you have not updated your standing order to reflect this + +To activate your membership again and gain access to the Makerspace please make sure your payments are as follows + + Monthly fee: £[% member.oldtier.dues / 100 | format('%.2f') %]/month + To: Swindon Makerspace CIC + Bank: Barclays + Sort Code: 20-84-58 + Account: 83789160 + Ref: [% member.bank_ref %] + + +Any issues please contact us either on Telegram or email us at info@swindon-makerspace.org +Regards, + +Swindon Makerspace \ No newline at end of file diff --git a/root/src/emails/membership_fees_change/membership_fees_change.html b/root/src/emails/membership_fees_change/membership_fees_change.html new file mode 100644 index 0000000..51eb263 --- /dev/null +++ b/root/src/emails/membership_fees_change/membership_fees_change.html @@ -0,0 +1,26 @@ +[% WRAPPER layout.html.tt %] + +

Dear [% member.name %],

+ +

Our membership rates have recently changed. Hopefully you have seen this in recent emails, this is due to increase in rent and similar bills

+

While you have access to the end of your current months membership, you will need to update your standing order to the new rate for your membership to continue

+

Payment details are the same below just incase along with the new monthly fees rate

+ +

+

    +
  • Monthly fee: £[% member.dues / 100 | format('%.2f') %]/month
  • +
  • To: Swindon Makerspace CIC
  • +
  • Bank: Barclays
  • +
  • Sort Code: 20-84-58
  • +
  • Account: 83789160
  • +
  • Ref: [% member.bank_ref %]
  • +
+

+ +

Please update your membership fees, failure to do so will move you to a donor tier which does not have access to the Makerspace

+

If this does happen paying the difference to complete the monthly fee will reactivate your account

+ +

Any issues please contact us either on Telegram or email us at info@swindon-makerspace.org

+

Regards,

+ +

Swindon Makerspace

\ No newline at end of file diff --git a/root/src/emails/membership_fees_change/membership_fees_change.txt b/root/src/emails/membership_fees_change/membership_fees_change.txt new file mode 100644 index 0000000..bf7775c --- /dev/null +++ b/root/src/emails/membership_fees_change/membership_fees_change.txt @@ -0,0 +1,23 @@ +Dear [% member.name %], + +Our membership rates have recently changed. Hopefully you have seen this in recent emails, this is due to increase in rent and similar bills +While you have access to the end of your current months membership, you will need to update your standing order to the new rate for your membership to continue +Payment details are the same below just incase along with the new monthly fees rate + + + + Monthly fee: £[% member.dues / 100 | format('%.2f') %]/month + To: Swindon Makerspace CIC + Bank: Barclays + Sort Code: 20-84-58 + Account: 83789160 + Ref: [% member.bank_ref %] + + +Please update your membership fees, failure to do so will move you to a donor tier which does not have access to the Makerspace + If this does happen paying the difference to complete the monthly fee will reactivate your account + +Any issues please contact us either on Telegram or email us at info@swindon-makerspace.org +Regards, + +Swindon Makerspace \ No newline at end of file diff --git a/root/src/emails/move_to_dontation_tier/move_to_dontation_tier.html b/root/src/emails/move_to_dontation_tier/move_to_dontation_tier.html new file mode 100644 index 0000000..083956d --- /dev/null +++ b/root/src/emails/move_to_dontation_tier/move_to_dontation_tier.html @@ -0,0 +1,28 @@ +[% WRAPPER layout.html.tt %] +

Dear [% member.name %],

+ +

Your membership has been moved to the donation tier.\n +This is because our current membership fees have changed and you have not updated your payments to reflect this.\n +This donation tier does not have access to the space

+ + +

To resolve this and return to your active membership please send the difference of the new membership against your previous payments and update your standing order

+ +

Difference to pay: [% member.oldtier.dues - member.last-payment / 100 | format('%.2f') %]

+ +

Incase you have fogotten our the payment details are +

    +
  • Monthly fee: £[% member.oldtier.dues / 100 | format('%.2f') %]/month
  • +
  • To: Swindon Makerspace CIC
  • +
  • Bank: Barclays
  • +
  • Sort Code: 20-84-58
  • +
  • Account: 83789160
  • +
  • Ref: [% member.bank_ref %]
  • +
+

+ +

Any issues please contact us either on Telegram or email us at info@swindon-makerspace.org

+

Regards,

+ +

Swindon Makerspace

+ diff --git a/root/src/emails/move_to_dontation_tier/move_to_dontation_tier.txt b/root/src/emails/move_to_dontation_tier/move_to_dontation_tier.txt new file mode 100644 index 0000000..d0cbcea --- /dev/null +++ b/root/src/emails/move_to_dontation_tier/move_to_dontation_tier.txt @@ -0,0 +1,22 @@ +Dear [% member.name %], + +Your membership has been moved to the donation tier. +This is because our current membership fees have changed and you have not updated your payments to reflect this. +This donation tier does not have access to the space + +To resolve this and return to your active membership please send the difference of the new membership against your previous payments and update your standing order + +Difference to pay: [% member.oldtier.dues - member.last-payment / 100 | format('%.2f') %] + +Incase you have fogotten our the payment details are + Monthly fee: £[% member.oldtier.dues / 100 | format('%.2f') %]/month + To: Swindon Makerspace CIC + Bank: Barclays + Sort Code: 20-84-58 + Account: 83789160 + Ref: [% member.bank_ref %] + +Any issues please contact us either on Telegram or email us at info@swindon-makerspace.org +Regards, + +Swindon Makerspace \ No newline at end of file From f52b289b9c0e0fd688c1686e7d53ba241d196050 Mon Sep 17 00:00:00 2001 From: Jess Robinson Date: Sat, 20 Dec 2025 17:17:53 +0000 Subject: [PATCH 4/6] Minor tier-change email template fixes --- .../membership_fees_change/membership_fees_change.html | 5 +++-- .../membership_fees_change/membership_fees_change.txt | 4 ++-- .../move_to_donation_tier.html} | 8 ++++---- .../move_to_donation_tier.txt} | 6 +++--- 4 files changed, 12 insertions(+), 11 deletions(-) rename root/src/emails/{move_to_dontation_tier/move_to_dontation_tier.html => move_to_donation_tier/move_to_donation_tier.html} (77%) rename root/src/emails/{move_to_dontation_tier/move_to_dontation_tier.txt => move_to_donation_tier/move_to_donation_tier.txt} (78%) diff --git a/root/src/emails/membership_fees_change/membership_fees_change.html b/root/src/emails/membership_fees_change/membership_fees_change.html index 51eb263..1a8b66a 100644 --- a/root/src/emails/membership_fees_change/membership_fees_change.html +++ b/root/src/emails/membership_fees_change/membership_fees_change.html @@ -18,9 +18,10 @@

Please update your membership fees, failure to do so will move you to a donor tier which does not have access to the Makerspace

-

If this does happen paying the difference to complete the monthly fee will reactivate your account

+

If you have already paid your old amount this month, you can pay the difference to top up, this will restore your account to your chosen tier if done within a month

Any issues please contact us either on Telegram or email us at info@swindon-makerspace.org

Regards,

-

Swindon Makerspace

\ No newline at end of file +

Swindon Makerspace

+[% END %] diff --git a/root/src/emails/membership_fees_change/membership_fees_change.txt b/root/src/emails/membership_fees_change/membership_fees_change.txt index bf7775c..9ccf4f0 100644 --- a/root/src/emails/membership_fees_change/membership_fees_change.txt +++ b/root/src/emails/membership_fees_change/membership_fees_change.txt @@ -15,9 +15,9 @@ Payment details are the same below just incase along with the new monthly fees r Please update your membership fees, failure to do so will move you to a donor tier which does not have access to the Makerspace - If this does happen paying the difference to complete the monthly fee will reactivate your account +If you have already paid your old amount this month, you can pay the difference to top up, this will restore your account to your chosen tier if done within a month. Any issues please contact us either on Telegram or email us at info@swindon-makerspace.org Regards, -Swindon Makerspace \ No newline at end of file +Swindon Makerspace diff --git a/root/src/emails/move_to_dontation_tier/move_to_dontation_tier.html b/root/src/emails/move_to_donation_tier/move_to_donation_tier.html similarity index 77% rename from root/src/emails/move_to_dontation_tier/move_to_dontation_tier.html rename to root/src/emails/move_to_donation_tier/move_to_donation_tier.html index 083956d..b64bcdf 100644 --- a/root/src/emails/move_to_dontation_tier/move_to_dontation_tier.html +++ b/root/src/emails/move_to_donation_tier/move_to_donation_tier.html @@ -2,17 +2,16 @@

Dear [% member.name %],

Your membership has been moved to the donation tier.\n -This is because our current membership fees have changed and you have not updated your payments to reflect this.\n +This is because our current membership fees have changed and you have not updated your payments to reflect this.
This donation tier does not have access to the space

-

To resolve this and return to your active membership please send the difference of the new membership against your previous payments and update your standing order

-

Difference to pay: [% member.oldtier.dues - member.last-payment / 100 | format('%.2f') %]

+

Difference to pay: [% (current_balance - min_dues ) / 100 | format('%.2f') %]

Incase you have fogotten our the payment details are

    -
  • Monthly fee: £[% member.oldtier.dues / 100 | format('%.2f') %]/month
  • +
  • Monthly fee: £[% min_dues / 100 | format('%.2f') %]/month
  • To: Swindon Makerspace CIC
  • Bank: Barclays
  • Sort Code: 20-84-58
  • @@ -26,3 +25,4 @@

    Swindon Makerspace

    +[% END %] diff --git a/root/src/emails/move_to_dontation_tier/move_to_dontation_tier.txt b/root/src/emails/move_to_donation_tier/move_to_donation_tier.txt similarity index 78% rename from root/src/emails/move_to_dontation_tier/move_to_dontation_tier.txt rename to root/src/emails/move_to_donation_tier/move_to_donation_tier.txt index d0cbcea..afd6a8c 100644 --- a/root/src/emails/move_to_dontation_tier/move_to_dontation_tier.txt +++ b/root/src/emails/move_to_donation_tier/move_to_donation_tier.txt @@ -6,10 +6,10 @@ This donation tier does not have access to the space To resolve this and return to your active membership please send the difference of the new membership against your previous payments and update your standing order -Difference to pay: [% member.oldtier.dues - member.last-payment / 100 | format('%.2f') %] +Difference to pay: [% (current_balance - min_dues ) / 100 | format('%.2f') %] Incase you have fogotten our the payment details are - Monthly fee: £[% member.oldtier.dues / 100 | format('%.2f') %]/month + Monthly fee: £[% min_dues / 100 | format('%.2f') %]/month To: Swindon Makerspace CIC Bank: Barclays Sort Code: 20-84-58 @@ -19,4 +19,4 @@ Incase you have fogotten our the payment details are Any issues please contact us either on Telegram or email us at info@swindon-makerspace.org Regards, -Swindon Makerspace \ No newline at end of file +Swindon Makerspace From 4bdc156e193c6114d0f2a9fc5f2f12f2116394fc Mon Sep 17 00:00:00 2001 From: Jess Robinson Date: Sat, 27 Dec 2025 14:54:06 +0000 Subject: [PATCH 5/6] Fix test --- t/ResultSetPerson.t | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/t/ResultSetPerson.t b/t/ResultSetPerson.t index df5cf39..198b5ce 100644 --- a/t/ResultSetPerson.t +++ b/t/ResultSetPerson.t @@ -37,13 +37,8 @@ my $schema = AccessSystem::Schema->connect("dbi:SQLite:$testdb"); like($no_token->{error}, qr/not recognised/, 'No such member with that token'); $test9->create_related('tokens', { id => '12345678', type => 'test token' }); -<<<<<<< HEAD my $no_thing = $schema->resultset('Person')->allowed_to_thing('12345678', 'blahblahblah'); like($no_thing->{error}, qr/not recognised/, 'No such missing thing'); -======= -my $no_allowed = $schema->resultset('Person')->allowed_to_thing('12345678', $thing->id); -like($no_allowed->{error}, qr{accepted/inducted}, 'Person cannot use the thing'); ->>>>>>> 60aad4e (Verify and payments checking for new fees / donor tier) my $thing = $schema->resultset('Tool')->create({ name => 'test thing', assigned_ip => '10.0.0.1', requires_induction => 1, team => 'Who knows' }); @@ -55,12 +50,7 @@ like($no_allowed->{error}, qr{accepted/inducted}, 'Person cannot use the thing') my $no_pay = $schema->resultset('Person')->allowed_to_thing('12345678', $thing->id); like($no_pay->{error}, qr/Pay up please/, 'Member hasnt paid'); -<<<<<<< HEAD $test9->create_related('payments', { paid_on_date => DateTime->now, expires_on_date => DateTime->now->add(months => 1, days => 14), amount_p => $test9->dues }); -======= -my $allowed = $test9->allowed->find({tool_id => $thing->id }); -$allowed->update({ pending_acceptance => 0 }); ->>>>>>> 60aad4e (Verify and payments checking for new fees / donor tier) my $no_confirm = $schema->resultset('Person')->allowed_to_thing('12345678', $thing->id); like($no_confirm->{error}, qr/Induction not confirmed/, 'Member hasnt confirmed induction'); From ba355095485c1e9ef63c2490970c2339d8f72f91 Mon Sep 17 00:00:00 2001 From: Jess Robinson Date: Sat, 27 Dec 2025 15:26:49 +0000 Subject: [PATCH 6/6] Expand docs for create_payment and allowed_to_thing --- lib/AccessSystem/Schema/Result/Person.pm | 33 +++++++++++++++++++++ lib/AccessSystem/Schema/ResultSet/Person.pm | 7 +++++ 2 files changed, 40 insertions(+) diff --git a/lib/AccessSystem/Schema/Result/Person.pm b/lib/AccessSystem/Schema/Result/Person.pm index c141ae7..80a0783 100644 --- a/lib/AccessSystem/Schema/Result/Person.pm +++ b/lib/AccessSystem/Schema/Result/Person.pm @@ -479,6 +479,13 @@ sub import_transaction { =head2 create_payment +OVERLAP_DAYS is set to 14: That is any new-member or returning member +is given one month +14 days as the length of their initial +access. They are "valid" as members/door + tool users, for this +time. Subsequent payments made while still valid add a further months +worth of access. The intention of this is to keep access over bank +holidays or payment issues. + Check if we are nearing the end of this member's paid membership, return true if not. @@ -488,6 +495,32 @@ can't find one. If the balance is >= 12*$monthly*0.1, then make a year payment. +Fees change 2025! + +If the member is nearing the end of their overlap period (2 days or +fewer to go), and was at some point a paidup member (previously made a +payment), and does not have enough balance to pay their dues (max +value between payment_override + tier minimum amount), they are +converted to a "donor only" tier member. + +If the balance is enough to pay their dues (as above) and was made a +donor member less than a month ago, they've presumably topped up/fixed +their amount, we convert them back to their old tier. + +Emails are sent for: + +=over + +=item New member payment received + +=item Returning member payment received + +=item Converted to donor member + +=item Reminder when within 5 days of expiry and no payment yet received. + +=back + =cut sub create_payment { diff --git a/lib/AccessSystem/Schema/ResultSet/Person.pm b/lib/AccessSystem/Schema/ResultSet/Person.pm index 218f8ce..c679545 100644 --- a/lib/AccessSystem/Schema/ResultSet/Person.pm +++ b/lib/AccessSystem/Schema/ResultSet/Person.pm @@ -81,6 +81,13 @@ or { error => 'Membership expired/unpaid', colour => 0x22 } +Fees change 2025! + +If member is being allowed access, check whether their most recent +payment matches their dues (maximum between the payment_override and +current tier price), if not beep, email and output a message on +screen. + =cut sub allowed_to_thing {