add a paid_manually column and use that to mark an order having been paid manually
authorTony Cook <tony@develop-help.com>
Mon, 21 May 2012 06:15:59 +0000 (16:15 +1000)
committerTony Cook <tony@develop-help.com>
Mon, 21 May 2012 06:15:59 +0000 (16:15 +1000)
also, send the paid/unpaid note to the audit log

also, require a csrfp for these actions

MANIFEST
schema/bse.sql
site/cgi-bin/bse.cfg
site/cgi-bin/modules/BSE/Shop/PaymentTypes.pm [new file with mode: 0644]
site/cgi-bin/modules/BSE/Shop/Util.pm
site/cgi-bin/modules/BSE/TB/Order.pm
site/cgi-bin/modules/BSE/UI/AdminShop.pm
site/templates/admin/order_detail.tmpl
site/templates/preload.tmpl [new file with mode: 0644]
site/util/mysql.str

index 0b16e08..c95b0d3 100644 (file)
--- a/MANIFEST
+++ b/MANIFEST
@@ -139,6 +139,7 @@ site/cgi-bin/modules/BSE/Search/BSE.pm
 site/cgi-bin/modules/BSE/Session.pm
 site/cgi-bin/modules/BSE/SessionSign.pm
 site/cgi-bin/modules/BSE/Shipping.pm
+site/cgi-bin/modules/BSE/Shop/PaymentTypes.pm
 site/cgi-bin/modules/BSE/Shop/Util.pm
 site/cgi-bin/modules/BSE/Sort.pm
 site/cgi-bin/modules/BSE/Storage/AmazonS3.pm
@@ -717,6 +718,7 @@ site/templates/menu/menu4.tmpl
 site/templates/menu/menu5.tmpl
 site/templates/meta/flv_flowplaylist.tmpl
 site/templates/noartbase.tmpl
+site/templates/preload.tmpl
 site/templates/printable/printable.tmpl
 site/templates/printable/wap.tmpl
 site/templates/search_base.tmpl
index 3527024..9d0652a 100644 (file)
@@ -342,6 +342,9 @@ create table orders (
   -- truncated credit card number
   ccPAN varchar(4) not null default '',
 
+  -- true if the order was paid manually
+  paid_manually integer not null default 0,
+
   primary key (id),
   index order_cchash(ccNumberHash),
   index order_userId(userId, orderDate)
index 5afaaa3..4f3b008 100644 (file)
@@ -9,6 +9,7 @@ public_files=/managed_assets/
 randomdata = /dev/urandom
 access_control=0
 file_handlers=flv
+preload_template=preload.tmpl
 
 [paths]
 ; the following needs to be set to a path writable by the BSE processes
diff --git a/site/cgi-bin/modules/BSE/Shop/PaymentTypes.pm b/site/cgi-bin/modules/BSE/Shop/PaymentTypes.pm
new file mode 100644 (file)
index 0000000..98160fa
--- /dev/null
@@ -0,0 +1,148 @@
+package BSE::Shop::PaymentTypes;
+use strict;
+
+our $VERSION = "1.000";
+
+use Exporter qw(import);
+
+our @EXPORT_OK = qw(PAYMENT_CC PAYMENT_CHEQUE PAYMENT_CALLME PAYMENT_MANUAL PAYMENT_PAYPAL payment_types);
+
+our @EXPORT = grep /^PAYMENT_/, @EXPORT_OK;
+
+our %EXPORT_TAGS =
+  (
+   default => \@EXPORT,
+  );
+
+use constant PAYMENT_CC => 0;
+use constant PAYMENT_CHEQUE => 1;
+use constant PAYMENT_CALLME => 2;
+use constant PAYMENT_MANUAL => 3;
+use constant PAYMENT_PAYPAL => 4;
+
+my @types =
+  (
+   {
+    id => PAYMENT_CC,
+    name => 'CC',
+    desc => 'Credit Card',
+    require => [ qw/cardNumber cardExpiry ccName/ ],
+   },
+   {
+    id => PAYMENT_CHEQUE,
+    name => 'Cheque',
+    desc => 'Cheque',
+    require => [],
+   },
+   {
+    id => PAYMENT_CALLME,
+    name => 'CallMe',
+    desc => 'Call customer for payment',
+    require => [],
+   },
+   {
+    id => PAYMENT_MANUAL,
+    name => 'Manual',
+    desc => 'Manual',
+    require => [],
+   },
+   {
+    id => PAYMENT_PAYPAL,
+    name => "PayPal",
+    desc => "PayPal",
+    require => [],
+   },
+  );
+
+=item payment_types($cfg)
+
+Returns payment type ids, and hashes describing each of the configured
+payment types.
+
+These are used to generate the tags used for testing whether payment
+types are available.  Also used for validating payment type
+information.
+
+=cut
+
+sub payment_types {
+  my ($cfg) = @_;
+
+  $cfg ||= BSE::Cfg->single;
+
+  my %types = map { $_->{id} => $_ } @types;
+
+  my @payment_types = split /,/, $cfg->entry('shop', 'payment_types', '0');
+
+  my %all_types = map { $_ => 1 } keys %types, @payment_types;
+  
+  for my $type (keys %all_types) {
+    my $hash = $types{$type}; # avoid autovivification
+    my $name = $cfg->entry('payment type names', $type, $hash->{name} || "Type 0 has no name");
+    my $desc = $cfg->entry('payment type descs', $type, 
+                          $hash->{desc} || $name);
+    my $enabled = !$cfg->entry('payment type disable', $hash->{name}, 0);
+    my @require = $hash->{require} ? @{$hash->{require}} : ();
+    @require = split /,/, $cfg->entry('payment type required', $type,
+                                     join ",", @require);
+    $types{$type} = 
+      {
+       id => $type,
+       name => $name, 
+       desc => $desc,
+       require => \@require,
+       sort => scalar $cfg->entry("payment type sort", $hash->{name}, $type),
+      };
+  }
+
+  for my $type (@payment_types) {
+    unless ($types{$type}) {
+      print STDERR "** payment type $type doesn't have a name defined\n";
+      next;
+    }
+    $types{$type}{enabled} = 1;
+  }
+
+  # credit card payments require either encrypted emails enabled or
+  # an online CC processing module
+  if ($types{+PAYMENT_CC}) {
+    my $noencrypt = $cfg->entryBool('shop', 'noencrypt', 0);
+    my $ccprocessor = $cfg->entry('shop', 'cardprocessor');
+
+    if ($noencrypt && !$ccprocessor) {
+      $types{+PAYMENT_CC}{enabled} = 0;
+      $types{+PAYMENT_CC}{message} =
+       "No card processor configured and encryption disabled";
+    }
+  }
+
+  # paypal requires api confguration
+  if ($types{+PAYMENT_PAYPAL} && $types{+PAYMENT_PAYPAL}{enabled}) {
+    require BSE::PayPal;
+
+    unless (BSE::PayPal->configured) {
+      $types{+PAYMENT_PAYPAL}{enabled} = 0;
+      $types{+PAYMENT_PAYPAL}{message} = "No API configuration";
+    }
+  }
+
+  return sort { $a->{sort} <=> $b->{sort} } values %types;
+}
+
+1;
+
+=head1 NAME
+
+BSE::Shop::PaymentTypes - defines payment type constants.
+
+=head1 SYNOPSIS
+
+  use BSE::Shop::PaymentTypes;
+
+  $order->set_paymentType(PAYMENT_CC);
+
+  use BSE::Shop::PaymentTypes 'payment_types';
+
+  my @types = payment_types();
+
+=cut
index 73dbcbe..bfcd1bd 100644 (file)
@@ -7,7 +7,7 @@ use vars qw(@ISA @EXPORT_OK);
                 payment_types order_item_opts
  PAYMENT_CC PAYMENT_CHEQUE PAYMENT_CALLME PAYMENT_MANUAL PAYMENT_PAYPAL/;
 
-our $VERSION = "1.006";
+our $VERSION = "1.007";
 
 our %EXPORT_TAGS =
   (
@@ -20,12 +20,7 @@ use BSE::Util::Tags qw(tag_article);
 use BSE::CfgInfo qw(custom_class);
 use Carp 'confess';
 use BSE::Util::HTML qw(escape_html);
-
-use constant PAYMENT_CC => 0;
-use constant PAYMENT_CHEQUE => 1;
-use constant PAYMENT_CALLME => 2;
-use constant PAYMENT_MANUAL => 3;
-use constant PAYMENT_PAYPAL => 4;
+use BSE::Shop::PaymentTypes qw(:default payment_types);
 
 =item shop_cart_tags($acts, $cart, $cart_prods, $req, $stage)
 
@@ -428,104 +423,4 @@ sub need_logon {
   return;
 }
 
-=item payment_types($cfg)
-
-Returns payment type ids, and hashes describing each of the configured
-payment types.
-
-These are used to generate the tags used for testing whether payment
-types are available.  Also used for validating payment type
-information.
-
-=cut
-
-sub payment_types {
-  my ($cfg) = @_;
-
-  my @types =
-    (
-     {
-      id => PAYMENT_CC, 
-      name => 'CC', 
-      desc => 'Credit Card',
-      require => [ qw/cardNumber cardExpiry ccName/ ],
-     },
-     {
-      id => PAYMENT_CHEQUE, 
-      name => 'Cheque', 
-      desc => 'Cheque',
-      require => [],
-     },
-     {
-      id => PAYMENT_CALLME,
-      name => 'CallMe',
-      desc => 'Call customer for payment',
-      require => [],
-     },
-     {
-      id => PAYMENT_PAYPAL,
-      name => "PayPal",
-      desc => "PayPal",
-      require => [],
-     },
-    );
-  my %types = map { $_->{id} => $_ } @types;
-
-  my @payment_types = split /,/, $cfg->entry('shop', 'payment_types', '0');
-  
-  for my $type (@payment_types) {
-    my $hash = $types{$type}; # avoid autovivification
-    my $name = $cfg->entry('payment type names', $type, $hash->{name});
-    my $desc = $cfg->entry('payment type descs', $type, 
-                          $hash->{desc} || $name);
-    my $enabled = !$cfg->entry('payment type disable', $hash->{name}, 0);
-    my @require = $hash->{require} ? @{$hash->{require}} : ();
-    @require = split /,/, $cfg->entry('payment type required', $type,
-                                     join ",", @require);
-    if ($name && $desc) {
-      $types{$type} = 
-       {
-        id => $type,
-        name => $name, 
-        desc => $desc,
-        require => \@require,
-       };
-    }
-  }
-
-  for my $type (@payment_types) {
-    unless ($types{$type}) {
-      print STDERR "** payment type $type doesn't have a name defined\n";
-      next;
-    }
-    $types{$type}{enabled} = 1;
-  }
-
-  # credit card payments require either encrypted emails enabled or
-  # an online CC processing module
-  if ($types{+PAYMENT_CC}) {
-    my $noencrypt = $cfg->entryBool('shop', 'noencrypt', 0);
-    my $ccprocessor = $cfg->entry('shop', 'cardprocessor');
-
-    if ($noencrypt && !$ccprocessor) {
-      $types{+PAYMENT_CC}{enabled} = 0;
-      $types{+PAYMENT_CC}{message} =
-       "No card processor configured and encryption disabled";
-    }
-  }
-
-  # paypal requires api confguration
-  if ($types{+PAYMENT_PAYPAL} && $types{+PAYMENT_PAYPAL}{enabled}) {
-    require BSE::PayPal;
-
-    unless (BSE::PayPal->configured) {
-      $types{+PAYMENT_PAYPAL}{enabled} = 0;
-      $types{+PAYMENT_PAYPAL}{message} = "No API configuration";
-    }
-  }
-
-  return values %types;
-}
-
-
 1;
index fce5ba9..f6bd334 100644 (file)
@@ -5,8 +5,9 @@ use Squirrel::Row;
 use vars qw/@ISA/;
 @ISA = qw/Squirrel::Row/;
 use Carp 'confess';
+use BSE::Shop::PaymentTypes;
 
-our $VERSION = "1.015";
+our $VERSION = "1.016";
 
 sub columns {
   return qw/id
@@ -28,7 +29,8 @@ sub columns {
            ccStatus2 ccTranId complete delivOrganization billOrganization
            delivStreet2 billStreet2 purchase_order shipping_method
            shipping_name shipping_trace
-          paypal_token paypal_tran_id freight_tracking stage ccPAN/;
+          paypal_token paypal_tran_id freight_tracking stage ccPAN
+          paid_manually/;
 }
 
 sub table {
@@ -96,6 +98,7 @@ sub defaults {
      freight_tracking => "",
      stage => "incomplete",
      ccPAN => "",
+     paid_manually => 0,
     );
 }
 
@@ -120,7 +123,7 @@ sub payment_columns {
   return qw/ccNumberHash ccName ccExpiryHash ccType
            paidFor paymentReceipt paymentType
            ccOnline ccSuccess ccReceipt ccStatus ccStatusText
-           ccStatus2 ccTranId/;
+           ccStatus2 ccTranId ccPAN paid_manually/;
 }
 
 =item billing_to_delivery_map
@@ -667,4 +670,18 @@ sub set_ccPANTruncate {
   $self->set_ccPAN($pan);
 }
 
+=item is_manually_paid
+
+Returns true if the order is marked as manually paid, either through
+the older PAYMENT_MANUAL paymentType value or via the newer flag.
+
+=cut
+
+sub is_manually_paid {
+  my ($self) = @_;
+
+  return $self->paidFor &&
+    ($self->paid_manually || $self->paymentType == PAYMENT_MANUAL);
+}
+
 1;
index 6d61646..1f1c324 100644 (file)
@@ -17,9 +17,9 @@ use BSE::Util::Iterate;
 use BSE::WebUtil 'refresh_to_admin';
 use BSE::Util::HTML qw(:default popup_menu);
 use BSE::Arrows;
-use BSE::Shop::Util qw(:payment order_item_opts nice_options);
+use BSE::Shop::Util qw(:payment order_item_opts nice_options payment_types);
 
-our $VERSION = "1.012";
+our $VERSION = "1.013";
 
 my %actions =
   (
@@ -754,6 +754,30 @@ sub tag_stage_select {
     );
 }
 
+=item target order_detail
+
+Display the details of an order.
+
+Variables set:
+
+=over
+
+=item *
+
+order - the order being displayed
+
+=item *
+
+payment_types - a list of configured payment types
+
+=item *
+
+payment_type_desc - a description of the current payment type
+
+=back
+
+=cut
+
 sub req_order_detail {
   my ($class, $req, $errors) = @_;
 
@@ -771,6 +795,11 @@ sub req_order_detail {
     my $siteuser;
     my $it = BSE::Util::Iterate->new;
 
+    $req->set_variable(order => $order);
+    my @pay_types = payment_types();
+    $req->set_variable(payment_types => \@pay_types);
+    my ($pay_type) = grep $_->{id} == $order->paymentType, @pay_types;
+    $req->set_variable(payment_type_desc => $pay_type ? $pay_type->{desc} : "Unknown");
     my %acts;
     %acts =
       (
@@ -855,16 +884,41 @@ sub req_order_filled {
   }
 }
 
+=item target order_paid
+
+Mark the order identified by C<id> as paid.
+
+Optionally accepts C<paymentType> which replaces the current payment
+type.
+
+Requires csrfp token C<shop_order_paid>.
+
+=cut
+
 sub req_order_paid {
-  my ($class, $req) = @_;
+  my ($self, $req) = @_;
 
-  return $class->_set_order_paid($req, 1);
+  $req->check_csrf("shop_order_paid")
+    or return $self->req_order_list($req, "Bad or missing csrf token: " . $req->csrf_error);
+
+  return $self->_set_order_paid($req, 1);
 }
 
+=item target order_unpaid
+
+Mark the order identified by C<id> as unpaid.
+
+Requires csrfp token C<shop_order_unpaid>.
+
+=cut
+
 sub req_order_unpaid {
-  my ($class, $req) = @_;
+  my ($self, $req) = @_;
 
-  return $class->_set_order_paid($req, 0);
+  $req->check_csrf("shop_order_unpaid")
+    or return $self->req_order_list($req, "Bad or missing csrf token: " . $req->csrf_error);
+
+  return $self->_set_order_paid($req, 0);
 }
 
 sub _set_order_paid {
@@ -875,18 +929,37 @@ sub _set_order_paid {
       my $order = BSE::TB::Orders->getByPkey($id)) {
     if ($order->paidFor != $value) {
       if ($value) {
-       $order->set_paymentType(PAYMENT_MANUAL);
+       my $pay_type = $req->cgi->param("paymentType");
+       if (defined $pay_type && $pay_type =~ /^[0-9]+$/) {
+         $order->set_paymentType($pay_type);
+       }
+       $order->set_paid_manually(1);
       }
       else {
-       $order->paymentType == PAYMENT_MANUAL
+       $order->is_manually_paid
          or return $class->req_order_detail($req, "You can only unpay manually paid orders");
       }
 
       $order->set_paidFor($value);
-      my $user = $req->user;
-      my $name = $user ? $user->logon : "--unknown--";
-      require POSIX;
-      $order->{instructions} .= "\nMarked " . ($value ? "paid" : "unpaid" ) . " by $name " . POSIX::strftime("%H:%M %d/%m/%Y", localtime);
+
+      if ($value) {
+       $req->audit
+         (
+          component => "shopadmin:order:paid",
+          level => "info",
+          object => $order,
+          msg => "Order " . $order->id . " marked paid",
+         );
+      }
+      else {
+       $req->audit
+         (
+          component => "shopadmin:order:unpaid",
+          level => "info",
+          object => $order,
+          msg => "Order " . $order->id . " marked unpaid",
+         );
+      }
       $order->save();
     }
 
index 6b3dc01..5e4ab41 100644 (file)
@@ -162,62 +162,74 @@ Shipping via <:order shipping_method:>
   <input type="submit" name="a_order_save" value="Save" />
 </form>
 
-<:switch:>
-<:case Eq [order paidFor] "0":>
+<:.if !order.paidFor :>
 <p>This order hasn't been paid</p>
-<p>If the customer has paid by cheque, transfer, etc, you can <a href="<:adminurl shopadmin a_order_paid 1 id [order id]:>">mark this order paid</a></p>
-<:case order ccOnline:>
+
+<form action="<:adminurl2 shopadmin:>" method="post">
+<input type="hidden" name="id" value="<:= order.id :>" />
+<:csrfp shop_order_paid hidden:>
+Payment type: <:.call "make_select", "name": "paymentType", "list": payment_types, "id": "id", "desc": "desc", "default": order.paymentType -:>
+<input type="submit" name="a_order_paid" value="Mark this order paid" /></form>
+<:.elsif order.is_manually_paid -:>
+<form action="<:adminurl2 shopadmin:>" method="post">
+This order has been manually marked paid via <:= payment_type_desc | html :>
+<input type="hidden" name="id" value="<:= order.id :>" />
+<:csrfp shop_order_unpaid hidden:>
+<input type="submit" name="a_order_unpaid" value="Mark this order unpaid" /></form>
+
+<:.elsif order.ccOnline:>
 <p>This was processed as an online credit card transaction.</p>
-<:if Order ccSuccess:>
+<:.if order.ccSuccess:>
 <p>This transaction was <b>SUCCESSFUL</b>.</p>
 <table>
 <tr><th>Receipt Number:</th><td><:order ccReceipt:></td></tr>
-<:ifOrder ccTranId:>
+<:.if order.ccTranId:>
 <tr><th>Transaction Id:</th><td><:order ccTranId:></td></tr>
-<:or:><:eif:>
-<:ifOrder ccName:>
+<:.end if:>
+<:.if order.ccName:>
 <tr><th>Card Holder:</th><td><:order ccName:></td>
-<:or:><:eif:>
-<:ifOrder ccPAN:>
+<:.end if:>
+<:.if order.ccPAN:>
 <tr><th>Card Number (partial):</th><td>...<:order ccPAN:></td>
-<:or:><:eif:>
+<:.end if:>
 </table>
-<:or Order:>
+<:.else:>
 <p>This transaction <b>FAILED</b>.</p>
 <table>
 <tr><th>Status:</th><td><:order ccStatus:></td>
 <tr><th>Error:</th><td><:order ccStatusText:></td>
-<:ifOrder ccStatus2:>
+<:.if order ccStatus2:>
 <tr><th>More status</th><td><:order ccStatus2:></td>
-<:or:><:eif:>
+<:.end if:>
 </table>
-<:eif Order:>
-<:case Eq [order paymentType] "0":><p>Payment made by credit card.  Credit card details can be found in the encrypted email you received when the customer made the order.</p>
-<:if Or [order ccPAN] [order ccName]:>
+<:.end if:>
+<:.elsif order.paymentType == 0:>
+<p>Payment made by credit card.  Credit card details can be found in the encrypted email you received when the customer made the order.</p>
+<:.if order.ccPAN or order.ccName :>
 <table>
-<:ifOrder ccName:>
+<:.if order.ccName:>
 <tr><th>Card Holder</th><td><:order ccName:></td>
-<:or:><:eif:>
-<:ifOrder ccPAN:>
+<:.end if:>
+<:.if order.ccPAN:>
 <tr><th>Card Number (partial)</th><td>...<:order ccPAN:></td>
-<:or:><:eif:>
+<:.end if:>
 </table>
-<:or Or:><:eif Or:>
-<:case Eq [order paymentType] "1":><p>Payment will be made by cheque.</p>
-<:case Eq [order paymentType] "2":><p>Contact the customer to arrange for payment.</p>
-<:case Eq [order paymentType] "3":><p>Paid manually (staff marked this order paid) - you can <a href="<:adminurl shopadmin a_order_unpaid 1 id [order id]:>">unmark it</a></p>
-<:case Eq [order paymentType] "4":><p>Paid via PayPal, transaction id <:order paypal_tran_id:><:ifUserCan bse_shop_order_refund_paypal:> <a href="<:adminurl shopadmin a_paypal_refund 1 id [order id]:>">Refund</a><:or:><:eif:></p>
-<:endswitch:>
+<:.end if:>
+<:.elsif order.paymentType == 1 -:>
+  <p>Payment will be made by cheque.</p>
+<:.elsif order.paymentType == 2 -:>
+  <p>Contact the customer to arrange for payment.</p>
+<:.elsif order.paymentType == 4:><p>Paid via PayPal, transaction id <:order paypal_tran_id:><:ifUserCan bse_shop_order_refund_paypal:> <a href="<:adminurl shopadmin a_paypal_refund 1 id [order id]:>">Refund</a><:or:><:eif:></p>
+<:.end if:>
 <:include custom/order_detail_payment.include optional:>
-<:if Order filled:>
+<:.if order.filled:>
 <p>This order was filled on <:date order whenFilled:> by <:order whoFilled:>.</p>
-<:or Order:>
-<:ifOrder complete:>
-<p>This order hasn't been filled yet.  <a href="<:adminurl shopadmin id [order id] order_filled Yep filled 1 detail 1:>">Mark order filled</a>.</p><:or:><:eif:>
-<:eif Order:>
-<:if Order instructions:>
+<:.elsif order.complete:>
+<p>This order hasn't been filled yet.  <a href="<:adminurl shopadmin id [order id] order_filled Yep filled 1 detail 1:>">Mark order filled</a>.</p>
+<:.end if:>
+<:.if order.instructions:>
 <p style="white-space: pre-wrap;"><:order instructions:></p>
-<:or Order:><:eif Order:>
+<:.end if:>
 
 <table class="editform" id="auditlog">
 <:include admin/include/audithead.tmpl:>
diff --git a/site/templates/preload.tmpl b/site/templates/preload.tmpl
new file mode 100644 (file)
index 0000000..a2d8ff0
--- /dev/null
@@ -0,0 +1,11 @@
+<:# utility definitions :>
+<:-.define make_select-:>
+  <select name="<:= name :>">
+  <:-.for i in list -:>
+    <option value="<:= i[id] |html :>"
+      <:- .if i[id] eq default :> selected="selected"<:.end if -:>
+    >
+    <:= i[desc] | html :>
+  <:-.end for-:>
+  </select>
+<:-.end define -:>
index be0e7d6..2c5ac41 100644 (file)
@@ -630,6 +630,7 @@ Column paypal_tran_id;varchar(255);YES;NULL;
 Column freight_tracking;varchar(255);NO;;
 Column stage;varchar(20);NO;;
 Column ccPAN;varchar(4);NO;;
+Column paid_manually;int(11);NO;0;
 Index PRIMARY;1;[id]
 Index order_cchash;0;[ccNumberHash]
 Index order_userId;0;[userId;orderDate]