From 0b81782a6257456e04fb8a5a7faf0dbfbf7bc166 Mon Sep 17 00:00:00 2001 From: mark Date: Tue, 12 Jul 2011 07:54:22 +0000 Subject: [PATCH] credit card expiration event, #13202 --- FS/FS/cust_main.pm | 54 +++++++++++++++++++++++ FS/FS/cust_main/Billing.pm | 2 +- FS/FS/msg_template.pm | 9 ++-- FS/FS/part_event/Condition/cust_paydate_within.pm | 46 +++++++++++++++++++ FS/FS/part_event/Condition/once_every.pm | 2 +- 5 files changed, 106 insertions(+), 7 deletions(-) create mode 100644 FS/FS/part_event/Condition/cust_paydate_within.pm diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm index 95ade20f9..1b059e6c2 100644 --- a/FS/FS/cust_main.pm +++ b/FS/FS/cust_main.pm @@ -2915,6 +2915,60 @@ sub paydate_monthyear { } } +=item paydate_epoch + +Returns the exact time in seconds corresponding to the payment method +expiration date. For CARD/DCRD customers this is the end of the month; +for others (COMP is the only other payby that uses paydate) it's the start. +Returns 0 if the paydate is empty or set to the far future. + +=cut + +sub paydate_epoch { + my $self = shift; + my ($month, $year) = $self->paydate_monthyear; + return 0 if !$year or $year >= 2037; + if ( $self->payby eq 'CARD' or $self->payby eq 'DCRD' ) { + $month++; + if ( $month == 13 ) { + $month = 1; + $year++; + } + return timelocal(0,0,0,1,$month-1,$year) - 1; + } + else { + return timelocal(0,0,0,1,$month-1,$year); + } +} + +=item paydate_epoch_sql + +Class method. Returns an SQL expression to obtain the payment expiration date +as a number of seconds. + +=cut + +# Special expiration date behavior for non-CARD/DCRD customers has been +# carefully preserved. Do we really use that? +sub paydate_epoch_sql { + my $class = shift; + my $table = shift || 'cust_main'; + my ($case1, $case2); + if ( driver_name eq 'Pg' ) { + $case1 = "EXTRACT( EPOCH FROM CAST( $table.paydate AS TIMESTAMP ) + INTERVAL '1 month') - 1"; + $case2 = "EXTRACT( EPOCH FROM CAST( $table.paydate AS TIMESTAMP ) )"; + } + elsif ( lc(driver_name) eq 'mysql' ) { + $case1 = "UNIX_TIMESTAMP( DATE_ADD( CAST( $table.paydate AS DATETIME ), INTERVAL 1 month ) ) - 1"; + $case2 = "UNIX_TIMESTAMP( CAST( $table.paydate AS DATETIME ) )"; + } + else { return '' } + return "CASE WHEN $table.payby IN('CARD','DCRD') + THEN ($case1) + ELSE ($case2) + END" +} + =item tax_exemption TAXNAME =cut diff --git a/FS/FS/cust_main/Billing.pm b/FS/FS/cust_main/Billing.pm index 0133164ec..4b9c3d328 100644 --- a/FS/FS/cust_main/Billing.pm +++ b/FS/FS/cust_main/Billing.pm @@ -1663,7 +1663,7 @@ sub do_cust_event { if $DEBUG > 1; #if ( my $error = $cust_event->do_event(%options) ) { #XXX %options? - if ( my $error = $cust_event->do_event() ) { + if ( my $error = $cust_event->do_event( 'time' => $time ) ) { #XXX wtf is this? figure out a proper dealio with return value #from do_event return $error; diff --git a/FS/FS/msg_template.pm b/FS/FS/msg_template.pm index e90cffd73..65acd9aac 100644 --- a/FS/FS/msg_template.pm +++ b/FS/FS/msg_template.pm @@ -249,7 +249,7 @@ sub prepare { } } } - $_ = encode_entities($_) foreach values(%hash); + $_ = encode_entities($_ || '') foreach values(%hash); ### @@ -386,12 +386,11 @@ sub substitutions { cust_status ucfirst_cust_status cust_statuscolor signupdate dundate - expdate packages recurdates ), - # expdate is a special case - [ signupdate_ymd => sub { time2str('%Y-%m-%d', shift->signupdate) } ], - [ dundate_ymd => sub { time2str('%Y-%m-%d', shift->dundate) } ], + [ expdate => sub { shift->paydate_epoch } ], #compatibility + [ signupdate_ymd => sub { $ymd->(shift->signupdate) } ], + [ dundate_ymd => sub { $ymd->(shift->dundate) } ], [ paydate_my => sub { sprintf('%02d/%04d', shift->paydate_monthyear) } ], [ otaker_first => sub { shift->access_user->first } ], [ otaker_last => sub { shift->access_user->last } ], diff --git a/FS/FS/part_event/Condition/cust_paydate_within.pm b/FS/FS/part_event/Condition/cust_paydate_within.pm new file mode 100644 index 000000000..4808e9083 --- /dev/null +++ b/FS/FS/part_event/Condition/cust_paydate_within.pm @@ -0,0 +1,46 @@ +package FS::part_event::Condition::cust_paydate_within; + +use strict; +use base qw( FS::part_event::Condition ); +use FS::Record qw( str2time_sql str2time_sql_closing ); +use Time::Local 'timelocal'; + +sub description { + 'Credit card expires within upcoming interval'; +} + +# Run the event when the customer's credit card expiration +# date is less than X days in the future. +# Combine this with a "once_every" condition so that the event +# won't repeat every day until the expiration date. + +sub eventtable_hashref { + { 'cust_main' => 1, + 'cust_bill' => 0, + 'cust_pkg' => 0, + }; +} + +sub option_fields { + ( + 'within' => { 'label' => 'Expiration date within', + 'type' => 'freq', + }, + ); +} + +sub condition { + my( $self, $cust_main, %opt ) = @_; + my $expire_time = $cust_main->paydate_epoch or return 0; + $opt{'time'} >= $self->option_age_from('within', $expire_time); +} + +sub condition_sql { + my ($self, $table, %opt) = @_; + my $expire_time = FS::cust_main->paydate_epoch_sql or return 'true'; + $opt{'time'} . ' >= ' . + $self->condition_sql_option_age_from('within', $expire_time); +} + +1; + diff --git a/FS/FS/part_event/Condition/once_every.pm b/FS/FS/part_event/Condition/once_every.pm index 2921b3a22..ef28078ed 100644 --- a/FS/FS/part_event/Condition/once_every.pm +++ b/FS/FS/part_event/Condition/once_every.pm @@ -31,7 +31,7 @@ sub condition { 'eventpart' => $self->eventpart, 'tablenum' => $tablenum, 'status' => { op=>'!=', value=>'failed' }, - '_date' => { op=>'>=', value=>$max_date }, + '_date' => { op=>'>', value=>$max_date }, }, 'extra_sql' => ( $opt{'cust_event'}->eventnum =~ /^(\d+)$/ ? " AND eventnum != $1 " -- 2.11.0