mark: A photo of Mark kneeling on top of the Taal Volcano in the Philippines. It was a long hike. (Default)
Mark Smith ([staff profile] mark) wrote in [site community profile] changelog2009-04-22 07:29 am

[dw-free] Payment system checkpoint commit for Janine to work on. The PayPal end-to-end is function

[commit: http://hg.dwscoalition.org/dw-free/rev/446f75a17f85]

Payment system checkpoint commit for Janine to work on. The PayPal end-to-
end is functional: we setup, confirm, and charge the order. Carts are
marked as PAID or PEND_PAID as appropriate, lots of logging, and general
progress.

Patch by [staff profile] mark.

Files modified:
  • bin/upgrading/en.dat
  • bin/upgrading/update-db-general.pl
  • cgi-bin/DW/Shop/Cart.pm
  • cgi-bin/DW/Shop/Engine.pm
  • cgi-bin/DW/Shop/Engine/PayPal.pm
  • cgi-bin/LJ/Widget/ShopCart.pm
  • htdocs/shop/checkout.bml
  • htdocs/shop/pp_confirm.bml
--------------------------------------------------------------------------------
diff -r a74da125f02d -r 446f75a17f85 bin/upgrading/en.dat
--- a/bin/upgrading/en.dat	Wed Apr 22 05:11:42 2009 +0000
+++ b/bin/upgrading/en.dat	Wed Apr 22 07:29:20 2009 +0000
@@ -1347,6 +1347,24 @@ error.noschwartz=TheSchwartz not install
 error.noschwartz=TheSchwartz not installed or not configured properly.
 
 error.notloggedin=You have to <a [[aopts]]>log in</a> in order to use this page.
+
+error.pay.dberr=Database error: [[errstr]].  Please try again later.
+
+error.pay.nodb=Failed to connect to database.  Please try again later.
+
+error.pay.paypal.connection=Temporary failure connecting to PayPal.  Please try again later.
+
+error.pay.paypal.engbadstate=Payment engine is in a bad state.  Please contact site administrators.
+
+error.pay.paypal.flownotfinished=PayPal order flow not finished.  Please try again later.
+
+error.pay.paypal.generic=PayPal error.  Please try again later.
+
+error.pay.paypal.noschwartz=Unable to contact job queue system.  Please try again later.
+
+error.pay.paypal.notoken=PayPal did not give us a token.  Please try again later.
+
+error.pay.paypal.schwartzinsert=Failed inserting job into queue system.  Please try again later.
 
 error.person=You must be authenticated as a person.
 
diff -r a74da125f02d -r 446f75a17f85 bin/upgrading/update-db-general.pl
--- a/bin/upgrading/update-db-general.pl	Wed Apr 22 05:11:42 2009 +0000
+++ b/bin/upgrading/update-db-general.pl	Wed Apr 22 07:29:20 2009 +0000
@@ -995,6 +995,10 @@ register_tabledrop("style");
 register_tabledrop("style");
 register_tabledrop("meme");
 register_tabledrop("content_flag");
+register_tabledrop("dw_payments");
+register_tabledrop("dw_pp_details");
+register_tabledrop("dw_pp_log");
+register_tabledrop("dw_pp_notify_log");
 
 register_tablecreate("portal", <<'EOC');
 CREATE TABLE portal (
@@ -2988,40 +2992,6 @@ CREATE TABLE embedcontent_preview (
 ) TYPE=InnoDB
 EOC
 
-register_tablecreate("dw_payments", <<'EOC');
-CREATE TABLE dw_payments (
-    paymentid int unsigned NOT NULL auto_increment,
-    paydate int unsigned NOT NULL,
-    pp_token varchar(20) NOT NULL,
-    from_userid int unsigned,
-    target_userid int unsigned,
-    target_username varchar(25),
-    typeid smallint unsigned NOT NULL,
-    duration smallint unsigned NOT NULL,
-    amount smallint unsigned NOT NULL,
-    status varchar(20) NOT NULL,
-
-    PRIMARY KEY (paymentid),
-    INDEX (paydate, pp_token),
-    INDEX (from_userid),
-    INDEX (target_userid),
-    INDEX (status)
-)
-EOC
-
-register_tablecreate("dw_pp_details", <<'EOC');
-CREATE TABLE dw_pp_details (
-    paymentid int unsigned NOT NULL,
-
-    email varchar(255),
-    firstname varchar(255),
-    lastname varchar(255),
-    payerid varchar(255),
-
-    PRIMARY KEY (paymentid)
-)
-EOC
-
 register_tablecreate("dw_paidstatus", <<'EOC');
 CREATE TABLE dw_paidstatus (
     userid int unsigned NOT NULL,
@@ -3032,25 +3002,6 @@ CREATE TABLE dw_paidstatus (
 
     PRIMARY KEY (userid),
     INDEX (expiretime)
-)
-EOC
-
-register_tablecreate("dw_pp_log", <<'EOC');
-CREATE TABLE dw_pp_log (
-    paymentid int unsigned NOT NULL,
-    transdate int unsigned NOT NULL,
-    pp_log varchar(3000) NOT NULL,
-
-    INDEX (paymentid)
-)
-EOC
-
-register_tablecreate("dw_pp_notify_log", <<'EOC');
-CREATE TABLE dw_pp_notify_log (
-    transdate int unsigned NOT NULL,
-    pp_log varchar(3000) NOT NULL,
-
-    INDEX (transdate)
 )
 EOC
 
@@ -3224,6 +3175,64 @@ CREATE table externalaccount (
 )
 EOC
 
+register_tablecreate('pp_tokens', <<'EOC');
+CREATE TABLE pp_tokens (
+    ppid int unsigned not null auto_increment,
+    inittime int unsigned not null,
+    touchtime int unsigned not null,
+    cartid int unsigned not null,
+    status varchar(20) not null,
+
+    token varchar(20) not null,
+    email varchar(255),
+    firstname varchar(255),
+    lastname varchar(255),
+    payerid varchar(255),
+
+    primary key (ppid),
+    unique (cartid),
+    index (token)
+);
+EOC
+
+register_tablecreate('pp_log', <<'EOC');
+CREATE TABLE pp_log (
+    ppid int unsigned not null,
+    transtime int unsigned not null,
+    req_content text not null,
+    res_content text not null,
+
+    index (ppid)
+);
+EOC
+
+register_tablecreate('pp_trans', <<'EOC');
+CREATE TABLE pp_trans (
+    ppid int unsigned not null,
+    cartid int unsigned not null,
+
+    transactionid varchar(19),
+    transactiontype varchar(15),
+    paymenttype varchar(7),
+    ordertime int unsigned,
+    amt decimal(10,2),
+    currencycode varchar(3),
+    feeamt decimal(10,2),
+    settleamt decimal(10,2),
+    taxamt decimal(10,2),
+    paymentstatus varchar(20),
+    pendingreason varchar(20),
+    reasoncode varchar(20),
+    ack varchar(20),
+    timestamp int unsigned,
+    build varchar(20),
+
+    index (ppid),
+    index (cartid)
+);
+EOC
+
+
 # NOTE: new table declarations go ABOVE here ;)
 
 
@@ -3945,11 +3954,6 @@ register_alter(sub {
                   "ALTER TABLE expunged_users MODIFY COLUMN user VARCHAR(25) NOT NULL" );
     }
 
-    unless ( column_type( 'dw_payments', 'target_username' ) =~ /25/ ) {
-        do_alter( 'dw_payments',
-                  "ALTER TABLE dw_payments MODIFY COLUMN target_username VARCHAR(25)" );
-    }
-
     unless ( column_type( "acctcode", "timegenerate" ) =~ /^\Qint(10) unsigned\E/ ) {
         do_alter( "acctcode",
                   "ALTER TABLE acctcode MODIFY COLUMN timegenerate INT UNSIGNED");
diff -r a74da125f02d -r 446f75a17f85 cgi-bin/DW/Shop/Cart.pm
--- a/cgi-bin/DW/Shop/Cart.pm	Wed Apr 22 05:11:42 2009 +0000
+++ b/cgi-bin/DW/Shop/Cart.pm	Wed Apr 22 07:29:20 2009 +0000
@@ -228,6 +228,19 @@ sub remove_item {
 }
 
 
+# get/set state
+sub state {
+    my ( $self, $newstate ) = @_;
+
+    return $self->{state}
+        unless defined $newstate;
+
+    $self->{state} = $newstate;
+    $self->save;
+
+    return $self->{state};
+}
+
 ################################################################################
 ## read-only accessor methods
 ################################################################################
@@ -237,7 +250,6 @@ sub userid { $_[0]->{userid}            
 sub userid { $_[0]->{userid}             }
 sub age    { time() - $_[0]->{starttime} }
 sub items  { $_[0]->{items} ||= []       }
-sub state  { $_[0]->{state}              }
 sub uniq   { $_[0]->{uniq}               }
 sub total  { $_[0]->{total}+0.00         }
 
diff -r a74da125f02d -r 446f75a17f85 cgi-bin/DW/Shop/Engine.pm
--- a/cgi-bin/DW/Shop/Engine.pm	Wed Apr 22 05:11:42 2009 +0000
+++ b/cgi-bin/DW/Shop/Engine.pm	Wed Apr 22 07:29:20 2009 +0000
@@ -79,4 +79,13 @@ sub err_is_temporary {
 }
 
 
+# fail_transaction()
+#
+# this is a 'something bad has happened, consider this cart and transaction
+# to be dead' sort of thing
+sub fail_transaction {
+    die "Please implement $_[0]->fail_transaction.\n";
+}
+
+
 1;
diff -r a74da125f02d -r 446f75a17f85 cgi-bin/DW/Shop/Engine/PayPal.pm
--- a/cgi-bin/DW/Shop/Engine/PayPal.pm	Wed Apr 22 05:11:42 2009 +0000
+++ b/cgi-bin/DW/Shop/Engine/PayPal.pm	Wed Apr 22 07:29:20 2009 +0000
@@ -41,8 +41,8 @@ sub new_from_token {
     my $dbh = DW::Pay::get_db_writer()
         or die "Database temporarily unavailable.\n"; # no object yet
 
-    my ( $ppid, $itime, $ttime, $cartid, $status ) =
-        $dbh->selectrow_array( 'SELECT ppid, inittime, touchtime, cartid, status FROM pp_tokens WHERE token = ?', undef, $token );
+    my ( $ppid, $itime, $ttime, $cartid ) =
+        $dbh->selectrow_array( 'SELECT ppid, inittime, touchtime, cartid FROM pp_tokens WHERE token = ?', undef, $token );
     return undef
         unless $cartid;
 
@@ -55,7 +55,34 @@ sub new_from_token {
         inittime => $itime,
         touchtime => $ttime,
         token => $token,
-        status => $status,
+        cart => $cart,
+    }, $class;
+}
+
+
+# new_from_cart( $cart )
+#
+# constructs an engine from a given cart.
+sub new_from_cart {
+    my ( $class, $cart ) = @_;
+
+    my $dbh = DW::Pay::get_db_writer()
+        or die "Database temporarily unavailable.\n"; # no object yet
+
+    my ( $ppid, $itime, $ttime, $cartid, $token ) =
+        $dbh->selectrow_array( 'SELECT ppid, inittime, touchtime, cartid, token FROM pp_tokens WHERE cartid = ?', undef, $cart->id );
+
+    # if they have no row in the database, then this is a new cart that hasn't
+    # yet really been through the PayPal flow?
+    return bless { cart => $cart }, $class
+        unless $cartid;
+
+    # it HAS, we have a row, so populate with all of the data we have
+    return bless {
+        ppid => $ppid,
+        inittime => $itime,
+        touchtime => $ttime,
+        token => $token,
         cart => $cart,
     }, $class;
 }
@@ -119,8 +146,8 @@ sub checkout_url {
 
     # now store this in the db
     $dbh->do(
-        q{INSERT INTO pp_tokens (ppid, inittime, touchtime, cartid, token, status)
-          VALUES (NULL, UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), ?, ?, 'init')},
+        q{INSERT INTO pp_tokens (ppid, inittime, touchtime, cartid, token)
+          VALUES (NULL, UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), ?, ?)},
         undef, $cart->id, $res->{token}
     );
     return $self->error( 'dberr', errstr => $dbh->errstr )
@@ -134,17 +161,19 @@ sub checkout_url {
 # confirm_order()
 #
 # does the final capture process to tell PayPal that we want to charge the
-# user for their payment.
+# user for their payment.  returns 1 on "money is ours yay" and 2 for
+# "money is pending".
 sub confirm_order {
     my $self = $_[0];
 
-    # ensure status is 'init'
+    # ensure the cart is in checkout state.  if it's still open or paid
+    # or something, we can't touch it.
     return $self->error( 'paypal.engbadstate' )
-        unless $self->status eq 'init';
+        unless $self->cart->state == $DW::Shop::STATE_CHECKOUT;
 
-    # now ensure we can get jobs sent off
-    my $sh = LJ::theschwartz()
-        or return $self->temp_error( 'paypal.noschwartz' );
+    # ensure we have db
+    my $dbh = DW::Pay::get_db_writer()
+        or return $self->temp_error( 'nodb' );
 
     # now we have to call out to PayPal to get some details on this order
     # and make sure that the user has finished the process and isn't just
@@ -162,19 +191,56 @@ sub confirm_order {
     $self->lastname( $res->{lastname} );
     $self->email( $res->{email} );
 
-    # okay, schedule the job to actually do the right thing
-    my $job = TheSchwartz::Job->new(
-        funcname => 'DW::Worker::Payment',
-        arg => { ppid => $self->ppid }
+    # and now try to capture the payment
+    my $res = $self->_pp_req(
+        'DoExpressCheckoutPayment',
+        token         => $self->token,
+        payerid       => $self->payerid,
+        amt           => $self->cart->display_total,
+        paymentaction => 'Sale',
     );
-    my $h = $sh->insert( $job );
-    return $self->temp_error( 'paypal.schwartzinsert' )
-        unless $h;
+    return $self->temp_error( 'paypal.generic' )
+        unless $res && $res->{transactionid};
 
-    # and mark this paypal item as processing
-    $self->status( 'queued' );
+    # okay, so we got something from them.  have to record this in the
+    # transaction table.  siiiimple, sure.
+    $dbh->do(
+        q{INSERT INTO pp_trans (ppid, cartid, transactionid, transactiontype, paymenttype, ordertime,
+            amt, currencycode, feeamt, settleamt, taxamt, paymentstatus, pendingreason, reasoncode,
+            ack, timestamp, build)
+          VALUES (?, ?, ?, ?, ?, UNIX_TIMESTAMP(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, UNIX_TIMESTAMP(?), ?)},
 
-    return 1;
+        undef, $self->ppid, $self->cart->id,
+        map { $res->{$_} } qw/ transactionid transactiontype paymenttype ordertime
+            amt currencycode feeamt settleamt taxamt paymentstatus pendingreason reasoncode
+            ack timestamp build /
+    );
+
+    # if there's a db error above, that's very disturbing and alarming
+    # FIXME: add $eng->send_alarm or something so that we can have the Management
+    # take a stab at fixing manually in exotic cases?
+    warn "Failure to save pp_trans: " . $dbh->errstr . "\n"
+        if $dbh->err;
+
+    # if this order is Complete (i.e., we have the money) then we note that
+    if ( $res->{paymentstatus} eq 'Completed' ) {
+        $self->cart->state( $DW::Shop::STATE_PAID );
+        return 1;
+    }
+
+    # okay, so it's pending... sad days
+    $self->cart->state( $DW::Shop::STATE_PEND_PAID );
+    return 2;
+}
+
+
+# called when something terrible has happened and we need to fully fail out
+# a transaction for some reason.  (payment not valid, etc.)
+sub fail_transaction {
+    my $self = $_[0];
+
+    # step 1) mark statuses
+#    $self->cart->
 }
 
 
@@ -249,7 +315,6 @@ sub touchtime { $_[0]->{touchtime} }
 
 
 # mutable accessors
-sub status { _getset( $_[0], 'status', $_[1] ) }
 sub payerid { _getset( $_[0], 'payerid', $_[1] ) }
 sub firstname { _getset( $_[0], 'firstname', $_[1] ) }
 sub lastname { _getset( $_[0], 'lastname', $_[1] ) }
diff -r a74da125f02d -r 446f75a17f85 cgi-bin/LJ/Widget/ShopCart.pm
--- a/cgi-bin/LJ/Widget/ShopCart.pm	Wed Apr 22 05:11:42 2009 +0000
+++ b/cgi-bin/LJ/Widget/ShopCart.pm	Wed Apr 22 07:29:20 2009 +0000
@@ -20,6 +20,8 @@ use base qw/ LJ::Widget /;
 use base qw/ LJ::Widget /;
 use Carp qw/ croak /;
 
+use DW::Shop;
+
 sub need_res { qw( stc/shop.css ) }
 
 sub render_body {
@@ -32,6 +34,11 @@ sub render_body {
 
     return $class->ml( 'widget.shopcart.error.noitems' )
         unless @{$cart->items};
+
+    # if the cart is not in state OPEN, mark this as a receipt load
+    # no matter where we are
+    $opts{receipt} = 1
+        unless $cart->state == $DW::Shop::STATE_OPEN;
 
     $ret .= $class->start_form
         unless $opts{receipt};
diff -r a74da125f02d -r 446f75a17f85 htdocs/shop/checkout.bml
--- a/htdocs/shop/checkout.bml	Wed Apr 22 05:11:42 2009 +0000
+++ b/htdocs/shop/checkout.bml	Wed Apr 22 07:29:20 2009 +0000
@@ -45,6 +45,9 @@ body<=
     my $url = $eng->checkout_url;
     return $eng->errstr
         unless $url;
+
+    # before redirecting, make sure we mark the cart as done
+    $cart->state( $DW::Shop::STATE_CHECKOUT );
     return BML::redirect( $url );
 }
 _code?>
diff -r a74da125f02d -r 446f75a17f85 htdocs/shop/pp_confirm.bml
--- a/htdocs/shop/pp_confirm.bml	Wed Apr 22 05:11:42 2009 +0000
+++ b/htdocs/shop/pp_confirm.bml	Wed Apr 22 07:29:20 2009 +0000
@@ -32,9 +32,9 @@ body<=
     return 'sorry, invalid token'
         unless $eng;
 
-    # if status is not 'init', send to the receipt page
+    # cart must be in checkout state
     return BML::redirect( "$LJ::SITEROOT/shop/pp_receipt?token=" . $eng->token )
-        unless $eng->status eq 'init';
+        unless $eng->cart->state == $DW::Shop::STATE_CHECKOUT;
 
     # if they didn't post, give them a form
     unless ( LJ::did_post() ) {
@@ -58,14 +58,25 @@ body<=
     return 'invalid form auth'
         unless LJ::check_form_auth();
 
+    # and now set the state, this has been checked out...
+    $eng->cart->state( $DW::Shop::STATE_CHECKOUT );
+
     # they want to pay us, yippee!
+    my $rv = $eng->confirm_order;
     return $eng->errstr
-        unless $eng->confirm_order;
+        unless $rv;
 
     # advise them the order has been placed and to watch their email
-    return '<?p Your order has been successfully placed.  Within the next few minutes PayPal should ' .
-           'finish processing and we will activate your order.  Watch your email! p?><?p ' .
-           "<a href='$LJ::SITEROOT/shop/pp_receipt?token=$token'>View Receipt</a> p?>";
+    if ( $rv == 1 ) {
+        return '<?p Your order has been successfully placed.  Within the next few minutes PayPal should ' .
+               'finish processing and we will activate your order.  Watch your email! p?><?p ' .
+               "<a href='$LJ::SITEROOT/shop/pp_receipt?token=$token'>View Receipt</a> p?>";
+
+    } elsif ( $rv == 2 ) {
+        return '<?p Your order has been successfully placed, but PayPal says it will take some time for ' .
+               'your payment to finish processing.  As soon as it does, we will email you. p?><?p ' .
+               "<a href='$LJ::SITEROOT/shop/pp_receipt?token=$token'>View Receipt</a> p?>";
+    }
 }
 _code?>
 <=body
--------------------------------------------------------------------------------