PayPal support for BSE
authorTony Cook <tony@develop-help.com>
Mon, 1 Nov 2010 05:34:41 +0000 (05:34 +0000)
committertony <tony@45cb6cf1-00bc-42d2-bb5a-07f51df49f94>
Mon, 1 Nov 2010 05:34:41 +0000 (05:34 +0000)
28 files changed:
MANIFEST
schema/bse.sql
site/cgi-bin/admin/shopadmin.pl
site/cgi-bin/modules/BSE/Cfg.pm
site/cgi-bin/modules/BSE/DB/Mysql.pm
site/cgi-bin/modules/BSE/PayPal.pm [new file with mode: 0644]
site/cgi-bin/modules/BSE/Request/Base.pm
site/cgi-bin/modules/BSE/Shop/Util.pm
site/cgi-bin/modules/BSE/TB/AuditEntry.pm
site/cgi-bin/modules/BSE/TB/AuditLog.pm
site/cgi-bin/modules/BSE/TB/Order.pm
site/cgi-bin/modules/BSE/TB/OrderItem.pm
site/cgi-bin/modules/BSE/UI/AdminShop.pm
site/cgi-bin/modules/BSE/UI/Dispatch.pm
site/cgi-bin/modules/BSE/UI/Shop.pm
site/cgi-bin/modules/BSE/Util/Tags.pm
site/data/db/bse_msg_base.data
site/data/db/bse_msg_defaults.data
site/docs/config.pod
site/htdocs/css/admin.css
site/templates/admin/include/auditentry.tmpl [new file with mode: 0644]
site/templates/admin/include/audithead.tmpl [new file with mode: 0644]
site/templates/admin/order_detail.tmpl
site/templates/checkoutfinal_base.tmpl
site/templates/checkoutpay_base.tmpl
site/templates/mailconfirm.tmpl
site/templates/mailorder.tmpl
site/util/mysql.str

index 1938cc4..60a5103 100644 (file)
--- a/MANIFEST
+++ b/MANIFEST
@@ -126,6 +126,7 @@ site/cgi-bin/modules/BSE/Password/Plain.pm
 site/cgi-bin/modules/BSE/Passwords.pm
 site/cgi-bin/modules/BSE/Permissions.pm
 site/cgi-bin/modules/BSE/ProductImportXLS.pm
+site/cgi-bin/modules/BSE/PayPal.pm
 site/cgi-bin/modules/BSE/Report.pm
 site/cgi-bin/modules/BSE/Request.pm
 site/cgi-bin/modules/BSE/Request/Base.pm
@@ -470,6 +471,8 @@ site/templates/admin/grouplist.tmpl
 site/templates/admin/helpicon.tmpl     Help icon template for admin templates
 site/templates/admin/image_edit.tmpl   Edit a single image
 site/templates/admin/interestemail.tmpl
+site/templates/admin/include/audithead.tmpl
+site/templates/admin/include/auditentry.tmpl
 site/templates/admin/logon.tmpl
 site/templates/admin/memberupdate/import.tmpl
 site/templates/admin/memberupdate/preview.tmpl
index fa3e11c..1ad51db 100644 (file)
@@ -324,6 +324,12 @@ create table orders (
   -- trace of the request and response
   shipping_trace text null,
 
+  -- paypal stuff
+  -- token from SetExpressCheckout
+  paypal_token varchar(255) null,
+  
+  paypal_tran_id varchar(255) null,
+
   primary key (id),
   index order_cchash(ccNumberHash),
   index order_userId(userId, orderDate)
index 9383801..054953a 100755 (executable)
@@ -1,6 +1,6 @@
 #!/usr/bin/perl -w
 # -d:ptkdb
-BEGIN { $ENV{DISPLAY} = '192.168.32.51:0.0'; }
+BEGIN { $ENV{DISPLAY} = '192.168.32.54:0.0'; }
 
 use strict;
 use FindBin;
index f74fb9b..3914e4b 100644 (file)
@@ -92,6 +92,47 @@ sub charset {
   return $single->entry('html', 'charset', 'iso-8859-1');
 }
 
+=item user_url($script, $target)
+
+=cut
+
+sub user_url {
+  my ($cfg, $script, $target, @options) = @_;
+
+  my $base = $script eq 'shop' ? $cfg->entryVar('site', 'secureurl') : '';
+  my $template;
+  if ($target) {
+    if ($script eq 'nuser') {
+      $template = "/cgi-bin/nuser.pl/user/TARGET";
+    }
+    else {
+      $template = "$base/cgi-bin/$script.pl?a_TARGET=1";
+    }
+    $template = $cfg->entry('targets', $script, $template);
+    $template =~ s/TARGET/$target/;
+  }
+  else {
+    if ($script eq 'nuser') {
+      $template = "/cgi-bin/nuser.pl/user";
+    }
+    else {
+      $template = "$base/cgi-bin/$script.pl";
+    }
+    $template = $cfg->entry('targets', $script.'_n', $template);
+  }
+  if (@options) {
+    $template .= $template =~ /\?/ ? '&' : '?';
+    my @entries;
+    while (my ($key, $value) = splice(@options, 0, 2)) {
+      require BSE::Util::HTML;
+      push @entries, "$key=" . BSE::Util::HTML::escape_uri($value);
+    }
+    $template .= join '&', @entries;
+  }
+
+  return $template;
+}
+
 1;
 
 =head1 AUTHOR
index 1ebb2eb..07328c2 100644 (file)
@@ -151,8 +151,8 @@ SQL
    Orders => 'select * from orders',
    getOrderByPkey => 'select * from orders where id = ?',
    getOrderItemByOrderId => 'select * from order_item where orderId = ?',
-   addOrder => 'insert orders values(null,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)',
-   replaceOrder => 'replace orders values(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)',
+   addOrder => 'insert orders values(null,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)',
+   replaceOrder => 'replace orders values(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)',
    addOrderItem => 'insert order_item values(null,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)',
    replaceOrderItem => 'replace order_item values(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)',
    getOrderByUserId => 'select * from orders where userId = ?',
diff --git a/site/cgi-bin/modules/BSE/PayPal.pm b/site/cgi-bin/modules/BSE/PayPal.pm
new file mode 100644 (file)
index 0000000..88eeeb9
--- /dev/null
@@ -0,0 +1,427 @@
+package BSE::PayPal;
+use strict;
+use BSE::Cfg;
+use BSE::Util::HTML;
+use BSE::Shop::Util qw(:payment);
+use Carp qw(confess);
+
+use constant DEF_TEST_WS_URL => "https://api-3t.sandbox.paypal.com/nvp";
+use constant DEF_TEST_REFRESH_URL => "https://www.sandbox.paypal.com/webscr";
+
+use constant DEF_LIVE_WS_URL => "https://api-3t.paypal.com/nvp";
+use constant DEF_LIVE_REFRESH_URL => "https://www.paypal.com/cgibin/webscr";
+
+my %defs =
+  (
+   test_ws_url => DEF_TEST_WS_URL,
+   test_refresh_url => DEF_TEST_REFRESH_URL,
+
+   live_ws_url => DEF_LIVE_WS_URL,
+   live_refresh_url => DEF_LIVE_REFRESH_URL,
+  );
+
+sub _test {
+  my ($cfg) = @_;
+
+  return $cfg->entry("paypal", "test", 1);
+}
+
+sub _cfg {
+  my ($cfg, $key) = @_;
+
+  my $realkey = _test($cfg) ? "test_$key" : "live_$key";
+  if (exists $defs{$realkey}) {
+    return $cfg->entry("paypal", $realkey, $defs{$realkey});
+  }
+  else {
+    return $cfg->entryErr("paypal", $realkey);
+  }
+}
+
+sub _base_ws_url {
+  my ($cfg) = @_;
+
+  return _cfg($cfg, "ws_url");
+}
+
+sub _base_refresh_url {
+  my ($cfg) = @_;
+
+  return _cfg($cfg, "refresh_url");
+}
+
+sub _api_signature {
+  my ($cfg) = @_;
+
+  return _cfg($cfg, "api_signature");
+}
+
+sub _api_username {
+  my ($cfg) = @_;
+
+  return _cfg($cfg, "api_username");
+}
+
+sub _api_password {
+  my ($cfg) = @_;
+
+  return _cfg($cfg, "api_password");
+}
+
+sub _format_amt {
+  my ($price) = @_;
+
+  return sprintf("%d.%02d", int($price / 100), $price % 100);
+}
+
+sub _order_amt {
+  my ($order) = @_;
+
+  return _format_amt($order->total);
+}
+
+sub _order_currency {
+  my ($order) = @_;
+
+  return BSE::Cfg->single->entry("shop", "currency", "AUD")
+}
+
+sub payment_url {
+  my ($class, %opts) = @_;
+
+  my $order = delete $opts{order}
+    or confess "Missing order";
+  my $rmsg = delete $opts{msg}
+    or confess "Missing msg";
+  my $who = delete $opts{user} || "U";
+  my $cfg = BSE::Cfg->single;
+
+  my %info = _set_express_checkout($cfg, $order, $who, $rmsg)
+    or return;
+
+  $order->set_paypal_token($info{TOKEN});
+  $order->save;
+
+  my $url = _make_url(_base_refresh_url($cfg),
+                     {
+                      cmd => "_express-checkout",
+                      token => $info{TOKEN},
+                      useraction => "confirm",
+                      AMT => _order_amt($order),
+                      CURRENCYCODE => _order_currency($order)
+                     }
+                    );
+
+#   BSE::TB::AuditLog->log
+#       (
+#        component => "shop:paypal:paymenturl",
+#        level => "debug",
+#        object => $order,
+#        actor => $who,
+#        msg => "URL $url",
+#       );
+
+  return $url;
+}
+
+# the _api_*() functions will die if not configured
+sub configured {
+  my $cfg = BSE::Cfg->single;
+
+  return eval
+    {
+      _api_username($cfg) && _api_password($cfg) && _api_signature($cfg);
+      1;
+    };
+}
+
+sub pay_order {
+  my ($class, %opts) = @_;
+
+  my $order = delete $opts{order}
+    or confess "Missing order";
+  my $req = delete $opts{req}
+    or confess "Missing req";
+  my $rmsg = delete $opts{msg}
+    or confess "Missing msg";
+
+  my $cgi = $req->cgi;
+  my $cfg = $req->cfg;
+  my $token = $cgi->param("token");
+  unless ($token) {
+    $$rmsg = $req->catmsg("msg:bse/shop/paypal/notoken");
+    return;
+  }
+  my $payerid = $cgi->param("PayerID");
+  unless ($payerid) {
+    $$rmsg = $req->catmsg("msg:bse/shop/paypal/nopayerid");
+    return;
+  }
+  unless ($token eq $order->paypal_token) {
+    print STDERR "cgi $token order ", $order->paypal_token, "\n";
+    $$rmsg = $req->catmsg("msg:bse/shop/paypal/badtoken");
+    return;
+  }
+
+  my %info;
+  $DB::single = 1;
+  if (_do_express_checkout_payment
+      ($cfg, $rmsg, $order, scalar($req->siteuser), $token, $payerid, \%info)) {
+    $order->set_paypal_tran_id($info{TRANSACTIONID});
+
+  }
+  elsif (keys %info) {
+    unless ($info{L_ERRORCODE}
+           && $info{L_ERRORCODE} == 10415
+           && $info{CHECKOUTSTATUS}
+           && $info{CHECKOUTSTATUS} eq "PaymentActionCompleted"
+           && $info{PAYMENTREQUEST_0_TRANSACTIONID}) {
+      return; # something else went wrong
+    }
+
+    # already processed, maybe there was an error when the user first
+    # returned, treat it as completed
+    $order->set_paypal_tran_id($info{PAYMENTREQUEST_0_TRANSACTIONID});
+  }
+  $order->set_paypal_token("");
+  $order->set_paidFor(1);
+  $order->set_paymentType(PAYMENT_PAYPAL);
+  $order->set_complete(1);
+  $order->save;
+  BSE::TB::AuditLog->log
+      (
+       component => "shop:paypal:pay",
+       level => "notice",
+       object => $order,
+       actor => scalar($req->siteuser) || "U",
+       msg => "Order paid via Paypal, transaction ".$order->paypal_tran_id,
+      );
+
+  return 1;
+}
+
+sub refund_order {
+  my ($class, %opts) = @_;
+
+  my $order = delete $opts{order}
+    or confess "Missing order";
+  my $rmsg = delete $opts{msg}
+    or confess "Missing msg";
+  my $req = delete $opts{req}
+    or confess "Missing req";
+
+  unless ($order->paymentType eq PAYMENT_PAYPAL) {
+    $$rmsg = "This order was not paid by paypal";
+    return;
+  }
+
+  my $cfg = BSE::Cfg->single;
+  my %info = _do_refund_transaction($cfg, $rmsg, $order, scalar($req->user))
+    or return;
+
+  $order->set_paidFor(0);
+  $order->save;
+
+  BSE::TB::AuditLog->log
+      (
+       component => "shop:paypal:refund",
+       level => "notice",
+       object => $order,
+       actor => scalar($req->user) || "U",
+       msg => "PayPal payment refunded, transaction $info{REFUNDTRANSACTIONID}",
+      );
+
+  return 1;
+}
+
+sub _do_refund_transaction {
+  my ($cfg, $rmsg, $order, $who) = @_;
+
+  my %params =
+    (
+     VERSION => "62.0",
+     TRANSACTIONID => $order->paypal_tran_id,
+     REFUNDTYPE => "Full",
+    );
+
+  my %info = _api_req($cfg, $rmsg, $order, $who, "RefundTransaction", \%params)
+    or return;
+
+  return %info;
+}
+
+sub _make_qparam {
+  my ($param) = @_;
+
+  return join("&", map { "$_=".escape_uri($param->{$_}) } sort keys %$param);
+}
+
+sub _make_url {
+  my ($base, $param) = @_;
+
+  my $sep = $base =~ /\?/ ? "&" : "?";
+
+  return $base . $sep . _make_qparam($param);
+}
+
+sub _shop_url {
+  my ($cfg, $action, @params) = @_;
+
+  return $cfg->user_url("shop", $action, @params);
+}
+
+sub _populate_from_order {
+  my ($params, $order, $cfg) = @_;
+
+  $params->{AMT} = _order_amt($order);
+  $params->{CURRENCYCODE} = _order_currency($order);
+
+  my $index = 0;
+  for my $item ($order->items) {
+    $params->{"L_NAME$index"} = $item->title;
+    $params->{"L_AMT$index"} = _format_amt($item->price);
+    $params->{"L_QTY$index"} = $item->units;
+    $params->{"L_NUMBER$index"} = $item->product_code
+      if $item->product_code;
+    ++$index;
+  }
+  $params->{SHIPPINGAMT} = _format_amt($order->shipping_cost)
+    if $order->shipping_cost;
+
+  # use our shipping information
+  my $country_code = $order->deliv_country_code;
+  if ($country_code && $cfg->entry("paypal", "shipping", 1)) {
+    $params->{SHIPTONAME} = $order->delivFirstName . " " . $order->delivLastName;
+    $params->{SHIPTOSTREET} = $order->delivStreet;
+    $params->{SHIPTOSTREET2} = $order->delivStreet2;
+    $params->{SHIPTOCITY} = $order->delivSuburb;
+    $params->{SHIPTOSTATE} = $order->delivState;
+    $params->{SHIPTOZIP} = $order->delivPostCode;
+    $params->{SHIPTOCOUNTRYCODE} = $country_code;
+    $params->{ADDROVERRIDE} = 1;
+  }
+  else {
+    $params->{NOSHIPPING} = 1;
+  }
+}
+
+sub _set_express_checkout {
+  my ($cfg, $order, $who, $rmsg) = @_;
+
+  my %params =
+    (
+     $cfg->entriesCS("paypal custom"),
+     VERSION => "62.0",
+     RETURNURL => _shop_url($cfg, "paypalret", order => $order->randomId),
+     CANCELURL => _shop_url($cfg, "paypalcan", order => $order->randomId),
+     PAYMENTACTION => "Sale",
+    );
+
+  _populate_from_order(\%params, $order, $cfg);
+
+  my %info = _api_req($cfg, $rmsg, $order, $who,"SetExpressCheckout",
+                     \%params)
+    or return;
+
+  unless ($info{TOKEN}) {
+    $$rmsg = "No token returned by PayPal";
+    return;
+  }
+
+  return %info;
+}
+
+ sub _get_express_checkout_details {
+   my ($cfg, $order, $who, $rmsg, $token) = @_;
+
+   my %params =
+     (
+      TOKEN => $token,
+      VERSION => "62.0",
+     );
+
+   my %info = _api_req($cfg, $rmsg, $order, $who, "GetExpressCheckoutDetails",
+                     \%params)
+     or return;
+
+   return %info;
+}
+
+sub _do_express_checkout_payment {
+  my ($cfg, $rmsg, $order, $who, $token, $payerid, $info) = @_;
+
+  my %params =
+    (
+     VERSION => "62.0",
+     PAYMENTACTION => "Sale",
+     TOKEN => $token,
+     PAYERID => $payerid,
+    );
+
+  _populate_from_order(\%params, $order, $cfg);
+
+  my %info = _api_req($cfg, $rmsg, $order, $who || "U", "DoExpressCheckoutPayment",
+                     \%params, $info)
+    or return;
+
+  return %info;
+}
+
+# Low level API request
+sub _api_req {
+  my ($cfg, $rmsg, $order, $who, $method, $param, $info) = @_;
+
+  $who ||= "U";
+
+  require LWP::UserAgent;
+  my $ua = LWP::UserAgent->new;
+  $param->{METHOD} = $method;
+  $param->{USER} = _api_username($cfg);
+  $param->{PWD} = _api_password($cfg);
+  $param->{SIGNATURE} = _api_signature($cfg);
+
+  my $post = _make_qparam($param);
+
+  my $req = HTTP::Request->new(POST => _base_ws_url($cfg));
+  $req->content($post);
+
+  my $result = $ua->request($req);
+
+  require BSE::TB::AuditLog;
+  BSE::TB::AuditLog->log
+      (
+       component => "shop:paypal",
+       function => $method,
+       level => "info",
+       object => $order,
+       actor => $who,
+       msg => "PayPal $method request",
+       dump => "Request:<<\n" . $req->as_string . "\n>>\n\nResult:<<\n" . $result->as_string . "\n>>",
+      );
+
+  my %info;
+  for my $entry (split /&/, $result->decoded_content) {
+    my ($key, $value) = split /=/, $entry, 2;
+    $info{$key} = unescape_uri($value);
+  }
+
+  %$info = %info if $info;
+  unless ($info{ACK} =~ /^Success/) {
+    BSE::TB::AuditLog->log
+       (
+        component => "shop:paypal",
+        function => $method,
+        level => "crit",
+        object => $order,
+        actor => $who,
+        msg => "PayPal $method failure",
+        dump => $result->as_string,
+       );
+    $$rmsg = $info{L_LONGMESSAGE0};
+    return;
+  }
+
+  return %info;
+}
+
+1;
index 3e931a9..5cdb085 100644 (file)
@@ -292,6 +292,13 @@ sub output_result {
   BSE::Template->output_result($req, $result);
 }
 
+# one day, this will mark the message as an error
+sub flash_error {
+  my ($self, @msg) = @_;
+
+  return $self->flash(@msg);
+}
+
 sub flash {
   my ($self, @msg) = @_;
 
@@ -309,6 +316,19 @@ sub flash {
   $self->session->{flash} = \@flash;
 }
 
+sub _str_msg {
+  my ($req, $msg) = @_;
+
+  if ($msg =~ /^(msg:[\w-]+(?:\/[\w-]+)+)(?::(.*))?$/) {
+    my $id = $1;
+    my $params = $2;
+    my @params = defined $params ? split(/:/, $params) : ();
+    $msg = $req->catmsg($id, \@params);
+  }
+
+  return $msg;
+}
+
 sub message {
   my ($req, $errors) = @_;
 
@@ -320,12 +340,7 @@ sub message {
       my @msgs = ref $errors->{$key} ? @{$errors->{$key}} : $errors->{$key};
 
       for my $msg (@msgs) {
-       if ($msg =~ /^(msg:[\w-]+(?:\/[\w-]+)+)(?::(.*))?$/) {
-         my $id = $1;
-         my $params = $2;
-         my @params = defined $params ? split(/:/, $params) : ();
-         $msg = $req->catmsg($id, \@params);
-       }
+       $msg = $req->_str_msg($msg);
       }
       $errors->{$key} = ref $errors->{$key} ? \@msgs : $msgs[0];
     }
@@ -352,13 +367,11 @@ sub message {
     push @lines, @{$req->session->{flash}};
     delete $req->session->{flash};
   }
-  $msg = join "<br />", map escape_html($_), @lines;
-  if (!$msg && $req->cgi->param('m')) {
-    $msg = join(' ', $req->cgi->param('m'));
-    $msg = escape_html($msg);
+  if (!@lines && $req->cgi->param('m')) {
+    push @lines, map $req->_str_msg($_), $req->cgi->param("m");
   }
 
-  return $msg;
+  return join "<br />", map escape_html($_), @lines;
 }
 
 sub dyn_response {
@@ -685,38 +698,7 @@ sub _encode_utf8 {
 sub user_url {
   my ($req, $script, $target, @options) = @_;
 
-  my $cfg = $req->cfg;
-  my $base = $script eq 'shop' ? $cfg->entryVar('site', 'secureurl') : '';
-  my $template;
-  if ($target) {
-    if ($script eq 'nuser') {
-      $template = "/cgi-bin/nuser.pl/user/TARGET";
-    }
-    else {
-      $template = "$base/cgi-bin/$script.pl?a_TARGET=1";
-    }
-    $template = $cfg->entry('targets', $script, $template);
-    $template =~ s/TARGET/$target/;
-  }
-  else {
-    if ($script eq 'nuser') {
-      $template = "/cgi-bin/nuser.pl/user";
-    }
-    else {
-      $template = "$base/cgi-bin/$script.pl";
-    }
-    $template = $cfg->entry('targets', $script.'_n', $template);
-  }
-  if (@options) {
-    $template .= $template =~ /\?/ ? '&' : '?';
-    my @entries;
-    while (my ($key, $value) = splice(@options, 0, 2)) {
-      push @entries, "$key=" . escape_uri($value);
-    }
-    $template .= join '&', @entries;
-  }
-
-  return $template;
+  return $req->cfg->user_url($script, $target, @options);
 }
 
 sub admin_tags {
index 6dc25f9..885cae9 100644 (file)
@@ -4,7 +4,14 @@ use vars qw(@ISA @EXPORT_OK);
 @ISA = qw/Exporter/;
 @EXPORT_OK = qw/shop_cart_tags cart_item_opts nice_options shop_nice_options
                 total shop_total load_order_fields basic_tags need_logon
-                get_siteuser payment_types order_item_opts/;
+                get_siteuser payment_types order_item_opts
+ PAYMENT_CC PAYMENT_CHEQUE PAYMENT_CALLME PAYMENT_MANUAL PAYMENT_PAYPAL/;
+
+our %EXPORT_TAGS =
+  (
+   payment => [ grep /^PAYMENT_/, @EXPORT_OK ],
+  );
+
 use Constants qw/:shop/;
 use BSE::Util::SQL qw(now_sqldate);
 use BSE::Util::Tags;
@@ -12,6 +19,12 @@ 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;
+
 =item shop_cart_tags($acts, $cart, $cart_prods, $req, $stage)
 
 Returns a list of tags which display the cart details
@@ -476,27 +489,34 @@ information.
 sub payment_types {
   my ($cfg) = @_;
 
-  my %types =
+  my @types =
     (
-     0 => { 
-          id => 0, 
-          name => 'CC', 
-          desc => 'Credit Card',
-          require => [ qw/cardNumber cardExpiry cardHolder/ ],
-         },
-     1 => { 
-          id => 1, 
-          name => 'Cheque', 
-          desc => 'Cheque',
-          require => [],
-         },
-     2 => {
-          id => 2,
-          name => 'CallMe',
-          desc => 'Call customer for payment',
-          require => [],
-         },
+     {
+      id => PAYMENT_CC, 
+      name => 'CC', 
+      desc => 'Credit Card',
+      require => [ qw/cardNumber cardExpiry cardHolder/ ],
+     },
+     {
+      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');
   
@@ -505,6 +525,7 @@ sub payment_types {
     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);
@@ -529,12 +550,24 @@ sub payment_types {
 
   # credit card payments require either encrypted emails enabled or
   # an online CC processing module
-  if ($types{0}) {
+  if ($types{+PAYMENT_CC}) {
     my $noencrypt = $cfg->entryBool('shop', 'noencrypt', 0);
     my $ccprocessor = $cfg->entry('shop', 'cardprocessor');
 
     if ($noencrypt && !$ccprocessor) {
-      $types{0}{enabled} = 0;
+      $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";
     }
   }
 
index 4f3226f..71b393d 100644 (file)
@@ -24,4 +24,48 @@ sub table {
   "bse_audit_log";
 }
 
+sub level_name {
+  my ($self) = @_;
+
+  return BSE::TB::AuditLog->level_id_to_name($self->level);
+}
+
+sub actor_name {
+  my ($self) = @_;
+
+  my $type = $self->actor_type;
+  if ($type eq "U") {
+    return "Unknown";
+  }
+  elsif ($type eq "E") {
+    return "Error";
+  }
+  elsif ($type eq "S") {
+    return "System";
+  }
+  elsif ($type eq "A") {
+    require BSE::TB::AdminUsers;
+    my $admin = BSE::TB::AdminUsers->getByPkey($self->actor_id);
+    if ($admin) {
+      return "Admin: ".$admin->logon;
+    }
+    else {
+      return "Admin: " . $self->actor_id. " (not found)";
+    }
+  }
+  elsif ($type eq "M") {
+    require SiteUsers;
+    my $user = SiteUsers->getByPkey($self->actor_id);
+    if ($user) {
+      return "Member: ".$user->userId;
+    }
+    else {
+      return "Member: ".$self->actor_id . " (not found)";
+    }
+  }
+  else {
+    return "Unknown type $type";
+  }
+}
+
 1;
index 60f2ed8..138773b 100644 (file)
@@ -4,7 +4,7 @@ use Squirrel::Table;
 use vars qw(@ISA $VERSION);
 @ISA = qw(Squirrel::Table);
 use BSE::TB::AuditEntry;
-use Scalar::Util;
+use Scalar::Util qw(blessed);
 
 sub rowClass {
   return 'BSE::TB::AuditEntry';
@@ -25,7 +25,29 @@ separated component, module and function.
 
 =item *
 
-level - level of event, one of 
+level - level of event, one of emerg, alert, crit, error, warning,
+notice, info, debug.
+
+=item *
+
+actor - the entity performing the actor, one of a SiteUser object, an
+AdminUser object, "S" for system, "U" for an unknown public user.
+
+=item *
+
+msg - a brief message
+
+=item *
+
+ip_address - the actor's IP address (optional, loaded from $REMOTE_ADDR).
+
+=item *
+
+object - an optional object being acted upon.
+
+=item *
+
+dump - an optional dump of debugging data
 
 =back
 
@@ -47,6 +69,11 @@ sub log {
   if ($component =~ /^(\w+):(\w*):(.+)$/) {
     @entry{qw/component module function/} = ( $1, $2, $3 );
   }
+  elsif ($component =~ /^(\w+):(\w*)$/) {
+    @entry{qw/component module/} = ( $1, $2 );
+    $entry{function} = delete $opts{function} || delete $opts{action}
+      or $class->crash("Missing function parameter");
+  }
   else {
     $entry{component} = $component;
     $entry{module} = delete $opts{module} || '';
@@ -76,16 +103,16 @@ sub log {
       $entry{actor_type} = "A";
     }
     else {
-      $entry{actor_type} = "A";
+      $entry{actor_type} = "M";
     }
     $entry{actor_id} = $actor->id;
   }
   else {
-    if ($actor eq "S") {
-      $entry{actor_type} = "S";
+    if ($actor =~ /^[US]$/) {
+      $entry{actor_type} = $actor;
     }
     else {
-      $entry{actor_type} = "U";
+      $entry{actor_type} = "E";
     }
     $entry{actor_id} = undef;
   }
index 3288dc2..6ec8c95 100644 (file)
@@ -25,7 +25,8 @@ sub columns {
            ccOnline ccSuccess ccReceipt ccStatus ccStatusText
            ccStatus2 ccTranId complete delivOrganization billOrganization
            delivStreet2 billStreet2 purchase_order shipping_method
-           shipping_name shipping_trace/;
+           shipping_name shipping_trace
+          paypal_token paypal_tran_id/;
 }
 
 sub defaults {
@@ -80,6 +81,8 @@ sub defaults {
     );
 }
 
+sub table { "orders" }
+
 sub address_columns {
   return qw/
            delivFirstName delivLastName delivStreet delivSuburb delivState
@@ -301,4 +304,17 @@ sub add_item {
   return BSE::TB::OrderItems->make(%item);
 }
 
+sub deliv_country_code {
+  my ($self) = @_;
+
+  my $use_codes = BSE::Cfg->single->entry("shop", "country_code", 0);
+  if ($use_codes) {
+    return $self->delivCountry;
+  }
+  else {
+    require BSE::Countries;
+    return BSE::Countries::bse_country_code($self->delivCountry);
+  }
+}
+
 1;
index 26347ef..5614f4f 100644 (file)
@@ -34,4 +34,13 @@ sub option_list {
     BSE::TB::OrderItemOptions->getBy(order_item_id => $self->{id});
 }
 
+sub product {
+  my ($self) = @_;
+
+  $self->productId == -1
+    and return;
+  require Products;
+  return Products->getByPkey($self->productId);
+}
+
 1;
index 9760080..969cadf 100644 (file)
@@ -17,7 +17,7 @@ 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(order_item_opts nice_options);
+use BSE::Shop::Util qw(:payment order_item_opts nice_options);
 
 my %actions =
   (
@@ -32,6 +32,7 @@ my %actions =
    order_unpaid => 'shop_order_unpaid',
    product_detail => '',
    product_list => '',
+   paypal_refund => 'bse_shop_order_refund_paypal',
   );
 
 sub actions {
@@ -676,10 +677,10 @@ sub _set_order_paid {
       my $order = BSE::TB::Orders->getByPkey($id)) {
     if ($order->paidFor != $value) {
       if ($value) {
-       $order->set_paymentType(3);
+       $order->set_paymentType(PAYMENT_MANUAL);
       }
       else {
-       $order->paymentType == 3
+       $order->paymentType == PAYMENT_MANUAL
          or return $class->req_order_detail($req, "You can only unpay manually paid orders");
       }
 
@@ -698,6 +699,28 @@ sub _set_order_paid {
   }
 }
 
+sub req_paypal_refund {
+  my ($self, $req) = @_;
+
+  my $id = $req->cgi->param('id');
+  if ($id and
+      my $order = BSE::TB::Orders->getByPkey($id)) {
+    require BSE::PayPal;
+    my $msg;
+    unless (BSE::PayPal->refund_order(order => $order,
+                                     req => $req,
+                                     msg => \$msg)) {
+      return $self->req_order_detail($req, $msg);
+    }
+
+    return $req->get_refresh($req->url(shopadmin => { "a_order_detail" => 1, id => $id }));
+  }
+  else {
+    $req->flash_error("Missing or invalid order id");
+    return $self->req_order_list($req);
+  }
+}
+
 #####################
 # utilities
 # perhaps some of these belong in a class...
index 70d4b22..e17cd88 100644 (file)
@@ -27,14 +27,14 @@ sub dispatch {
   }
   unless ($action) {
     for my $check (keys %$actions) {
-      if ($cgi->param("$prefix$check") || $cgi->param("$prefix$check.x")) {
+      if ($cgi->param("a_$check") || $cgi->param("a_$check.x")) {
        $action = $check;
        last;
       }
     }
     if (!$action && $prefix ne 'a_') {
       for my $check (keys %$actions) {
-       if ($cgi->param("a_$check") || $cgi->param("a_$check.x")) {
+       if ($cgi->param("$prefix$check") || $cgi->param("$prefix$check.x")) {
          $action = $check;
          last;
        }
index aef60a2..4bc3674 100644 (file)
@@ -3,7 +3,7 @@ use strict;
 use base 'BSE::UI::Dispatch';
 use BSE::Util::HTML qw(:default popup_menu);
 use BSE::Util::SQL qw(now_sqldate now_sqldatetime);
-use BSE::Shop::Util qw(need_logon shop_cart_tags payment_types nice_options 
+use BSE::Shop::Util qw(:payment need_logon shop_cart_tags payment_types nice_options 
                        cart_item_opts basic_tags order_item_opts);
 use BSE::CfgInfo qw(custom_class credit_card_class bse_default_country);
 use BSE::TB::Orders;
@@ -16,10 +16,7 @@ use DevHelp::Validate qw(dh_validate dh_validate_hash);
 use Digest::MD5 'md5_hex';
 use BSE::Shipping;
 use BSE::Countries qw(bse_country_code);
-
-use constant PAYMENT_CC => 0;
-use constant PAYMENT_CHEQUE => 1;
-use constant PAYMENT_CALLME => 2;
+use BSE::Util::Secure qw(make_secret);
 
 use constant MSG_SHOP_CART_FULL => 'Your shopping cart is full, please remove an item and try adding an item again';
 
@@ -40,6 +37,8 @@ my %actions =
    payment => 1,
    orderdone => 1,
    location => 1,
+   paypalret => 1,
+   paypalcan => 1,
   );
 
 my %field_map = 
@@ -787,6 +786,7 @@ sub req_show_payment {
      ifMultPaymentTypes => @payment_types > 1,
      checkedPayment => [ \&tag_checkedPayment, $payment, \%types_by_name ],
      ifPayments => [ \&tag_ifPayments, \@payment_types, \%types_by_name ],
+     paymentTypeId => [ \&tag_paymentTypeId, \%types_by_name ],
      error_img => [ \&tag_error_img, $cfg, $errors ],
      total => $order->{total},
      delivery_in => $order->{delivery_in},
@@ -1010,6 +1010,7 @@ sub req_payment {
     }
   }
 
+  $order->set_randomId(make_secret($cfg));
   $order->{ccOnline} = 0;
   
   my $ccprocessor = $cfg->entry('shop', 'cardprocessor');
@@ -1065,27 +1066,49 @@ sub req_payment {
       $order->{ccExpiryHash} = md5_hex($ccExpiry);
     }
   }
+  elsif ($paymentType == PAYMENT_PAYPAL) {
+    require BSE::PayPal;
+    my $msg;
+    my $url = BSE::PayPal->payment_url(order => $order,
+                                      user => $user,
+                                      msg => \$msg);
+    unless ($url) {
+      $session->{order_work} = $order->{id};
+      my %errors;
+      $errors{_} = "PayPal error: $msg" if $msg;
+      return $class->req_show_payment($req, \%errors);
+    }
+
+    # have to mark it complete so it doesn't get used by something else
+    return BSE::Template->get_refresh($url, $req->cfg);
+  }
 
   # order complete
   $order->{complete} = 1;
   $order->save;
 
+  $class->_finish_order($order, $req);
+
+  return BSE::Template->get_refresh($req->user_url(shop => 'orderdone'), $req->cfg);
+}
+
+# do final processing of an order after payment
+sub _finish_order {
+  my ($self, $req, $order) = @_;
+
+
   my $custom = custom_class($req->cfg);
   $custom->can("order_complete")
     and $custom->order_complete($req->cfg, $order);
 
   # set the order displayed by orderdone
-  $session->{order_completed} = $order->{id};
-  $session->{order_completed_at} = time;
+  $req->session->{order_completed} = $order->{id};
+  $req->session->{order_completed_at} = time;
 
-  my $noencrypt = $cfg->entryBool('shop', 'noencrypt', 0);
-  $class->_send_order($req, $order, \@dbitems, \@products, $noencrypt,
-                     \%subscribing_to);
+  $self->_send_order($req, $order);
 
   # empty the cart ready for the next order
-  delete @{$session}{qw/order_info order_info_confirmed cart order_work/};
-
-  return BSE::Template->get_refresh($req->user_url(shop => 'orderdone'), $req->cfg);
+  delete @{$req->session}{qw/order_info order_info_confirmed cart order_work/};
 }
 
 sub req_orderdone {
@@ -1255,6 +1278,16 @@ sub tag_ifPayment {
   return $payment == $type;
 }
 
+sub tag_paymentTypeId {
+  my ($types_by_name, $args) = @_;
+
+  if (exists $types_by_name->{$args}) {
+    return $types_by_name->{$args};
+  }
+
+  return '';
+}
+
 
 sub _validate_cfg {
   my ($class, $req, $rmsg) = @_;
@@ -1289,12 +1322,12 @@ sub req_recalculate {
 }
 
 sub _send_order {
-  my ($class, $req, $order, $items, $products, $noencrypt, 
-      $subscribing_to) = @_;
+  my ($class, $req, $order) = @_;
 
   my $cfg = $req->cfg;
   my $cgi = $req->cgi;
 
+  my $noencrypt = $cfg->entryBool('shop', 'noencrypt', 0);
   my $crypto_class = $cfg->entry('shop', 'crypt_module',
                                 $Constants::SHOP_CRYPTO);
   my $signing_id = $cfg->entry('shop', 'crypt_signing_id',
@@ -1317,6 +1350,16 @@ sub _send_order {
     $extras{$key} = sub { $data };
   }
 
+  my @items = $order->items;
+  my @products = map $_->product, @items;
+  my %subscribing_to;
+  for my $product (@products) {
+    my $sub = $product->subscription;
+    if ($sub) {
+      $subscribing_to{$sub->{text_id}} = $sub;
+    }
+  }
+
   my $item_index = -1;
   my @options;
   my $option_index;
@@ -1325,32 +1368,32 @@ sub _send_order {
     (
      %extras,
      custom_class($cfg)
-     ->order_mail_actions(\%acts, $order, $items, $products, 
+     ->order_mail_actions(\%acts, $order, \@items, \@products, 
                          $session->{custom}, $cfg),
      BSE::Util::Tags->static(\%acts, $cfg),
      iterate_items_reset => sub { $item_index = -1; },
      iterate_items => 
      sub { 
-       if (++$item_index < @$items) {
+       if (++$item_index < @items) {
         $option_index = -1;
         @options = order_item_opts($req,
-                                   $items->[$item_index], 
-                                   $products->[$item_index]);
+                                   $items[$item_index], 
+                                   $products[$item_index]);
         return 1;
        }
        return 0;
      },
-     item=> sub { $items->[$item_index]{$_[0]}; },
+     item=> sub { $items[$item_index]{$_[0]}; },
      product => 
      sub { 
-       my $value = $products->[$item_index]{$_[0]};
+       my $value = $products[$item_index]{$_[0]};
        defined($value) or $value = '';
        $value;
      },
      order => sub { $order->{$_[0]} },
      extended => 
      sub {
-       $items->[$item_index]{units} * $items->[$item_index]{$_[0]};
+       $items[$item_index]{units} * $items[$item_index]{$_[0]};
      },
      _format =>
      sub {
@@ -1374,7 +1417,7 @@ sub _send_order {
      ifOptions => sub { @options },
      options => sub { nice_options(@options) },
      with_wrap => \&tag_with_wrap,
-     ifSubscribingTo => [ \&tag_ifSubscribingTo, $subscribing_to ],
+     ifSubscribingTo => [ \&tag_ifSubscribingTo, \%subscribing_to ],
     );
 
   my $mailer = BSE::Mail->new(cfg=>$cfg);
@@ -1678,7 +1721,7 @@ sub _fillout_order {
   $values->{orderDate} = now_sqldatetime;
 
   # this should be hard to guess
-  $values->{randomId} ||= md5_hex(time().rand().{}.$$);
+  $values->{randomId} = md5_hex(time().rand().{}.$$);
 
   return 1;
 }
@@ -1946,4 +1989,88 @@ sub _same_options {
   return 1;
 }
 
+sub _paypal_order {
+  my ($self, $req, $rmsg) = @_;
+
+  my $id = $req->cgi->param("order");
+  unless ($id) {
+    $$rmsg = $req->catmsg("msg:bse/shop/paypal/noorderid");
+    return;
+  }
+  my ($order) = BSE::TB::Orders->getBy(randomId => $id);
+  unless ($order) {
+    $$rmsg = $req->catmsg("msg:bse/shop/paypal/unknownorderid");
+    return;
+  }
+
+  return $order;
+}
+
+=item paypalret
+
+Handles PayPal returning control.
+
+Expects:
+
+=over
+
+=item *
+
+order - the randomId of the order
+
+=item *
+
+token - paypal token we originally supplied to paypal.  Supplied by
+PayPal.
+
+=item *
+
+PayerID - the paypal user who paid the order.  Supplied by PayPal.
+
+=back
+
+=cut
+
+sub req_paypalret {
+  my ($self, $req) = @_;
+
+  require BSE::PayPal;
+  BSE::PayPal->configured
+      or return $self->req_cart($req, { _ => "msg:bse/shop/paypal/unconfigured" });
+
+  my $msg;
+  my $order = $self->_paypal_order($req, \$msg)
+    or return $self->req_show_payment($req, { _ => $msg });
+
+  $order->complete
+    and return $self->req_cart($req, { _ => "msg:bse/shop/paypal/alreadypaid" });
+
+  unless (BSE::PayPal->pay_order(req => $req,
+                                order => $order,
+                                msg => \$msg)) {
+    return $self->req_show_payment($req, { _ => $msg });
+  }
+
+  $self->_finish_order($req, $order);
+
+  return $req->get_refresh($req->user_url(shop => "orderdone"));
+}
+
+sub req_paypalcan {
+  my ($self, $req) = @_;
+
+  require BSE::PayPal;
+  BSE::PayPal->configured
+      or return $self->req_cart($req, { _ => "msg:bse/shop/paypal/unconfigured" });
+
+  my $msg;
+  my $order = $self->_paypal_order($req, \$msg)
+    or return $self->req_show_payment($req, { _ => $msg });
+
+  $req->flash("msg:bse/shop/paypal/cancelled");
+
+  my $url = $req->user_url(shop => "show_payment");
+  return $req->get_refresh($url);
+}
+
 1;
index 2c66f3e..3edce1c 100644 (file)
@@ -662,11 +662,18 @@ sub make_multidependent_iterator {
 }
 
 sub admin {
-  my ($class, $acts, $cfg) = @_;
+  my ($class, $acts, $cfg, $req) = @_;
 
+  my $oit = BSE::Util::Iterate::Objects->new(cfg => $cfg);
   return
     (
      help => [ \&tag_help, $cfg, 'admin' ],
+     $oit->make
+     (
+      single => "auditentry",
+      plural => "auditlog",
+      code => [ iter_auditlog => $class, $req ],
+     ),
     );
 }
 
@@ -690,6 +697,15 @@ sub tag_stylecfg {
   return $cfg->entry("help style $style", $name, $default);
 }
 
+sub iter_auditlog {
+  my ($class, $req, $args, $acts, $funcname, $templater) = @_;
+
+  my (@args) = DevHelp::Tags->get_parms($args, $acts, $templater);
+  require BSE::TB::AuditLog;
+  return sort { $b->id cmp $a->id }
+    BSE::TB::AuditLog->getBy(@args);
+}
+
 sub tag_help {
   my ($cfg, $defstyle, $args) = @_;
 
index 48ecc59..8ee7943 100644 (file)
@@ -76,3 +76,32 @@ description: Attempted to create or save a message to a language not in the conf
 id: bse/admin/message/badmultiline
 description: Multiple lines of text supplied when creating or saving a single line message
 
+id: bse/shop/
+description: Shop messages
+
+id: bse/shop/paypal/
+description: PayPal messages
+
+id: bse/shop/paypal/noorderid
+description: No order value was passed back from PayPal
+
+id: bse/shop/paypal/notoken
+description: No token value was passed back from PayPal
+
+id: bse/shop/paypal/nopayerid
+description: No PayerID value was passed back from PayPal
+
+id: bse/shop/paypal/unknownorderid
+description: Order id supplied by PayPal is unknown
+
+id: bse/shop/paypal/cancelled
+description: Payment through PayPal was cancelled by the user
+
+id: bse/shop/paypal/unconfigured
+description: Entry via one of the PayPal entries when PayPal isn't configured
+
+id: bse/shop/paypal/alreadypaid
+description: Returned from PayPal for an order that was already paid.  The payment through PayPal won't be completed.
+
+id: bse/shop/paypal/badtoken
+description: The transaction token supplied by PayPal to paypalret doesn't match the token value stored in the order
index 31786e7..628b081 100644 (file)
@@ -33,6 +33,30 @@ message: Unknown language code - no entry found in [languages]
 id: bse/admin/message/badmultiline
 message: Message $1:s may contain only a single line of text
 
+id: bse/shop/paypal/notoken
+message: No token supplied by PayPal
+
+id: bse/shop/paypal/nopayerid
+message: No PayerID supplied by PayPal
+
+id: bse/shop/paypal/noorderid
+message: No order ID supplied by PayPal
+
+id: bse/shop/paypal/unknownorderid
+message: Unknown order ID supplied by PayPal
+
+id: bse/shop/paypal/unconfigured
+message: PayPal payments cannot be processed, PayPal support is not configured
+
+id: bse/shop/paypal/alreadypaid
+message: Order has already been paid
+
+id: bse/shop/paypal/cancelled
+message: PayPal payment cancelled
+
+id: bse/shop/paypal/badtoken
+message: The token returned by PayPal doesn't match the token generated for your order.
+
 id: test/test/multiline
 message: <<TEXT
 This message has
index d2ae48f..6862a8e 100644 (file)
@@ -777,6 +777,10 @@ The possible payment types are:
 
 2 - contact customer for details
 
+=item *
+
+3 - paypal - see L<paypal.pod>
+
 =back
 
 Other types can be added by adding entries to the [payment type names]
@@ -863,6 +867,11 @@ Maximum number of entries in the cart.  This limits the number of
 distinct products (with options) in the cart, not the total
 quantities.  Default: Unlimited.
 
+=item currency
+
+The shop currency as a 3-letter currency code.  Default: AUD.
+Currencies other than "AUD" aren't supported by most of the system.
+
 =back
 
 =head2 [shipping]
index 4befd09..ccc5bc6 100644 (file)
@@ -364,3 +364,71 @@ img.bse_image_thumb {
   background-color: #888;
   }
 
+#menu_wrapper .column {
+  float: left;
+  width: 20em;
+  margin: 5px;
+  background-color: #FFF;
+}
+
+#menu_wrapper .title {
+  font-weight: bold;
+  text-align: left;
+  padding: 5px;
+  font-size: 120%;
+}
+
+#menu_wrapper .column li {
+  list-style: none;
+  margin-top: 5px;
+  /*margin-bottom: 5px;*/
+}
+
+#menu_wrapper .column>ul ul {
+  padding-left: 1em;
+  padding-right: 0px;
+  margin-left: 0px;
+  margin-right: 0px;
+}
+
+#menu_wrapper .column>ul {
+  padding-left: 0px;
+  margin-left: 10px;
+  margin-right: 10px;
+}
+
+/*#menu_wrapper .column>ul>li {
+  margin-top: 10px;
+}*/
+
+#menu_wrapper .column li a:link,
+#menu_wrapper .column li a:visited {
+  display: block;
+  border: 1px dotted #8080FF;
+  padding: 5px;
+  text-decoration: none;
+  background-color: #CCCCFF;
+  color: #000;
+}
+
+#menu_wrapper .column li a:hover {
+  background-color: #8080FF;
+}
+
+#menu_wrapper li>ul.submenu { 
+  display: none;
+}
+
+#menu_wrapper li:hover>ul.submenu { 
+  display: block;
+  position: absolute;
+  background: #FFF;
+  width: 17em;
+  padding-right: 1em;
+  border: 1px solid #8080ff;
+}
+
+#auditlog .col_when_at,
+#auditlog .col_who { 
+  white-space: nowrap;
+}
diff --git a/site/templates/admin/include/auditentry.tmpl b/site/templates/admin/include/auditentry.tmpl
new file mode 100644 (file)
index 0000000..22abd2b
--- /dev/null
@@ -0,0 +1,8 @@
+<tr class="audit<:auditentry level:>">
+  <td class="col_when_at"><:date "%H:%M %d/%m/%Y" auditentry when_at:></td>
+  <td class="col_level"><:auditentry level_name:></td>
+  <td class="col_actor"><:auditentry actor_name:>
+</td>
+   <td class="col_what"><:auditentry component:>/<:auditentry module:>/<:auditentry function:></td>
+   <td class="col_msg"><:auditentry msg:></td>
+</tr>
\ No newline at end of file
diff --git a/site/templates/admin/include/audithead.tmpl b/site/templates/admin/include/audithead.tmpl
new file mode 100644 (file)
index 0000000..fbca715
--- /dev/null
@@ -0,0 +1,7 @@
+<tr>
+  <th>When</th>
+  <th>Level</th>
+  <th>Who</th>
+  <th>What</th>
+  <th>Message</th>
+</tr>
index a65ec4e..a18066c 100644 (file)
@@ -10,6 +10,7 @@
 <:ifSiteuser id:><a href="/cgi-bin/admin/siteusers.pl?a_edit=1&amp;id=<:siteuser id:>">Edit Member</a> |
 <a href="/cgi-bin/admin/siteusers.pl?a_edit=1&amp;id=<:siteuser id:>&amp;_t=orders">Other member orders</a> |<:or:><:eif:>
 </p>
+<:ifMessage:><div class="message"><:message:></div><:or:><:eif:>
 <h2>Order details - No: #<:order id:></h2>
 <:ifOrder complete:><:or:><p><b><font size="+1">This order is incomplete and should not be filled.</font></b></p><:eif:>
 <table cellpadding="6" cellspacing="1" border="0">
@@ -110,9 +111,6 @@ Shipping via <:order shipping_method:>:
           <td align=right nowrap><:money order gst:></td>
 </tr>
 </table>
-</td>
-</tr>
-</table>
 <:switch:>
 <:case Eq [order paidFor] "0":>
 <p>This order hasn't been paid</p>
@@ -141,6 +139,7 @@ Shipping via <:order shipping_method:>:
 <: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="<:script:>?a_order_unpaid=1&amp;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="<:script:>?a_paypal_refund=1&amp;id=<:order id:>">Refund</a><:or:><:eif:></p>
 <:endswitch:>
 <:include custom/order_detail_payment.include optional:>
 <:if Order filled:>
@@ -152,4 +151,13 @@ Shipping via <:order shipping_method:>:
 <:if Order instructions:>
 <p style="white-space: pre-wrap;"><:order instructions:></p>
 <:or Order:><:eif Order:>
+
+<table class="editform" id="auditlog">
+<:include admin/include/audithead.tmpl:>
+<:iterator begin auditlog object_id [order id] object_type BSE::TB::Order:>
+<:include admin/include/auditentry.tmpl:>
+<:iterator end auditlog:>
+</table>
+
+</table>
 </body></html>
index fade5b7..d31ce7b 100644 (file)
@@ -38,6 +38,9 @@
 <:if Payment CallMe:>
 <p>We will call you to arrange payment.</p>
 <:or Payment:><:eif Payment:>
+<:if Payment PayPal:>
+<p>Paid via PayPal, transaction ID <:order paypal_tran_id:></p>
+<:or Payment:><:eif Payment:>
 <:include custom/checkout_final_payments.include optional:>
 </font> 
 <table width="100%" border="0" cellspacing="0" cellpadding="0">
index 1059ec9..27bc062 100644 (file)
   <hr size="1" noshade>
 <:ifMsg:><p><b><font face="Verdana, Arial, Helvetica, sans-serif" size="2"><:msg:></font></b></p><:or:><:eif:>
   <:if Payments CC :>
-  <:if MultPaymentTypes:><p><font face="Verdana, Arial, Helvetica, sans-serif" size="2"><input type="radio" name="paymentType" value="0" <:checkedPayment CC:>> Credit Card</font></p><:or MultPaymentTypes:><input type=hidden name=paymentType value=0 <:checkedPayment CC:>><:eif MultPaymentTypes:>
+  <:if MultPaymentTypes:><p><font face="Verdana, Arial, Helvetica, sans-serif" size="2"><input type="radio" name="paymentType" value="<:paymentTypeId CC:>" <:checkedPayment CC:>> Credit Card</font></p><:or MultPaymentTypes:><input type=hidden name=paymentType value="<:paymentTypeId CC:>" <:checkedPayment CC:>><:eif MultPaymentTypes:>
   <table border="0" cellspacing="0" cellpadding="0">
     <tr> 
       <td> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> Name on 
   </table>
   <:or Payments:><:eif Payments:>
   <:if Payments Cheque:>
-  <:if MultPaymentTypes:><p><font face="Verdana, Arial, Helvetica, sans-serif" size="2"> <input type=radio name=paymentType value=1 <:checkedPayment Cheque:>/>
-    Cheque</font></p><:or MultPaymentTypes:><input type=hidden name=paymentType value=1><:eif MultPaymentTypes:>
+  <:if MultPaymentTypes:><p><font face="Verdana, Arial, Helvetica, sans-serif" size="2"> <input type="radio" name="paymentType" value="<:paymentTypeId Cheque:>" <:checkedPayment Cheque:>/>
+    Cheque</font></p><:or MultPaymentTypes:><input type="hidden" name="paymentType" value="<:paymentTypeId Cheque:>"><:eif MultPaymentTypes:>
   <p> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> Please send your cheque to:</font></p>
   <ul> <font face="Verdana, Arial, Helvetica, sans-serif" size="2"> <:cfg shop address1:><br />
      <:cfg shop address2:><br />
      <:cfg shop address3:></font></ul>
   <:or Payments:><:eif Payments:>
   <:if Payments CallMe:>
-   <:if MultPaymentTypes:><p><font face="Verdana, Arial, Helvetica, sans-serif" size="2"><input type=radio name=paymentType value=2 <:checkedPayment CallMe:>/> Contact me for billing details</font></p>
+   <:if MultPaymentTypes:><p><font face="Verdana, Arial, Helvetica, sans-serif" size="2"><input type="radio" name="paymentType" value="<:paymentTypeId CallMe:>" <:checkedPayment CallMe:>/> Contact me for billing details</font></p>
    <:or MultPaymentTypes:>
-     <input type=hidden name=paymentType value=2>
+     <input type="hidden" name="paymentType" value="<:paymentTypeId CallMe:>">
      <p><font face="Verdana, Arial, Helvetica, sans-serif" size="2">We will call you to arrange for payment.</font></p>
    <:eif MultPaymentTypes:>
   <:or Payments:>
 
   <:eif Payments:>
+  <:if Payments PayPal:>
+<:if MultPaymentTypes:><p><font face="Verdana, Arial, Helvetica, sans-serif" size="2"> <input type=radio name=paymentType value="<:paymentTypeId PayPal:>" <:checkedPayment PayPal:>/>
+<img src="https://www.paypal.com/en_AU/i/logo/PayPal_mark_37x23.gif" align="absmiddle" style="margin-right:7px;"><span style="font-size:11px; font-family: Arial, Verdana;">The safer, easier way to pay.</span></font></p><:or MultPaymentTypes:><input type=hidden name=paymentType value=1><:eif MultPaymentTypes:>
+
+  <:or Payments:><:eif Payments:>
   <:include custom/payment_type.include optional:>
   <p>&nbsp; </p>
   <font face="Verdana, Arial, Helvetica, sans-serif" size="3"> <b>Tax Invoice 
index dddffbd..ff346f4 100644 (file)
@@ -30,7 +30,8 @@ Receipt No.  : <:order ccReceipt:>
 :><:eif Order:><:or:><:eif
 :><:ifEq [order paymentType] "1" :>Will be paid by cheque<:or
 :><:eif:><:ifEq [order paymentType] "2"
-:>We will call you to arrange for payment<:or:><:eif:><:
+:>We will call you to arrange for payment<:or:><:eif:><:ifEq [order paymentType] "4"
+:>Paid by PayPal, transaction id <:order paypal_tran_id:><:or:><:eif:><:
 include custom/payment_type_email.include:>
 To be shipped by: <:order shipping_method:>
 <:if Order instructions:>
index 9aba4a1..f5d1a52 100644 (file)
@@ -33,7 +33,8 @@ Name on Card : <:order ccName:>
 Card Type    : <:order ccType :><:eif Order:><:or:><:eif
 :><:ifEq [order paymentType] "1" :>Will be paid by cheque<:or
 :><:eif:><:ifEq [order paymentType] "2"
-:>Please call the customer to arrange for payment<:or:><:eif:><:
+:>Please call the customer to arrange for payment<:or:><:eif:><:ifEq [order paymentType] "4"
+:>Paid by PayPal, transaction id <:order paypal_tran_id:><:or:><:eif:><:
 include custom/payment_type_email.include:>
 To be shipped by: <:order shipping_method:>
 <:if Order instructions:>
index 618c250..afa5b66 100644 (file)
@@ -458,6 +458,8 @@ Column purchase_order;varchar(80);NO;;
 Column shipping_method;varchar(64);NO;;
 Column shipping_name;varchar(40);NO;;
 Column shipping_trace;text;YES;NULL;
+Column paypal_token;varchar(255);YES;NULL;
+Column paypal_tran_id;varchar(255);YES;NULL;
 Index PRIMARY;1;[id]
 Index order_cchash;0;[ccNumberHash]
 Index order_userId;0;[userId;orderDate]