summaryrefslogtreecommitdiff
path: root/FS/t/suite
diff options
context:
space:
mode:
Diffstat (limited to 'FS/t/suite')
-rwxr-xr-xFS/t/suite/00-new_customer.t67
-rwxr-xr-xFS/t/suite/01-order_pkg.t49
-rwxr-xr-xFS/t/suite/02-bill_customer.t38
-rwxr-xr-xFS/t/suite/03-realtime_pay.t40
-rwxr-xr-xFS/t/suite/04-pkg_change_status.t103
-rwxr-xr-xFS/t/suite/05-prorate_sync_same_day.t97
-rwxr-xr-xFS/t/suite/06-prorate_defer_bill.t92
-rwxr-xr-xFS/t/suite/07-pkg_change_location.t82
-rwxr-xr-xFS/t/suite/08-sales_tax.t76
-rw-r--r--FS/t/suite/WRITING93
10 files changed, 737 insertions, 0 deletions
diff --git a/FS/t/suite/00-new_customer.t b/FS/t/suite/00-new_customer.t
new file mode 100755
index 000000000..8e86459d1
--- /dev/null
+++ b/FS/t/suite/00-new_customer.t
@@ -0,0 +1,67 @@
+#!/usr/bin/perl
+
+use FS::Test;
+use Test::More tests => 4;
+
+my $FS = FS::Test->new;
+# get the form
+$FS->post('/edit/cust_main.cgi');
+my $form = $FS->form('CustomerForm');
+
+my %params = (
+ residential_commercial => 'Residential',
+ agentnum => 1,
+ refnum => 1,
+ last => 'Customer',
+ first => 'New',
+ invoice_email => 'newcustomer@fake.freeside.biz',
+ bill_address1 => '123 Example Street',
+ bill_address2 => 'Apt. Z',
+ bill_city => 'Sacramento',
+ bill_state => 'CA',
+ bill_zip => '94901',
+ bill_country => 'US',
+ bill_coord_auto => 'Y',
+ daytime => '916-555-0100',
+ night => '916-555-0200',
+ ship_address1 => '125 Example Street',
+ ship_address2 => '3rd Floor',
+ ship_city => 'Sacramento',
+ ship_state => 'CA',
+ ship_zip => '94901',
+ ship_country => 'US',
+ ship_coord_auto => 'Y',
+ invoice_ship_address => 'Y',
+ postal_invoice => 'Y',
+ billday => '1',
+ no_credit_limit => 1,
+ # payment method
+ custpaybynum0_payby => 'CARD',
+ custpaybynum0_payinfo => '4012888888881881',
+ custpaybynum0_paydate_month => '12',
+ custpaybynum0_paydate_year => '2020',
+ custpaybynum0_paycvv => '123',
+ custpaybynum0_payname => '',
+ custpaybynum0_weight => 1,
+);
+foreach (keys %params) {
+ $form->value($_, $params{$_});
+}
+$FS->post($form);
+ok( $FS->error eq '' , 'form posted' );
+if (
+ ok($FS->redirect =~ m[^/view/cust_main.cgi\?(\d+)], 'new customer accepted')
+) {
+ my $custnum = $1;
+ my $cust = $FS->qsearchs('cust_main', { custnum => $1 });
+ isa_ok ( $cust, 'FS::cust_main' );
+ $FS->post($FS->redirect);
+ ok ( $FS->error eq '' , 'can view customer' );
+} else {
+ # try to display the error message, or if not, show everything
+ $FS->post($FS->redirect);
+ diag ($FS->error);
+ done_testing(2);
+}
+
+1;
diff --git a/FS/t/suite/01-order_pkg.t b/FS/t/suite/01-order_pkg.t
new file mode 100755
index 000000000..ab5a2ddc6
--- /dev/null
+++ b/FS/t/suite/01-order_pkg.t
@@ -0,0 +1,49 @@
+#!/usr/bin/perl
+
+use Test::More tests => 4;
+use FS::Test;
+use Date::Parse 'str2time';
+my $FS = FS::Test->new;
+
+# get the form
+$FS->post('/misc/order_pkg.html', custnum => 2);
+my $form = $FS->form('OrderPkgForm');
+
+# Customer #2 has three packages:
+# a $30 monthly prorate, a $90 monthly prorate, and a $25 annual prorate.
+# Next bill date on the monthly prorates is 2016-04-01.
+# Add a new package that will start billing on 2016-03-20 (to make prorate
+# behavior visible).
+
+my %params = (
+ pkgpart => 2,
+ quantity => 1,
+ start => 'on_date',
+ start_date => '03/20/2016',
+ package_comment0 => $0, # record the test we're executing
+);
+
+$form->find_input('start')->disabled(0); # JS
+foreach (keys %params) {
+ $form->value($_, $params{$_});
+}
+$FS->post($form);
+ok( $FS->error eq '' , 'form posted' );
+if (
+ ok( $FS->page =~ m[location = '.*/view/cust_main.cgi.*\#cust_pkg(\d+)'],
+ 'new package accepted' )
+) {
+ # on success, sends us back to cust_main view with #cust_pkg$pkgnum
+ # but with an in-page javascript redirect
+ my $pkg = $FS->qsearchs('cust_pkg', { pkgnum => $1 });
+ isa_ok( $pkg, 'FS::cust_pkg' );
+ ok($pkg->start_date == str2time('2016-03-20'), 'start date set');
+} else {
+ # try to display the error message, or if not, show everything
+ $FS->post($FS->redirect);
+ diag ($FS->error);
+ done_testing(2);
+}
+
+1;
+
diff --git a/FS/t/suite/02-bill_customer.t b/FS/t/suite/02-bill_customer.t
new file mode 100755
index 000000000..3fa908e96
--- /dev/null
+++ b/FS/t/suite/02-bill_customer.t
@@ -0,0 +1,38 @@
+#!/usr/bin/perl
+
+use FS::Test;
+use Test::More tests => 6;
+use Test::MockTime 'set_fixed_time';
+use Date::Parse 'str2time';
+use FS::cust_main;
+
+my $FS = FS::Test->new;
+
+# After test 01: cust#2 has a package set to bill on 2016-03-20.
+# Set local time.
+my $date = '2016-03-20';
+set_fixed_time(str2time($date));
+my $cust_main = FS::cust_main->by_key(2);
+my @return;
+
+# Bill the customer.
+my $error = $cust_main->bill( return_bill => \@return );
+ok($error eq '', "billed on $date") or diag($error);
+
+# should be an invoice now
+my $cust_bill = $return[0];
+isa_ok($cust_bill, 'FS::cust_bill');
+
+# Apr 1 - Mar 20 = 12 days = 288 hours
+# Apr 1 - Mar 1 = 31 days - 1 hour (DST) = 743 hours
+# 288/743 * $30 = $11.63 recur + $20.00 setup
+ok( $cust_bill->charged == 31.63, 'prorated first month correctly' );
+
+# the package bill date should now be 2016-04-01
+my @lineitems = $cust_bill->cust_bill_pkg;
+ok( scalar(@lineitems) == 1, 'one package was billed' );
+my $pkg = $lineitems[0]->cust_pkg;
+ok( $pkg->status eq 'active', 'package is now active' );
+ok( $pkg->bill == str2time('2016-04-01'), 'package bill date set correctly' );
+
+1;
diff --git a/FS/t/suite/03-realtime_pay.t b/FS/t/suite/03-realtime_pay.t
new file mode 100755
index 000000000..17456bb15
--- /dev/null
+++ b/FS/t/suite/03-realtime_pay.t
@@ -0,0 +1,40 @@
+#!/usr/bin/perl
+
+use FS::Test;
+use Test::More tests => 2;
+use FS::cust_main;
+
+my $FS = FS::Test->new;
+
+# In the stock database, cust#5 has open invoices
+my $cust_main = FS::cust_main->by_key(5);
+my $balance = $cust_main->balance;
+ok( $balance > 10.00, 'customer has an outstanding balance of more than $10.00' );
+
+# Get the payment form
+$FS->post('/misc/payment.cgi?payby=CARD;custnum=5');
+my $form = $FS->form('OneTrueForm');
+$form->value('amount' => '10.00');
+$form->value('custpaybynum' => '');
+$form->value('payinfo' => '4012888888881881');
+$form->value('month' => '01');
+$form->value('year' => '2020');
+# payname and location fields should already be set
+$form->value('save' => 1);
+$form->value('auto' => 1);
+$FS->post($form);
+
+# on success, gives a redirect to the payment receipt
+my $paynum;
+if ($FS->redirect =~ m[^/view/cust_pay.html\?(\d+)]) {
+ pass('payment processed');
+ $paynum = $1;
+} elsif ( $FS->error ) {
+ fail('payment rejected');
+ diag ( $FS->error );
+} else {
+ fail('unknown result');
+ diag ( $FS->page );
+}
+
+1;
diff --git a/FS/t/suite/04-pkg_change_status.t b/FS/t/suite/04-pkg_change_status.t
new file mode 100755
index 000000000..cc969983a
--- /dev/null
+++ b/FS/t/suite/04-pkg_change_status.t
@@ -0,0 +1,103 @@
+#!/usr/bin/perl
+
+=head2 DESCRIPTION
+
+Tests the effect of a scheduled change on the status of an active or
+suspended package. Ref RT#38564.
+
+Correct: A scheduled package change should result in a package with the same
+status as before.
+
+=cut
+
+use strict;
+use Test::More tests => 20;
+use FS::Test;
+use Date::Parse 'str2time';
+use Test::MockTime qw(set_fixed_time);
+use FS::cust_main;
+use FS::cust_pkg;
+my $FS = FS::Test->new;
+
+# Create two package defs with the suspend_bill flag, and one with
+# the unused_credit_change flag.
+my $part_pkg = $FS->qsearchs('part_pkg', { pkgpart => 2 });
+my $error;
+my @part_pkgs;
+foreach my $i (0, 1) {
+ $part_pkgs[$i] = $part_pkg->clone;
+ $part_pkgs[$i]->insert(options => { $part_pkg->options,
+ 'suspend_bill' => 1,
+ 'unused_credit_change' => $i } );
+ BAIL_OUT("can't configure package: $error") if $error;
+}
+
+# For customer #3, order four packages. 0-1 will be suspended, 2-3 will not.
+# 1 and 3 will use $part_pkgs[1], the one with unused_credit_change.
+
+my $cust = $FS->qsearchs('cust_main', { custnum => 3 });
+my @pkgs;
+foreach my $i (0..3) {
+ $pkgs[$i] = FS::cust_pkg->new({ pkgpart => $part_pkgs[$i % 2]->pkgpart });
+ $error = $cust->order_pkg({ cust_pkg => $pkgs[$i] });
+ BAIL_OUT("can't order package: $error") if $error;
+}
+
+# On Mar 25, bill the customer.
+
+set_fixed_time(str2time('2016-03-25'));
+$error = $cust->bill_and_collect;
+ok( $error eq '', 'initially bill customer' );
+# update our @pkgs to match
+@pkgs = map { $_->replace_old } @pkgs;
+
+# On Mar 26, suspend packages 0-1.
+
+set_fixed_time(str2time('2016-03-25'));
+my $reason_type = $FS->qsearchs('reason_type', { type => 'Suspend Reason' });
+foreach my $i (0,1) {
+ $error = $pkgs[$i]->suspend(reason => {
+ typenum => $reason_type->typenum,
+ reason => 'Test suspension + future package change',
+ });
+ ok( $error eq '', "suspended package $i" ) or diag($error);
+ $pkgs[$i] = $pkgs[$i]->replace_old;
+}
+
+# For each of these packages, clone the package def, then schedule a future
+# change (on Mar 26) to that package.
+my $change_date = str2time('2016-03-26');
+my @new_pkgs;
+foreach my $i (0..3) {
+ my $pkg = $pkgs[$i];
+ my $new_part_pkg = $pkg->part_pkg->clone;
+ $error = $new_part_pkg->insert( options => { $pkg->part_pkg->options } );
+ ok( $error eq '', 'created new package def' ) or diag($error);
+ $error = $pkg->change_later(
+ pkgpart => $new_part_pkg->pkgpart,
+ start_date => $change_date,
+ );
+ ok( $error eq '', 'scheduled package change' ) or diag($error);
+ $new_pkgs[$i] = $FS->qsearchs('cust_pkg', {
+ pkgnum => $pkg->change_to_pkgnum
+ });
+ ok( $new_pkgs[$i], 'future package was created' );
+}
+
+# Then bill the customer on that date.
+set_fixed_time($change_date);
+$error = $cust->bill_and_collect;
+ok( $error eq '', 'billed customer on change date' ) or diag($error);
+
+foreach my $i (0,1) {
+ $new_pkgs[$i] = $new_pkgs[$i]->replace_old;
+ ok( $new_pkgs[$i]->status eq 'suspended', "new package $i is suspended" )
+ or diag($new_pkgs[$i]->status);
+}
+foreach my $i (2,3) {
+ $new_pkgs[$i] = $new_pkgs[$i]->replace_old;
+ ok( $new_pkgs[$i]->status eq 'active', "new package $i is active" )
+ or diag($new_pkgs[$i]->status);
+}
+
+1;
diff --git a/FS/t/suite/05-prorate_sync_same_day.t b/FS/t/suite/05-prorate_sync_same_day.t
new file mode 100755
index 000000000..91a8efa74
--- /dev/null
+++ b/FS/t/suite/05-prorate_sync_same_day.t
@@ -0,0 +1,97 @@
+#!/usr/bin/perl
+
+=head2 DESCRIPTION
+
+Tests the effect of ordering and activating two sync_bill_date packages on
+the same day. Ref RT#42108.
+
+Correct: If the packages have prorate_round_day = 1 (round nearest), or 3
+(round down) then the second package should be prorated one day short. If
+they have prorate_round_day = 2 (round up), they should be billed
+for the same amount. In both cases they should have the same next bill date.
+
+=cut
+
+use strict;
+use Test::More tests => 9;
+use FS::Test;
+use Date::Parse 'str2time';
+use Date::Format 'time2str';
+use Test::MockTime qw(set_fixed_time);
+use FS::cust_main;
+use FS::cust_pkg;
+use FS::Conf;
+my $FS= FS::Test->new;
+
+foreach my $prorate_mode (1, 2, 3) {
+ diag("prorate_round_day = $prorate_mode");
+ # Create a package def with the sync_bill_date option.
+ my $error;
+ my $old_part_pkg = $FS->qsearchs('part_pkg', { pkgpart => 5 });
+ my $part_pkg = $old_part_pkg->clone;
+ BAIL_OUT("existing pkgpart 5 is not a flat monthly package")
+ unless $part_pkg->freq eq '1' and $part_pkg->plan eq 'flat';
+ $error = $part_pkg->insert(
+ options => { $old_part_pkg->options,
+ 'sync_bill_date' => 1,
+ 'prorate_round_day' => $prorate_mode, }
+ );
+
+ BAIL_OUT("can't configure package: $error") if $error;
+
+ my $pkgpart = $part_pkg->pkgpart;
+ # Create a clean customer with no other packages.
+ my $location = FS::cust_location->new({
+ address1 => '123 Example Street',
+ city => 'Sacramento',
+ state => 'CA',
+ country => 'US',
+ zip => '94901',
+ });
+ my $cust = FS::cust_main->new({
+ agentnum => 1,
+ refnum => 1,
+ last => 'Customer',
+ first => 'Sync bill date',
+ invoice_email => 'newcustomer@fake.freeside.biz',
+ bill_location => $location,
+ ship_location => $location,
+ });
+ $error = $cust->insert;
+ BAIL_OUT("can't create test customer: $error") if $error;
+
+ my @pkgs;
+ # Create and bill the first package.
+ set_fixed_time(str2time('2016-03-10 08:00'));
+ $pkgs[0] = FS::cust_pkg->new({ pkgpart => $pkgpart });
+ $error = $cust->order_pkg({ 'cust_pkg' => $pkgs[0] });
+ BAIL_OUT("can't order package: $error") if $error;
+ $error = $cust->bill_and_collect;
+ # Check the amount billed.
+ my ($cust_bill_pkg) = $pkgs[0]->cust_bill_pkg;
+ my $recur = $part_pkg->base_recur;
+ ok( $cust_bill_pkg->recur == $recur, "first package recur is $recur" )
+ or diag("first package recur is ".$cust_bill_pkg->recur);
+
+ # Create and bill the second package.
+ set_fixed_time(str2time('2016-03-10 16:00'));
+ $pkgs[1] = FS::cust_pkg->new({ pkgpart => $pkgpart });
+ $error = $cust->order_pkg({ 'cust_pkg' => $pkgs[1] });
+ BAIL_OUT("can't order package: $error") if $error;
+ $error = $cust->bill_and_collect;
+
+ # Check the amount billed.
+ if ( $prorate_mode == 1 or $prorate_mode == 3 ) {
+ # it should be one day short, in March
+ $recur = sprintf('%.2f', $recur * 30/31);
+ }
+ ($cust_bill_pkg) = $pkgs[1]->cust_bill_pkg;
+ ok( $cust_bill_pkg->recur == $recur, "second package recur is $recur" )
+ or diag("second package recur is ".$cust_bill_pkg->recur);
+
+ my @next_bill = map { time2str('%Y-%m-%d', $_->replace_old->get('bill')) } @pkgs;
+
+ ok( $next_bill[0] eq $next_bill[1],
+ "both packages will bill again on $next_bill[0]" )
+ or diag("first package bill date is $next_bill[0], second package is $next_bill[1]");
+}
diff --git a/FS/t/suite/06-prorate_defer_bill.t b/FS/t/suite/06-prorate_defer_bill.t
new file mode 100755
index 000000000..e14b8ec21
--- /dev/null
+++ b/FS/t/suite/06-prorate_defer_bill.t
@@ -0,0 +1,92 @@
+#!/usr/bin/perl
+
+=head2 DESCRIPTION
+
+Tests the prorate_defer_bill behavior when a package is started on the cutoff day,
+and when it's started later in the month.
+
+Correct: The package started on the cutoff day should be charged a setup fee and a
+full period. The package started later in the month should be charged a setup fee,
+a full period, and the partial period.
+
+=cut
+
+use strict;
+use Test::More tests => 11;
+use FS::Test;
+use Date::Parse 'str2time';
+use Date::Format 'time2str';
+use Test::MockTime qw(set_fixed_time);
+use FS::cust_main;
+use FS::cust_pkg;
+use FS::Conf;
+my $FS= FS::Test->new;
+
+my $error;
+
+my $old_part_pkg = $FS->qsearchs('part_pkg', { pkgpart => 2 });
+my $part_pkg = $old_part_pkg->clone;
+BAIL_OUT("existing pkgpart 2 is not a prorated monthly package")
+ unless $part_pkg->freq eq '1' and $part_pkg->plan eq 'prorate';
+$error = $part_pkg->insert(
+ options => { $old_part_pkg->options,
+ 'prorate_defer_bill' => 1,
+ 'cutoff_day' => 1,
+ 'setup_fee' => 100,
+ 'recur_fee' => 30,
+ }
+);
+BAIL_OUT("can't configure package: $error") if $error;
+
+my $cust = $FS->new_customer('Prorate defer');
+$error = $cust->insert;
+BAIL_OUT("can't create test customer: $error") if $error;
+
+my @pkgs;
+foreach my $start_day (1, 11) {
+ diag("prorate package starting on day $start_day");
+ # Create and bill the first package.
+ my $date = str2time("2016-04-$start_day");
+ set_fixed_time($date);
+ my $pkg = FS::cust_pkg->new({ pkgpart => $part_pkg->pkgpart });
+ $error = $cust->order_pkg({ 'cust_pkg' => $pkg });
+ BAIL_OUT("can't order package: $error") if $error;
+
+ # bill the customer on the order date
+ $error = $cust->bill_and_collect;
+ $pkg = $pkg->replace_old;
+ push @pkgs, $pkg;
+ my ($cust_bill_pkg) = $pkg->cust_bill_pkg;
+ if ( $start_day == 1 ) {
+ # then it should bill immediately
+ ok($cust_bill_pkg, "package was billed") or next;
+ ok($cust_bill_pkg->setup == 100, "setup fee was charged");
+ ok($cust_bill_pkg->recur == 30, "one month was charged");
+ } elsif ( $start_day == 11 ) {
+ # then not
+ ok(!$cust_bill_pkg, "package billing was deferred");
+ ok($pkg->setup == $date, "package setup date was set");
+ }
+}
+diag("first of month billing...");
+my $date = str2time('2016-05-01');
+set_fixed_time($date);
+my @bill;
+$error = $cust->bill_and_collect(return_bill => \@bill);
+# examine the invoice...
+my $cust_bill = $bill[0] or BAIL_OUT("neither package was billed");
+for my $pkg ($pkgs[0]) {
+ diag("package started day 1:");
+ my ($cust_bill_pkg) = grep {$_->pkgnum == $pkg->pkgnum} $cust_bill->cust_bill_pkg;
+ ok($cust_bill_pkg, "was billed") or next;
+ ok($cust_bill_pkg->setup == 0, "no setup fee was charged");
+ ok($cust_bill_pkg->recur == 30, "one month was charged");
+}
+for my $pkg ($pkgs[1]) {
+ diag("package started day 11:");
+ my ($cust_bill_pkg) = grep {$_->pkgnum == $pkg->pkgnum} $cust_bill->cust_bill_pkg;
+ ok($cust_bill_pkg, "was billed") or next;
+ ok($cust_bill_pkg->setup == 100, "setup fee was charged");
+ ok($cust_bill_pkg->recur == 50, "twenty days + one month was charged");
+}
+
diff --git a/FS/t/suite/07-pkg_change_location.t b/FS/t/suite/07-pkg_change_location.t
new file mode 100755
index 000000000..6744f78ef
--- /dev/null
+++ b/FS/t/suite/07-pkg_change_location.t
@@ -0,0 +1,82 @@
+#!/usr/bin/perl
+
+=head2 DESCRIPTION
+
+Test scheduling a package location change through the UI, then billing
+on the day of the scheduled change.
+
+=cut
+
+use Test::More tests => 6;
+use FS::Test;
+use Date::Parse 'str2time';
+use Date::Format 'time2str';
+use Test::MockTime qw(set_fixed_time);
+use FS::cust_pkg;
+my $FS = FS::Test->new;
+my $error;
+
+# set up a customer with an active package
+my $cust = $FS->new_customer('Future location change');
+$error = $cust->insert;
+my $pkg = FS::cust_pkg->new({pkgpart => 2});
+$error ||= $cust->order_pkg({ cust_pkg => $pkg });
+my $date = str2time('2016-04-01');
+set_fixed_time($date);
+$error ||= $cust->bill_and_collect;
+BAIL_OUT($error) if $error;
+
+# get the form
+my %args = ( pkgnum => $pkg->pkgnum,
+ pkgpart => $pkg->pkgpart,
+ locationnum => -1);
+$FS->post('/misc/change_pkg.cgi', %args);
+my $form = $FS->form('OrderPkgForm');
+
+# Schedule the package change two days from now.
+$date += 86400*2;
+my $date_str = time2str('%x', $date);
+
+my %params = (
+ start_date => $date_str,
+ delay => 1,
+ address1 => int(rand(1000)) . ' Changed Street',
+ city => 'New City',
+ state => 'CA',
+ zip => '90001',
+ country => 'US',
+);
+
+diag "requesting location change to $params{address1}";
+
+foreach (keys %params) {
+ $form->value($_, $params{$_});
+}
+$FS->post($form);
+ok( $FS->error eq '' , 'form posted' );
+if ( ok( $FS->page =~ m[location.reload], 'location change accepted' )) {
+ #nothing
+} else {
+ $FS->post($FS->redirect);
+ BAIL_OUT( $FS->error);
+}
+# check that the package change is set
+$pkg = $pkg->replace_old;
+my $new_pkgnum = $pkg->change_to_pkgnum;
+ok( $new_pkgnum, 'package change is scheduled' );
+
+# run it and check that the package change happened
+diag("billing customer on $date_str");
+set_fixed_time($date);
+my $error = $cust->bill_and_collect;
+BAIL_OUT($error) if $error;
+
+$pkg = $pkg->replace_old;
+ok($pkg->get('cancel'), "old package is canceled");
+my $new_pkg = $FS->qsearchs('cust_pkg', { pkgnum => $new_pkgnum });
+ok($new_pkg->setup, "new package is active");
+ok($new_pkg->cust_location->address1 eq $params{'address1'}, "new location is correct")
+ or diag $new_pkg->cust_location->address1;
+
+1;
+
diff --git a/FS/t/suite/08-sales_tax.t b/FS/t/suite/08-sales_tax.t
new file mode 100755
index 000000000..bf1ae48c8
--- /dev/null
+++ b/FS/t/suite/08-sales_tax.t
@@ -0,0 +1,76 @@
+#!/usr/bin/perl
+
+=head2 DESCRIPTION
+
+Tests basic sales tax calculations, including consolidation and rounding.
+The invoice will have two charges that add up to $50 and two taxes:
+- Tax 1, 8.25%, for $4.125 in tax, which will round up.
+- Tax 2, 8.245%, for $4.1225 in tax, which will round down.
+
+Correct: The invoice will have one line item for each of those taxes, with
+the correct amount.
+
+=cut
+
+use strict;
+use Test::More tests => 2;
+use FS::Test;
+use Date::Parse 'str2time';
+use Date::Format 'time2str';
+use Test::MockTime qw(set_fixed_time);
+use FS::cust_main;
+use FS::cust_pkg;
+use FS::Conf;
+my $FS= FS::Test->new;
+
+# test configuration
+my @taxes = (
+ [ 'Tax 1', 8.250, 4.13 ],
+ [ 'Tax 2', 8.245, 4.12 ],
+);
+
+# Create the customer and charge them
+my $cust = $FS->new_customer('Basic taxes');
+$cust->bill_location->state('AZ'); # move it away from the default of CA
+my $error;
+$error = $cust->insert;
+BAIL_OUT("can't create test customer: $error") if $error;
+$error = $cust->charge( {
+ amount => 25.00,
+ pkg => 'Test charge 1',
+} ) ||
+$cust->charge({
+ amount => 25.00,
+ pkg => 'Test charge 2',
+});
+BAIL_OUT("can't create test charges: $error") if $error;
+
+# Create tax defs
+foreach my $tax (@taxes) {
+ my $cust_main_county = FS::cust_main_county->new({
+ 'country' => 'US',
+ 'state' => 'AZ',
+ 'exempt_amount' => 0.00,
+ 'taxname' => $tax->[0],
+ 'tax' => $tax->[1],
+ });
+ $error = $cust_main_county->insert;
+ BAIL_OUT("can't create tax definitions: $error") if $error;
+}
+
+# Bill the customer
+set_fixed_time(str2time('2016-03-10 08:00'));
+my @return;
+$error = $cust->bill( return_bill => \@return );
+BAIL_OUT("can't bill charges: $error") if $error;
+my $cust_bill = $return[0] or BAIL_OUT("no invoice generated");
+# Check amounts
+diag("Tax on 25.00 + 25.00");
+foreach my $cust_bill_pkg ($cust_bill->cust_bill_pkg) {
+ next if $cust_bill_pkg->pkgnum;
+ my ($tax) = grep { $_->[0] eq $cust_bill_pkg->itemdesc } @taxes;
+ if ( $tax ) {
+ ok ( $cust_bill_pkg->setup eq $tax->[2], "Tax at rate $tax->[1]% = $tax->[2]")
+ or diag("is ". $cust_bill_pkg->setup);
+ }
+}
diff --git a/FS/t/suite/WRITING b/FS/t/suite/WRITING
new file mode 100644
index 000000000..d9421cc7b
--- /dev/null
+++ b/FS/t/suite/WRITING
@@ -0,0 +1,93 @@
+WRITING TESTS
+
+Load the test database (kept in FS-Test/share/test.sql for now). This has
+a large set of customers in a known initial state. You can login through
+the web interface as "admin"/"admin" to examine the state of things and plan
+your test.
+
+The test scripts now have access to BOTH sides of the web interface, so you
+can create an object through the UI and then examine its internal
+properties, etc.
+
+ use Test::More tests => 1;
+ use FS::Test;
+ my $FS = FS::Test->new;
+
+$FS has qsearch and qsearchs methods for finding objects directly. You can
+do anything with those objects that Freeside backend code could normally do.
+For example, this will bill a customer:
+
+ my $cust = $FS->qsearchs('cust_main', { custnum => 52 });
+ my $error = $cust->bill;
+
+TESTING UI INTERACTION
+
+To fetch a page from the UI, use the post() method:
+
+ $FS->post('/view/cust_main.cgi?52');
+ ok( $FS->error eq '', 'fetched customer view' ) or diag($FS->error);
+ ok( $FS->page =~ /Customer, New/, 'customer is named "Customer, New"' );
+
+To simulate a user filling in and submitting a form, first fetch the form,
+and select it by name:
+
+ $FS->post('/edit/svc_acct.cgi?98');
+ my $form = $FS->form('OneTrueForm');
+
+then fill it in and submit it:
+
+ $form->value('clear_password', '1234abcd');
+ $FS->post($form);
+
+and examine the result:
+
+ my $svc_acct = $FS->qsearch('svc_acct', { svcnum => 98 });
+ ok( $svc_acct->_password eq '1234abcd', 'password was changed' );
+
+TESTING UI FLOW (EDIT/PROCESS/VIEW SEQUENCE)
+
+Forms for editing records will post to a processing page. $FS->post($form)
+handles this. The processing page will usually redirect back to the view
+page on success, and back to the edit form with an error on failure.
+Determine which kind of redirect it is. If it's a redirect to the edit form,
+you need to follow it to report the error.
+
+ if ( $FS->redirect =~ m[^/view/svc_acct.cgi] ) {
+
+ pass('redirected to view page');
+
+ } elsif ( $FS->redirect =~ m[^/edit/svc_acct.cgi] ) {
+
+ fail('redirected back to edit form');
+ $FS->post($FS->redirect);
+ diag($FS->error);
+
+ } else {
+
+ fail('unsure what happened');
+ diag($FS->page);
+
+ }
+
+RUNNING TESTS AT A SPECIFIC DATE
+
+Important for testing package billing. Test::MockTime provides the
+set_fixed_time() function, which will freeze the time returned by the time()
+function at a specific value. I recommend giving it a unix timestamp rather
+than a date string to avoid any confusion about time zones.
+
+Note that FS::Cron::bill and some other parts of the system look at the $^T
+variable (the time that the current program started running). You can
+override that by just assigning to the variable.
+
+Customers in the test database are billed up through Mar 1 2016. This will
+bill a customer for the next month after that:
+
+ use Test::MockTime qw(set_fixed_time);
+ use Date::Parse qw(str2time);
+
+ my $cust = $FS->qsearchs('cust_main', { custnum => 52 });
+ set_fixed_time( str2time('2016-04-01') );
+ $cust->bill;
+
+