Андрей (andy) wrote in changelog,

[ljcom] r8455: LJSUP-5801 (LJ Wallet // provide interna...

Committer: ailyin
LJSUP-5801 (LJ Wallet // provide internal documentation for the LJ Wallet changes)
U   trunk/bin/upgrading/en_LJ.dat
U   trunk/cgi-bin/LJ/Pay/Method.pm
U   trunk/cgi-bin/LJ/Pay/Wallet/Error.pm
U   trunk/cgi-bin/LJ/Pay/Wallet/Log.pm
U   trunk/cgi-bin/LJ/Pay/Wallet/Stats.pm
U   trunk/cgi-bin/LJ/Pay/Wallet.pm
U   trunk/htdocs/admin/accounts/acctedit.bml
U   trunk/htdocs/pay/modify.bml
Modified: trunk/bin/upgrading/en_LJ.dat
===================================================================
--- trunk/bin/upgrading/en_LJ.dat	2010-04-07 10:13:09 UTC (rev 8454)
+++ trunk/bin/upgrading/en_LJ.dat	2010-04-07 10:18:58 UTC (rev 8455)
@@ -8466,6 +8466,10 @@
 
 wallet.error_no_sum=Invalid characters: please use only numbers in the amount field.
 
+wallet.error_cannot_save_payitem=Couldn't update the cart item to indicate it's been delivered (#[[piid]]).
+
+wallet.error_cart_already_paid=Cart [[cart]] has already been paid for.
+
 wallet.tokens.item.name=LJ Tokens
 
 wallet.tokens.item.type=[[qty]] [[?qty|token|tokens|tokens]] ($[[amt]] USD)
@@ -8604,4 +8608,4 @@
 
 wallet.bml.admin.stat.th.tokens=tokens
 
-wallet.suggest_buy_tokens=Your [[feature_name]] balance is insufficient. <a href="[[landing]]">Buy LJ tokens</a> to use this method.
\ No newline at end of file
+wallet.suggest_buy_tokens=Your [[feature_name]] balance is insufficient. <a href="[[landing]]">Buy LJ tokens</a> to use this method.

Modified: trunk/cgi-bin/LJ/Pay/Method.pm
===================================================================
--- trunk/cgi-bin/LJ/Pay/Method.pm	2010-04-07 10:13:09 UTC (rev 8454)
+++ trunk/cgi-bin/LJ/Pay/Method.pm	2010-04-07 10:18:58 UTC (rev 8455)
@@ -1,8 +1,61 @@
 package LJ::Pay::Method;
 
+=head1 NAME
+
+LJ::Pay::Method - the Shop payment method class
+
+=head1 SYNOPSIS
+
+ my $cart = LJ::Pay::Payment->load(37);
+ my $methods = LJ::Pay::Method->suitable_methods($cart);
+
+ foreach my $group (keys %$methods) {
+     foreach my $method (@{$methods->{$group}}) {
+         print "$method\n";
+     }
+ }
+
+ my $method_class = LJ::Pay::Method->class_by_code($POST{'pay_method'});
+ die "method not suitable"
+     unless $method_class->suitable_for($cart);
+
+ $method_class->checkout_process($cart);
+ my ($title, $body);
+ if ($method_class->checkout_should_redirect($cart)) {
+     return $method_class->checkout_redirect($cart);
+ } else {
+     $method_class->checkout_render($cart, \$title, \$body);
+ }
+ print "<title>$title</title><body>$body</body>";
+
+=cut
+
 use strict;
 use Carp qw();
 
+=head1 CONSTANTS
+
+ METHODS_LIST => {
+     'group1' => [
+        'method1', # corresponds to the LJ::Pay::Method::method1 class
+        'method2',
+     ],
+     'group2' => [
+        'method3',
+        'method4',
+     ],
+ };
+
+ GROUPS_ORDER => [ qw(group1 group2) ];
+
+These two constants are supposed to be used in the LJ::Widget::Payment widget;
+when it figures how to display the "checkout method" drop-down, it should take
+METHODS_LIST, filter it, leaving only suitable methods (in fact, the
+suitable_methods subroutine does the filtering itself), and then output
+several <optgroup>s in order of GROUPS_ORDER as the presentation logic demands.
+
+=cut
+
 use constant METHODS_LIST => {
     'fast' => [
         'Wallet',
@@ -31,17 +84,49 @@
     $CLASS_MAP{$method->code} = $method;
 }
 
-# parent class methods
+=head1 METHODS
+
+=head2 Base class methods (final)
+
+=head3 all_methods
+
+Returns an arrayref containing all subclasses.
+
+This is a final method.
+
+=cut
+
 sub all_methods {
     return \@METHODS;
 }
 
+=head3 class_by_code
+
+Given a payment method code (that is, what is stored in the DB and what is
+passed as a form value), returns the corresponding class.
+
+This is a final method.
+
+=cut
+
 sub class_by_code {
     my ($class, $code) = @_;
 
     return $CLASS_MAP{$code};
 }
 
+=head3 suitable_methods
+
+Given an LJ::Pay::Payment object, returns all payment methods suitable for
+the given cart.
+
+The return value format is similar to that of the METHODS_LIST contant; it is
+guaranteed, however, that there are no empty method groups.
+
+This is a final method.
+
+=cut
+
 sub suitable_methods {
     my ($class, $cart) = @_;
 
@@ -64,7 +149,54 @@
     return \%ret;
 }
 
-# virtual methods
+=head2 Virtual methods
+
+=head3 code
+
+Returns a method "code", i.e. a string value used to store indication of this
+method in the database and to use in web forms.
+
+=cut
+
+sub code { 'unknown' }
+
+=head3 name
+
+Returns a method name to be used in the "select method" dropdown.
+
+=cut
+
+sub name {
+    my ($class) = @_;
+
+    return LJ::Lang::ml('pay.cart.paymentmethod.' . $class->code);
+}
+
+=head3 name_generic
+
+Returns a method name to be used in the "transactions history" dropdown. This
+is generally the same as name(), with Wallet being a notable exception.
+
+=cut
+
+sub name_generic {
+    my ($class) = @_;
+
+    return $class->name;
+}
+
+=head3 suitable_for
+
+Given an LJ::Pay::Payment object, returns a boolen value indicating whether
+the given payment method can be used to pay for that cart.
+
+This is a virtual method; it is overriden in the Free subclass only. This is
+because suitable_for from the base class calls a two virtual functions,
+and it is easier to override those. Please refer to the function code
+for further information.
+
+=cut
+
 sub suitable_for {
     my ($class, $cart) = @_;
 
@@ -86,9 +218,46 @@
     return $ret;
 }
 
+=head3 suitable_for helpers
+
+=over 2
+
+=item can_pay_for_coppa
+
+Returns a boolean value indicating whether the given method can be used to pay
+for a "Coppa" cart item.
+
+=item suitable_for_additional_checks
+
+Given a cart, returns a boolean value indicating whether this method can be
+used to pay for that cart assuming that the usual suitable_for conditions are
+met.
+
+=back
+
+=cut
+
 sub can_pay_for_coppa { 0 }
+
 sub suitable_for_additional_checks { 1 }
 
+=head2 allow_open_proxy
+
+Returns a boolean value indicating whether this method can be used by clients
+that are behind an open proxy. The default is "no", with Yandex Money being
+a notable exception.
+
+=cut
+
+sub allow_open_proxy { 0 }
+
+=head3 error_message_unsuitable
+
+Given a cart, returns a string value containing an error message informing
+user that the given method is unsuitable for that cart.
+
+=cut
+
 sub error_message_unsuitable {
     my ($class, $cart) = @_;
 
@@ -101,31 +270,46 @@
         ": " . $class->code;
 }
 
-sub code { 'unknown' }
+=head2 Purely virtual methods
 
-sub name {
-    my ($class) = @_;
+=head3 checkout_process
 
-    return LJ::Lang::ml('pay.cart.paymentmethod.' . $class->code);
-}
+Given a cart, does whatever actions the system needs to do when using this
+method before rendering a page or redirecting the user to another page.
 
-# used by LJ::Widget::WalletHistory
-sub name_generic {
-    my ($class) = @_;
+=cut
 
-    return $class->name;
-}
-
 sub checkout_process { }
 
+=head3 checkout_should_redirect
+
+Given a cart, returns a boolean value indicating whether the user should be
+redirected to checkout that cart using this method.
+
+=cut
+
 sub checkout_should_redirect { 1 }
 
+=head3 checkout_redirect
+
+Given a cart, redirects the client to the page where they can proceed with
+the checkout.
+
+=cut
+
 sub checkout_redirect {
     my ($class, $cart) = @_;
 
     BML::redirect('/');
 }
 
+=head3 checkout_redirect
+
+Given a cart and two scalarrefs (title and body), renders a page to inform
+user how to proceed with the checkout.
+
+=cut
+
 sub checkout_render {
     my ($class, $cart, $title, $body) = @_;
 
@@ -133,6 +317,4 @@
     $$body = 'checkout body';
 }
 
-sub allow_open_proxy { 0 }
-
 1;

Modified: trunk/cgi-bin/LJ/Pay/Wallet/Error.pm
===================================================================
--- trunk/cgi-bin/LJ/Pay/Wallet/Error.pm	2010-04-07 10:13:09 UTC (rev 8454)
+++ trunk/cgi-bin/LJ/Pay/Wallet/Error.pm	2010-04-07 10:18:58 UTC (rev 8455)
@@ -1,5 +1,33 @@
 package LJ::Pay::Wallet::Error;
 
+=head1 NAME
+
+LJ::Pay::Wallet::Error - the LJ Wallet exceptions module
+
+This module handles LJ Wallet errors: raising them, and converting them to
+strings to show to the user.
+
+=head1 SYNOPSIS
+
+ use LJ::Pay::Wallet::Error;
+
+ eval {
+     LJ::Pay::Wallet::Error->raise(ERROR_CODE, {
+         'param1' => $value1,
+         'param2' => $value2,
+
+         # gets ljuser_display'ed when converted to string
+         'param_user' => LJ::load_userid(3449),
+     });
+ };
+
+ if ($@) {
+     print $@->as_string;
+     print $@; # syntactic sugar for the above, using overload
+ }
+
+=cut
+
 use strict;
 use Exporter;
 use Carp qw();
@@ -24,6 +52,8 @@
         ERROR_NO_SUM                    => 12,
         ERROR_RCPT_INELIGIBLE           => 13,
         ERROR_REMOTE_INELIGIBLE         => 14,
+        ERROR_CANNOT_SAVE_PAYITEM       => 15,
+        ERROR_CART_ALREADY_PAID         => 16,
     };
 
     our @EXPORT = ();
@@ -33,6 +63,160 @@
     );
 }
 
+=head1 ERROR CODES
+
+=over 2
+
+=item ERROR_INVALID_PAYITEM
+
+The payitems table row loaded has been found to not be WalletTokens, so
+using an LJ::Pay::Wallet method to deliver it is incorrect.
+
+Parameters: piid.
+
+Thrown by LJ::Pay::Wallet::deliver_money.
+
+=item ERROR_CANNOT_GET_RCPT
+
+The system failed to load the recipient of the specified PayItem.
+
+Parameters: uid (the user ID of the user the system attempted to load).
+
+Thrown by LJ::Pay::Wallet::deliver_money.
+
+=item ERROR_CANNOT_SAVE_PAYITEM
+
+The system failed to update the payitems table row to indicate that the item
+containing WalletTokens has been delivered successfully. In light of this, the
+transaction has been rolled back so as to prevent the item from being delivered
+twice.
+
+Parameteres: piid.
+
+Thrown by LJ::Pay::Wallet::deliver_money.
+
+=item ERROR_CANNOT_GET_CART
+
+The system failed to load the specified cart to try to pay for it from the
+owner's Wallet.
+
+Parameters: cartid (payid of the cart the system attempted to load).
+
+LJ::Pay::Wallet::pay_for_cart.
+
+=item ERROR_INVALID_PAY_METHOD
+
+The payments row loaded has been found to not have "wallet" listed as payment
+method, so using an LJ::Pay::Wallet method to pay for the corresponding cart
+is incorrect.
+
+Parameters: feature_name, cart (the payid-anum code of the cart).
+
+Thrown by LJ::Pay::Wallet::pay_for_cart.
+
+=item ERROR_CANNOT_LOAD_CART_OWNER
+
+The system failed to load the owner of the specified cart to check that they
+are eligible to use Wallet to pay for the cart.
+
+Parameters: uid.
+
+Thrown by LJ::Pay::Wallet::pay_for_cart.
+
+=item ERROR_CANNOT_SAVE_CART
+
+The system failed to update the cart to indicate that it has been paid for, so
+it rolled back the changes so as to not write off money from user's balance
+without giving them anything in return.
+
+Parameters: cart.
+
+Thrown by LJ::Pay::Wallet::pay_for_cart.
+
+=item ERROR_DB
+
+A database error occured.
+
+Parameters: str (the error string as returned by the DB driver).
+
+=item ERROR_INELIGIBLE
+
+The system found out that the user who tries to use the wallet is not eligible
+to use it.
+
+Parameters: user (LJ::User object), feature_name.
+
+Thrown by LJ::Pay::Wallet::pay_for_cart.
+
+=item ERROR_CART_ALREADY_PAID
+
+The system found out that the specified cart has already been paid for. In
+light of this, the transaction has been rolled back to prevent writing off
+tokens from the cart owner's balance twice for paying for the same cart.
+
+Parameters: cart.
+
+Thrown by LJ::Pay::Wallet::pay_for_cart.
+
+=item ERROR_INSUFFICIENT_FUNDS
+
+The system found out that it is not possible to write off the specified amount
+of tokens from the user's Wallet, because the user does not have that many.
+
+Parameters: need, have (how much tokens are needed, how much the user has).
+
+Thrown by LJ::Pay::Wallet::pay_for_cart, LJ::Pay::Wallet::try_add,
+/shop/wallet.bml logic.
+
+=item ERROR_UNKNOWN_RCPT
+
+The system failed to find out the "tokens transfer" recipient with the
+specified username.
+
+Parameters: username.
+
+Thrown by the /shop/wallet.bml logic.
+
+=item ERROR_INVALID_SUM
+
+The system found out that the sum of a "tokens transfer" transaction has been
+specified incorrectly.
+
+Parameters: sum.
+
+Thrown by the /shop/wallet.bml logic.
+
+=item ERROR_NO_SUM
+
+The system found out that the sum of a "tokens transfer" transaction has not
+been specified.
+
+No parameters.
+
+Thrown by the /shop/wallet.bml logic.
+
+=item ERROR_RCPT_INELIGIBLE
+
+The system found out that the recipient of a "tokens transfer" transaction
+is not eligible to use Wallet.
+
+Parameters: user (the recipient, and LJ::User object).
+
+Thrown by the /shop/wallet.bml logic.
+
+=item ERROR_REMOTE_INELIGIBLE
+
+The system found out that the user who interfaces with the site is not eligible
+to use Wallet.
+
+No parameters.
+
+Thrown by the /shop/wallet.bml logic.
+
+=back
+
+=cut
+
 use constant $errors;
 
 my $error_strings = { map { $errors->{$_} => lc($_) } keys %$errors };
@@ -40,6 +224,16 @@
 use overload
     '""' => \&as_string;
 
+=head1 METHODS
+
+=head2 new
+
+The constructor.
+
+ die LJ::Pay::Wallet::Error->new(ERROR_CODE, { 'param' => $value });
+
+=cut
+
 sub new {
     my ($class, $code, $details) = @_;
 
@@ -51,28 +245,33 @@
     };
 }
 
+=head2 raise
+
+Create a new object and die, throwing the created object as an exception.
+
+ LJ::Pay::Wallet::Error->raise(ERROR_CODE, { 'param' => $value });
+
+=cut
+
 sub raise {
     my ($class, $code, $details) = @_;
     die $class->new($code, $details);
 }
 
+=head2 as_string
+
+Convert an LJ::Pay::Wallet::Error object to a string.
+
+ eval {
+     # a code that throws an LJ::Pay::Wallet::Error object
+ };
+
+ if ($@) {
+     print $@->as_string;
+     print "$@"; # syntactic sugar
+ }
+
 =cut
-my %error_format = (
-    ERROR_INVALID_PAYITEM()         => 'payitem %piid% is not WalletTokens',
-    ERROR_CANNOT_GET_RCPT()         => 'cannot get rcpt: uid=%uid%',
-    ERROR_CANNOT_GET_CART()         => 'cannot load cart: cartid=%cartid%',
-    ERROR_INVALID_PAY_METHOD()      => 'payment method for cart=%cart% is not "wallet"',
-    ERROR_CANNOT_LOAD_CART_OWNER()  => 'cannot load cart owner: uid=%uid%',
-    ERROR_INSUFFICIENT_FUNDS()      => 'insufficient funds: need=%need%, have=%have%',
-    ERROR_CANNOT_SAVE_CART()        => 'cannot mark cart as paid: %cart%',
-    ERROR_DB()                      => 'database error: %str%',
-    ERROR_INELIGIBLE()              => 'user %user% is not eligible to use LJ Wallet',
-    ERROR_RCPT_INELIGIBLE()         => 'user %user% is not eligible to use LJ Wallet',
-    ERROR_UNKNOWN_RCPT()            => 'cannot load recipient: username=%username%',
-    ERROR_INVALID_SUM()             => 'invalid sum: %sum%',
-    ERROR_NO_SUM()                  => 'sum not specified',
-);
-=cut
 
 sub as_string {
     my ($self) = @_;
@@ -93,3 +292,9 @@
 }
 
 1;
+
+=head1 SEE ALSO
+
+L<LJ::Pay::Wallet|LJ::Pay::Wallet>
+
+=cut

Modified: trunk/cgi-bin/LJ/Pay/Wallet/Log.pm
===================================================================
--- trunk/cgi-bin/LJ/Pay/Wallet/Log.pm	2010-04-07 10:13:09 UTC (rev 8454)
+++ trunk/cgi-bin/LJ/Pay/Wallet/Log.pm	2010-04-07 10:18:58 UTC (rev 8455)
@@ -1,5 +1,101 @@
 package LJ::Pay::Wallet::Log;
 
+=head1 NAME
+
+LJ::Pay::Wallet::Log - logging the LJ Wallet transactions, as well as
+requesting information from the logs.
+
+=head1 SYNOPSIS
+
+ use LJ::Pay::Wallet::Log qw(:actions :statuses :bits);
+
+ my $logitem = LJ::Pay::Wallet::Log->log(
+     'userid' => $u->id,
+     'action' => ACTION_ADD,
+     'qty' => $pi->get_qty,
+     'payid' => $pi->get_payid,
+     'piid' => $piid,
+ );
+
+ $logitem->update('status' => STATUS_FINISHED, 'time_end' => time);
+
+ # all parameters are optional
+ my $logitems = LJ::Pay::Wallet::Log->query(
+     'require_bits_set'   => [ BIT_SUP ],
+     'require_bits_unset' => [ BIT_PAID ],
+     'period' => [
+         time - 86400, # minimum time_end
+         time,         # maximum time_end
+     ],
+     'userid' => 3449,
+ );
+
+ foreach my $logitem (@$logitems) {
+     my $payitem = $logitem->item; # can be undef
+     my $cart = $logitem->cart;
+     my $u = $logitem->user;
+
+     print
+        $logitem->id,
+        $logitem->userid,
+        $logitem->action, # ACTION_ADD or ACTION_REMOVE
+        $logitem->qty,
+        $logitem->payid,
+        $logitem->piid,
+        $logitem->time_start,
+        $logitem->time_end,
+        $logitem->status; # STATUS_STARTED, STATUS_FINISHED or STATUS_ERROR
+ }
+
+=head1 DATABASE SCHEMA
+
+ CREATE TABLE user_wallet_history (
+     logid INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
+     userid INT NOT NULL DEFAULT 0,
+     action CHAR(1) NOT NULL DEFAULT "",
+     qty DECIMAL(10,3) DEFAULT 0.0,
+     payid INT NOT NULL DEFAULT 0,
+     piid INT NOT NULL DEFAULT 0,
+     time_start INT NOT NULL DEFAULT 0,
+     time_end INT NOT NULL DEFAULT 0,
+     status CHAR(1) NOT NULL DEFAULT "S",
+     misc BIGINT UNSIGNED NOT NULL DEFAULT 0,
+
+     INDEX(userid, timestamp),
+     INDEX(timestamp),
+     INDEX(payid)
+ )
+
+This table may not be transaction-safe; indices here are important, however.
+
+=head1 SEE ALSO
+
+=over 2
+
+\item *
+
+C<LJ::Pay::Wallet>
+
+\item *
+
+C<LJ::Pay::Wallet::Stats>
+
+\item *
+
+C</admin/accounts/wallet-history.bml>
+
+\item *
+
+C</shop/wallet.bml>
+
+\item *
+
+C<LJ::Widget::WalletHistory>
+
+=back
+
+=cut
+
 use strict;
 use Exporter;
 use Carp qw();
@@ -19,7 +115,7 @@
         STATUS_FINISHED => 'F',
         STATUS_ERROR    => 'E',
     };
-    
+
     $bits = {
         BIT_SUP         => 0,
         BIT_PAID        => 1,

Modified: trunk/cgi-bin/LJ/Pay/Wallet/Stats.pm
===================================================================
--- trunk/cgi-bin/LJ/Pay/Wallet/Stats.pm	2010-04-07 10:13:09 UTC (rev 8454)
+++ trunk/cgi-bin/LJ/Pay/Wallet/Stats.pm	2010-04-07 10:18:58 UTC (rev 8455)
@@ -1,5 +1,76 @@
 package LJ::Pay::Wallet::Stats;
 
+=head1 NAME
+
+LJ::Pay::Wallet::Stats - requesting various statistics about LJ Wallet usage.
+
+=head1 SYNOPSIS
+
+ use LJ::Pay::Wallet::Log qw(:bits);
+ use LJ::Pay::Wallet::Stats;
+
+ # all parameters are optional
+ my %params = (
+     'require_bits_set'   => [ BIT_SUP ],
+     'require_bits_unset' => [ BIT_PAID ],
+     'period' => [
+         time - 86400, # minimum time_end
+         time,         # maximum time_end
+     ],
+ );
+
+ my $general_stat = LJ::Pay::Wallet::Stats->general_stat(%params);
+
+ print $general_stat->{'in'}, $general_stat->{'out'};
+ foreach my $k (qw(vgift xfer paidacct addon_upi addon_fb other)) {
+     print $k, " => ", $general_stat->{'out_breakdown'}->{$k}
+ }
+
+ # period is ignored by this one
+ my $top_wallets = LJ::Pay::Wallet::Stats->top_wallets(%params);
+ foreach my $row (@$top_wallets) {
+     my ($userid, $balance) = @$row;
+     print "$userid => $balance\n";
+ }
+
+ my $top_activity = LJ::Pay::Wallet::Stats->top_activity(%params);
+ foreach my $row (@$top_activity) {
+     my ($userid, $transactions) = @$row;
+     print "$userid => $transactions\n";
+ }
+
+ my $top_tokens_in = LJ::Pay::Wallet::Stats->top_tokens_in(%params);
+ foreach my $row (@$top_tokens_in) {
+     my ($userid, $tokens) = @$row;
+     print "$userid => $tokens\n";
+ }
+
+ my $top_tokens_out = LJ::Pay::Wallet::Stats->top_tokens_out(%params);
+ foreach my $row (@$top_tokens_out) {
+     my ($userid, $tokens) = @$row;
+     print "$userid => $tokens\n";
+ }
+
+=head1 SEE ALSO
+
+=over 2
+
+\item *
+
+C<LJ::Pay::Wallet>
+
+\item *
+
+C<LJ::Pay::Wallet::Log>
+
+\item *
+
+C</admin/accounts/wallet-stat.bml>
+
+=back
+
+=cut
+
 use strict;
 use LJ::Pay::Wallet::Log qw(:actions :bits :statuses);
 

Modified: trunk/cgi-bin/LJ/Pay/Wallet.pm
===================================================================
--- trunk/cgi-bin/LJ/Pay/Wallet.pm	2010-04-07 10:13:09 UTC (rev 8454)
+++ trunk/cgi-bin/LJ/Pay/Wallet.pm	2010-04-07 10:18:58 UTC (rev 8455)
@@ -1,5 +1,80 @@
 package LJ::Pay::Wallet;
 
+=head1 NAME
+
+LJ::Pay::Wallet - the main LJ Wallet module
+
+This module handles LJ Wallet transactions, balance,
+eligibility, and the other general stuff. The other Shop modules that are
+related to the wallet should call this one for the logic.
+
+=head1 INTRODUCTION
+
+The LJ Wallet feature provides an internal LiveJournal currency. To list the
+terms, each user in the system has their own "balance", a number indicating
+how much money ("tokens") they have. The user is able to add "tokens" to
+the balance, paying for them with real world money using whatever payment
+methods the Shop provides. The user is able to spend the "tokens" from the
+balance, using them as a payment method to pay for Shop carts.
+
+=head1 SYNOPSIS
+
+ use LJ::Pay::Wallet;
+
+ my $remote = LJ::get_remote();
+
+ # ensure that $remote is eligible to use the feature
+ die "Not eligible"
+     unless LJ::Pay::Wallet->is_user_eligibile($remote);
+
+ # get to know how much tokens $remote has on the balance
+ print LJ::Pay::Wallet->get_user_balance($remote);
+
+ # get an LJ::Pay::Payment::PayItem of type "WalletTokens" and
+ # "deliver" it, adding tokens to the user's balance
+ my $piid = 3;
+ LJ::Pay::Wallet->deliver_money($piid);
+
+ # get an LJ::Pay::Payment cart having "wallet" set as the payment
+ # method and pay for it from the cart owner's wallet
+ my $payid = 5;
+ LJ::Pay::Wallet->pay_for_cart($payid);
+
+ # add or subtract money from the user's wallet, without logging stuff
+ # to the user's history for the user to see
+ LJ::Pay::Wallet->try_add($remote, 3000); # add
+ LJ::Pay::Wallet->try_add($remote, -5000); # subtract
+
+ # get a link like "LJ Wallet (xxx tokens)" to show
+ # in the site navigation
+ my $link = LJ::Pay::Wallet->get_wallet_link($remote);
+
+=head1 CONVENTIONS
+
+All methods of this module are "class" methods, in the sense that they are
+supposed to be called like LJ::Pay::Wallet->example_method.
+
+When modifying data, this module uses LJ::Transaction-powered transactions,
+to ensure that the database stays consistent.
+
+In case of an error, this module raises an LJ::Pay::Wallet::Error exception.
+The transaction in proccess is rolled back, effecting in no changes made.
+Please refer to L<LJ::Pay::Wallet::Error> for the error types.
+
+=head1 DATABASE SCHEMA
+
+ CREATE TABLE `user_wallet` (
+   `userid` int(11) NOT NULL default '0',
+   `balance` decimal(10,3) default '0.000',
+   PRIMARY KEY  (`userid`),
+   KEY `balance` (`balance`)
+ )
+
+The table must use a transactional-safe MySQL engine, like InnoDB, to ensure
+atomicity of transactions.
+
+=cut
+
 use strict;
 
 use LJ::Transaction;
@@ -7,15 +82,46 @@
 use LJ::Pay::Wallet::Stats;
 use LJ::Pay::Wallet::Error qw(:codes);
 
+=head1 CONSTANTS
+
+ # the "relative to the siteroot" URL of the landing page
+ LJ::Pay::Wallet::LANDING_URL => '/wallet/';
+
+ # the absolute URL of the landing page
+ LJ::Pay::Wallet::LANDING_URL_FULL => $LJ::SITEROOT . LANDING_URL;
+
+ # how much tokens USD $1 equals to
+ # (note that tokens may be sold at a discounted price, as specified in
+ # LJ::Pay::Payment::PayItem::WalletTokens)
+ LJ::Pay::Wallet::EXCHANGE_RATE => 100;
+
+=cut
+
 use constant LANDING_URL => "/wallet/";
 use constant LANDING_URL_FULL => $LJ::SITEROOT . LANDING_URL;
 
 use constant EXCHANGE_RATE => 100;
 
+=head1 METHODS
+
+=head2 Utility functions
+
+=head3 get_user_balance
+
+Called with an LJ::User argument, returns the balance for the given user.
+
+ my $u = LJ::load_userid(3449);
+ my $balance = LJ::Pay::Wallet->get_user_balance($u);
+
+Can only raise an ERROR_DB exception; theoretically, it can only occur in case
+of the DB being down.
+
+=cut
+
 sub get_user_balance {
     my ($class, $u) = @_;
 
-    my $dbh = LJ::get_db_writer(); $dbh->{'HandleError'} = \&handle_db_error;
+    my $dbh = LJ::get_db_writer(); $dbh->{'HandleError'} = \&_handle_db_error;
     my ($balance) = $dbh->selectrow_array(
         'SELECT balance FROM user_wallet WHERE userid=?', undef, $u->id
     );
@@ -24,11 +130,120 @@
     return $balance;
 }
 
+=head3 is_user_eligible
+
+Called with an LJ::User argument, returns a boolean value indicating whether
+this user is eligible to use the Wallet feature.
+
+ my $u = LJ::load_userid(3449);
+ my $eligible = LJ::Pay::Wallet->is_user_eligible($u);
+ return $eligible ? 'eligible' : 'not eligible';
+
+This function does as much as calling underlying LJ::User functions, so it
+should not raise any exceptions.
+
+=cut
+
+sub is_user_eligible {
+    my ($class, $u) = @_;
+
+    return 0 if $u->is_deleted;
+    return 0 if $u->is_expunged;
+    return 0 if $u->is_suspended;
+    return 0 if $u->is_underage;
+
+    return 1;
+}
+
+=head3 get_wallet_link
+
+Called with an LJ::User argument, returns a string value containing HTML code
+for a navigation link displayed to that user. If the user is ineligible to use
+the Wallet functionality, an empty string is returned.
+
+ my $remote = LJ::get_remote();
+ my $link = LJ::Pay::Wallet->get_wallet_link($remote);
+
+Can only raise an ERROR_DB exception; theoretically, it can only occur in case
+of the DB being down.
+
+B<Used> in layouts (horizon/lanzelot).
+
+=cut
+
+sub get_wallet_link {
+    my ($class, $u) = @_;
+
+    return '' unless $class->is_user_eligible($u);
+
+    my $balance = int $class->get_user_balance($u);
+
+    my $link = "$LJ::SITEROOT/wallet/";
+    my $mlcode = $balance > 0 ? 'wallet.nav_link' : 'wallet.nav_link.empty';
+    my $text = LJ::Lang::ml($mlcode, {
+        'feature_name' => LJ::Lang::ml('wallet.feature_name'),
+        'balance' => $balance,
+    });
+
+    return qq{<a href="$link">$text</a>};
+}
+
+=head2 Transferring money around
+
+=head3 deliver_money
+
+Called with a "pay item ID" scalar argument, converts the pay item to the
+tokens in the recipient's wallet. The item is marked "delivered fine"
+afterwards.
+
+This operation is atomic; it either completes fully or throws an error without
+actually making any changes to the data.
+
+The transaction is logged to the user's history using the LJ::Pay::Wallet::Log
+API.
+
+ my $piid = 42;
+ LJ::Pay::Wallet->deliver_money($piid);
+
+Can raise the following exceptions:
+
+=over 2
+
+=item *
+
+ERROR_INVALID_PAYITEM: the function tried to load the payitems table row only
+to find out that the item is in fact not "wallet tokens", so this subroutine
+is not the correct way to deliver the item.
+
+=item *
+
+ERROR_CANNOT_GET_RCPT: the function failed to load the recipient of the item;
+it cannot elaborate about the reason, because the error is thrown when
+LJ::want_user returns undef, and there's no API to find out why.
+
+=item *
+
+ERROR_CANNOT_SAVE_PAYITEM: the function tried to update the payitems row to
+indicate that it have delivered the item, but failed for some reason; therefore,
+the system can't be sure the item doesn't get delivered again, that's why the
+need to rollback.
+
+=item *
+
+ERROR_DB, which is a generic DB error.
+
+=back
+
+B<Used> in LJ::Pay::Payment::PayItem::WalletTokens, in the "_deliver_item"
+method.
+
+=cut
+
 sub deliver_money {
     my ($class, $piid) = @_;
 
     my $pi = LJ::Pay::Payment::PayItem->load($piid);
-    
+
     LJ::Pay::Wallet::Error->raise(ERROR_INVALID_PAYITEM, { 'piid' => $piid })
         unless $pi->get_item eq 'tokens';
 
@@ -48,7 +263,7 @@
         'piid' => $piid,
     );
 
-    my $tx = LJ::Transaction->new($dbh, \&handle_db_error);
+    my $tx = LJ::Transaction->new($dbh, \&_handle_db_error);
 
     eval {
         # ensure that the row is present in the table;
@@ -70,9 +285,13 @@
             WHERE userid=?
         }, undef, $pi->get_qty, $u->id);
 
-        $dbh->do(
+        my $affected = int $dbh->do(
             'UPDATE payitems SET status="done" WHERE piid=?', undef, $piid
         );
+
+        LJ::Pay::Wallet::Error->raise(ERROR_CANNOT_SAVE_PAYITEM, {
+            'piid' => $piid,
+        }) unless $affected;
     };
 
     if ($@) {
@@ -85,6 +304,80 @@
     $logitem->update('status' => STATUS_FINISHED, 'time_end' => time);
 }
 
+=head3 pay_for_cart
+
+Called with a "payment ID" scalar argument, calculates how much it'd cost to pay
+for that cart, writes off the corresponding amount of tokens from the cart
+owner's balance and marks the cart as "user paid for it succesfully", allowing
+for the cart items to be delivered later.
+
+This operation is atomic; it either completes fully or throws an error without
+actually making any changes to the data.
+
+The transaction is logged to the user's history using the LJ::Pay::Wallet::Log
+API.
+
+ my $payid = 37;
+ LJ::Pay::Wallet->pay_for_cart($payid);
+
+Can raise the following exceptions:
+
+=over 2
+
+=item *
+
+ERROR_CANNOT_GET_CART: the function tried to load the cart to pay for but
+failed. It cannot elaborate on the details, for the LJ::Pay::Payment->load
+method is being used for loading the cart and the error is raised when that
+method returns undef. There is no underlying API to find out why the cart
+didn't load.
+
+=item *
+
+ERROR_INVALID_PAY_METHOD: the function loaded the cart only to find out that
+its assigned method is not exactly Wallet, so calling this function is not the
+correct way to have this cart paid for.
+
+=item *
+
+ERROR_CART_ALREADY_PAID: the function loaded the cart only to find out that
+it has already been paid for; one cannot pay for the same cart twice, so this
+restriction is enforced.
+
+=item *
+
+ERROR_CANNOT_LOAD_CART_OWNER: the function failed to load the cart owner;
+it cannot elaborate about the reason, because the error is thrown when
+LJ::want_user returns undef, and there's no API to find out why.
+
+=item *
+
+ERROR_INELIGIBLE: the function loaded the cart owner only to find out that
+they are not eligibile to use Wallet, so they cannot pay for that cart
+from their wallet.
+
+=item *
+
+ERROR_INSUFFICIENT_FUNDS: the function tried to subtract the necessary amount
+of tokens from the cart owner's balance but found out that the cart owner
+does not have enough.
+
+=item *
+
+ERROR_CANNOT_SAVE_CART: the function tried to save the cart to indicate that
+it has been paid for; at that point, it needed to rollback so as to not write
+off their money without providing them what they wanted to pay for.
+
+=item *
+
+ERROR_DB: a generic DB error.
+
+=back
+
+B<Used> in the (SSL) /pay/wallet.bml page logic.
+
+=cut
+
 sub pay_for_cart {
     my ($class, $payid) = @_;
 
@@ -93,10 +386,14 @@
     LJ::Pay::Wallet::Error->raise(ERROR_CANNOT_GET_CART, { 'cartid' => $payid })
         unless $p;
 
-    LJ::Pay::Wallet::Error->raise(ERROR_CANNOT_GET_CART, {
+    LJ::Pay::Wallet::Error->raise(ERROR_INVALID_PAY_METHOD, {
         'cart' => $p->get_cart_as_string
     }) unless $p->get_method eq 'wallet';
 
+    LJ::Pay::Wallet::Error->raise(ERROR_CART_ALREADY_PAID, {
+        'cart' => $p->get_cart_as_string
+    }) unless $p->get_mailed eq 'C';
+
     my $uid = $p->get_userid;
     my $u = LJ::want_user($uid);
 
@@ -119,7 +416,7 @@
         'payid' => $payid,
     );
 
-    my $tx = LJ::Transaction->new($dbh, \&handle_db_error);
+    my $tx = LJ::Transaction->new($dbh, \&_handle_db_error);
 
     eval {
         my $qty = $amt * EXCHANGE_RATE;
@@ -127,7 +424,7 @@
         my $affected = int $dbh->do(q{
             UPDATE user_wallet
             SET balance = balance - ?
-            WHERE userid=? AND balance > ?
+            WHERE userid=? AND balance >= ?
         }, undef, $qty, $u->id, $qty);
 
         LJ::Pay::Wallet::Error->raise(ERROR_INSUFFICIENT_FUNDS, {
@@ -156,43 +453,47 @@
     $logitem->update('status' => STATUS_FINISHED, 'time_end' => time);
 }
 
-sub is_user_eligible {
-    my ($class, $u) = @_;
+=head3 try_add
 
-    return 0 if $u->is_deleted;
-    return 0 if $u->is_expunged;
-    return 0 if $u->is_suspended;
-    return 0 if $u->is_underage;
+Called with an LJ::User argument and a numeric value indicating amount of
+tokens (which can be negative), tries to modify the user's balance by
+adding or subtracting the specified amount from it.
 
-    return 1;
-}
+This operation is atomic; it either completes fully or throws an error without
+actually making any changes to the data.
 
-sub get_wallet_link {
-    my ($class, $u) = @_;
+ my $u = LJ::load_userid(3449);
+ LJ::Pay::Wallet->try_add($u, 3000); # add
+ LJ::Pay::Wallet->try_add($u, -5000); # subtract
 
-    return '' unless $class->is_user_eligible($u);
+Can raise the following exceptions:
 
-    my $balance = int $class->get_user_balance($u);
+=over 2
 
-    my $link = "$LJ::SITEROOT/wallet/";
-    my $mlcode = $balance > 0 ? 'wallet.nav_link' : 'wallet.nav_link.empty';
-    my $text = LJ::Lang::ml($mlcode, {
-        'feature_name' => LJ::Lang::ml('wallet.feature_name'),
-        'balance' => $balance,
-    });
+=item *
 
-    return qq{<a href="$link">$text</a>};
-}
+ERROR_INSUFFICIENT_FUNDS: the function tried to subtract the necessary amount
+of tokens from the user's balance but found out that they don't have enough.
 
+=item *
+
+ERROR_DB: a generic DB error.
+
+=back
+
+B<Used> in the /admin/accounts/acctedit.bml page logic.
+
+=cut
+
 sub try_add {
     my ($class, $u, $qty) = @_;
 
     my $dbh = LJ::get_db_writer();
 
-    my $tx = LJ::Transaction->new($dbh, \&handle_db_error);
+    my $tx = LJ::Transaction->new($dbh, \&_handle_db_error);
 
     eval {
-        # prevent race, see comment in deliver_money
+        # prevent race, see the comment in deliver_money
         $dbh->do(
             'INSERT IGNORE INTO user_wallet SET userid=?, balance=0', undef,
             $u->id
@@ -201,7 +502,7 @@
         my $affected = int $dbh->do(qq{
             UPDATE user_wallet
             SET balance=balance+?
-            WHERE userid=? AND balance+? > 0
+            WHERE userid=? AND balance+? >= 0
         }, undef, $qty, $u->id, $qty);
 
         LJ::Pay::Wallet::Error->raise(ERROR_INSUFFICIENT_FUNDS, {
@@ -212,15 +513,50 @@
 
     if ($@) {
         $tx->rollback;
-        die "$@";
+        die $@;
     }
 
     $tx->commit;
 }
 
-sub handle_db_error {
+sub _handle_db_error {
     my ($error_str) = @_;
     LJ::Pay::Wallet::Error->raise(ERROR_DB, { 'str' => $error_str });
 }
 
 1;
+
+=head1 SEE ALSO
+
+=head2 Modules
+
+L<LJ::Pay::Wallet::Log|LJ::Pay::Wallet::Log>,
+L<LJ::Pay::Wallet::Stats|LJ::Pay::Wallet::Stats>,
+L<LJ::Pay::Wallet::Error|LJ::Pay::Wallet::Error>,
+L<LJ::Pay::Method::Wallet|LJ::Pay::Method::Wallet>,
+L<LJ::Pay::Payment::PayItem::WalletTokens|LJ::Pay::Payment::PayItem::WalletTokens>
+
+=head2 User-facing pages
+
+ # actually displays as LANDING_URL
+ /shop/wallet.bml
+
+ /admin/accounts/wallet-history.bml
+ /admin/accounts/wallet-stat.bml
+
+ # admin page, allows to modify user's balance, among other things
+ /admin/accounts/acctedit.bml
+
+=head2 Privileges
+
+ # unlocks wallet-history and wallet-stat, above
+ moneyview
+
+ # unlocks acctedit
+ moneyenter
+
+=head2 JIRA
+
+The main implementation ticket: L<https://jira.sup.com/browse/LJSUP-5634>
+
+=cut

Modified: trunk/htdocs/admin/accounts/acctedit.bml
===================================================================
--- trunk/htdocs/admin/accounts/acctedit.bml	2010-04-07 10:13:09 UTC (rev 8454)
+++ trunk/htdocs/admin/accounts/acctedit.bml	2010-04-07 10:18:58 UTC (rev 8455)
@@ -285,7 +285,8 @@
         if ($POST{'wallet_add'}) {
             my $qty = int $POST{'wallet_add'};
 
-            LJ::Pay::Wallet->try_add($u, $qty);
+            eval { LJ::Pay::Wallet->try_add($u, $qty) };
+            die "$@" if $@;
 
             my $logmsg;
             if ($qty > 0) {

Modified: trunk/htdocs/pay/modify.bml
===================================================================
--- trunk/htdocs/pay/modify.bml	2010-04-07 10:13:09 UTC (rev 8454)
+++ trunk/htdocs/pay/modify.bml	2010-04-07 10:18:58 UTC (rev 8455)
@@ -218,7 +218,7 @@
 
         $meth_class->checkout_process($cartobj);
         if ($meth_class->checkout_should_redirect($cartobj)) {
-            $meth_class->checkout_redirect($cartobj);
+            return $meth_class->checkout_redirect($cartobj);
         } else {
             $meth_class->checkout_render($cartobj, \$title, \$body);
             return;

Tags: andy, bml, dat, ljcom, pm
  • Post a new comment

    Error

    Anonymous comments are disabled in this journal

    default userpic

    Your reply will be screened

    Your IP address will be recorded 

  • 0 comments