[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
mark.
Files modified:
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]](https://www.dreamwidth.org/img/silk/identity/user_staff.png)
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 --------------------------------------------------------------------------------