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

[ljcom] r10833: LJSUP-9157 (Self promo)

Committer: ailyin
LJSUP-9157 (Self promo)
A   trunk/bin/maint/selfpromo.pl
U   trunk/bin/maint/taskinfo-local.txt
U   trunk/bin/upgrading/en_LJ.dat
U   trunk/bin/upgrading/update-db-local.pl
A   trunk/cgi-bin/LJ/Console/Command/SelfPromoCancel.pm
A   trunk/cgi-bin/LJ/Pay/Payment/PayItem/SelfPromo.pm
U   trunk/cgi-bin/LJ/Pay/Payment/PayItem.pm
A   trunk/cgi-bin/LJ/Pay/SelfPromo.pm
U   trunk/cgi-bin/LJ/Widget/Shop/PaymentMethods.pm
A   trunk/cgi-bin/LJ/Widget/Shop/View/SelfPromo.pm
A   trunk/htdocs/shop/selfpromo.bml
A   trunk/htdocs/shop/selfpromo.bml.text.local
U   trunk/templates/Shop/Error.tmpl
A   trunk/templates/Shop/SelfPromo.tmpl
A   trunk/templates/Shop/SelfPromoPreview.tmpl
U   trunk/templates/Shop/Warnings.tmpl
Added: trunk/bin/maint/selfpromo.pl
===================================================================
--- trunk/bin/maint/selfpromo.pl	                        (rev 0)
+++ trunk/bin/maint/selfpromo.pl	2011-08-10 06:35:54 UTC (rev 10833)
@@ -0,0 +1,13 @@
+#!/usr/bin/perl
+use strict;
+use warnings;
+
+use vars qw( %maint );
+
+use LJ::Pay::SelfPromo;
+
+$maint{'selfpromo_check'} = sub {
+    LJ::Pay::SelfPromo->check_current_entry;
+};
+
+1;

Modified: trunk/bin/maint/taskinfo-local.txt
===================================================================
--- trunk/bin/maint/taskinfo-local.txt	2011-08-10 06:25:56 UTC (rev 10832)
+++ trunk/bin/maint/taskinfo-local.txt	2011-08-10 06:35:54 UTC (rev 10833)
@@ -62,3 +62,6 @@
 
 inactive.pl:
   inactive_populate_active - populate the "active_user" table with the new users
+
+selfpromo.pl:
+  selfpromo_check - Check the current entry promoted via SelfPromo if it still should be promoted (not deleted / became ineligible / expired)

Modified: trunk/bin/upgrading/en_LJ.dat
===================================================================
--- trunk/bin/upgrading/en_LJ.dat	2011-08-10 06:25:56 UTC (rev 10832)
+++ trunk/bin/upgrading/en_LJ.dat	2011-08-10 06:35:54 UTC (rev 10833)
@@ -1,5 +1,5 @@
 ;; -*- coding: utf-8 -*-
-+msn.webiface.add_promote_liveid=[[username]]@livejournal.com has not activated LJ Messenger. Would you like to tell them about it?
+msn.webiface.add_promote_liveid=[[username]]@livejournal.com has not activated LJ Messenger. Would you like to tell them about it?
 
 account_level.plus=Plus
 
@@ -5479,6 +5479,186 @@
 secret.travel_wo_parents.first_place|staleness=1
 secret.travel_wo_parents.first_place=Where did you take your first trip without your parents?
 
+selfpromo.notification.activate.body<<
+Dear [[poster]],
+
+Promotion of your entry at [[entry_url]] was enabled successfully.
+
+Thank you for using the service.
+
+Best!
+
+LiveJournal team
+http://www.livejournal.com
+.
+
+selfpromo.notification.activate.subject=Entry promotion enabled successfully
+
+selfpromo.notification.admin_cancel.body<<
+Dear [[admin]],
+
+You have successfully cancelled the promotion of the entry at [[entry_url]].
+
+- LiveJournal
+.
+
+selfpromo.notification.admin_cancel.subject=SelfPromo cancelled
+
+selfpromo.notification.deactivate.admin.body<<
+Dear [[poster]],
+
+Promotion of your entry at [[entry_url]] has been discontinued due to a Terms of Use violation. Your entry stayed promoted for [[duration_hours]] [[?duration_hours|hour|hours|hours]]. You have been granted [[refund_amount]] [[?refund_amount|token|tokens|tokens]] as a refund.
+
+If you want to promote an entry again, you can purchase another promotion at the promotion page (http://www.livejournal.com/shop/selfpromo.bml).
+
+If you think it was removed in error, please contact our Support team (http://www.livejournal.com/support/) and let us know.
+
+Thank you for using the service!
+
+LiveJournal Team
+http://www.livejournal.com
+.
+
+selfpromo.notification.deactivate.admin.subject=Entry promotion discontinued
+
+selfpromo.notification.deactivate.buyout.body<<
+Dear [[poster]],
+
+Promotion of your entry at [[entry_url]] has been discontinued because someone else bought it out. Your entry stayed promoted for [[duration_hours]] [[?duration_hours|hour|hours|hours]]. You have been granted [[refund_amount]] [[?refund_amount|token|tokens|tokens]] as a refund.
+
+If you want to promote an entry again, you can purchase another promotion at the promotion page (http://www.livejournal.com/shop/selfpromo.bml).
+
+Thank you for using the service!
+
+LiveJournal Team
+http://www.livejournal.com
+.
+
+selfpromo.notification.deactivate.buyout.subject=Entry promotion discontinued
+
+selfpromo.notification.deactivate.deleted.body<<
+Dear [[poster]],
+
+Promotion of your entry at [[entry_url]] has been discontinued because it was deleted.
+
+Thank you for using the service!
+
+LiveJournal Team
+http://www.livejournal.com
+.
+
+selfpromo.notification.deactivate.deleted.subject=Entry promotion discontinued
+
+selfpromo.notification.deactivate.expired.body<<
+Dear [[poster]],
+
+Promotion of your entry at [[entry_url]] has expired.
+
+If you want to promote an entry again, you can purchase another promotion at the promotion page (http://www.livejournal.com/shop/selfpromo.bml).
+
+Thank you for using the service!
+
+LiveJournal Team
+http://www.livejournal.com
+.
+
+selfpromo.notification.deactivate.expired.subject=Your promotion has expired and been deactivated
+
+selfpromo.notification.deactivate.ineligible.entry_adult_content.body<<
+Dear [[poster]],
+
+Promotion of your entry at [[entry_url]] has been discontinued because it has been marked as containing adult content.
+
+Thank you for using the service!
+
+LiveJournal Team
+http://www.livejournal.com
+.
+
+selfpromo.notification.deactivate.ineligible.entry_adult_content.subject=Entry promotion discontinued
+
+selfpromo.notification.deactivate.ineligible.entry_invisible.body<<
+Dear [[poster]],
+
+Promotion of your entry at [[entry_url]] has been discontinued because it has failed to remain a visible one (i. e. it was deleted or suspended).
+
+Thank you for using the service!
+
+LiveJournal Team
+http://www.livejournal.com
+.
+
+selfpromo.notification.deactivate.ineligible.entry_invisible.subject=Entry promotion discontinued
+
+selfpromo.notification.deactivate.ineligible.entry_not_public.body<<
+Dear [[poster]],
+
+Promotion of your entry at [[entry_url]] has been discontinued because it is no longer a publicly-available one.
+
+Thank you for using the service!
+
+LiveJournal Team
+http://www.livejournal.com
+.
+
+selfpromo.notification.deactivate.ineligible.entry_not_public.subject=Entry promotion discontinued
+
+selfpromo.notification.deactivate.ineligible.journal_adult_content.body<<
+Dear [[poster]],
+
+Promotion of your entry at [[entry_url]] has been discontinued because your journal has been marked as containing adult content.
+
+Thank you for using the service!
+
+LiveJournal Team
+http://www.livejournal.com
+.
+
+selfpromo.notification.deactivate.ineligible.journal_adult_content.subject=Entry promotion discontinued
+
+selfpromo.notification.deactivate.ineligible.journal_invisible.body<<
+Dear [[poster]],
+
+Promotion of your entry at [[entry_url]] has been discontinued because that journal has failed to remain a visible one (i. e. it was deleted or suspended).
+
+Thank you for using the service!
+
+LiveJournal Team
+http://www.livejournal.com
+.
+
+selfpromo.notification.deactivate.ineligible.journal_invisible.subject=Entry promotion discontinued
+
+selfpromo.notification.deactivate.ineligible.poster_invisible.body<<
+Dear [[poster]],
+
+Promotion of your entry at [[entry_url]] has been discontinued because your account has failed to remain a visible one (i. e. it was deleted or suspended).
+
+Thank you for using the service!
+
+LiveJournal Team
+http://www.livejournal.com
+.
+
+selfpromo.notification.deactivate.ineligible.poster_invisible.subject=Entry promotion discontinued
+
+selfpromo.notification.deactivate.withdraw.body<<
+Dear [[poster]],
+
+You have successfully withdrawn promotion of your entry at [[entry_url]], so it is no longer promoted.
+
+Thank you for using the service!
+
+LiveJournal Team
+http://www.livejournal.com
+.
+
+selfpromo.notification.deactivate.withdraw.subject=Entry promotion withdrawn
+
+selfpromo.shop_item.name=Promotion of the <a href="[[entry_url]]">entry</a>
+
+selfpromo.shop_item.type=Self Promo service
+
 service_page_reskining.xbox.url=http://sixapart.adbureau.net/adclick/CID=000016fb0000000000000000
 
 setting.sg.invitations.title=New setting

Modified: trunk/bin/upgrading/update-db-local.pl
===================================================================
--- trunk/bin/upgrading/update-db-local.pl	2011-08-10 06:25:56 UTC (rev 10832)
+++ trunk/bin/upgrading/update-db-local.pl	2011-08-10 06:35:54 UTC (rev 10833)
@@ -1693,6 +1693,23 @@
     ) ENGINE=InnoDB
 });
 
+
+## see LJ::Pay::SelfPromo
+register_tablecreate( 'selfpromo', qq{
+    CREATE TABLE `selfpromo` (
+        `promoid`   int(11) NOT NULL auto_increment,
+        `journalid` int(11) NOT NULL default '0',
+        `posterid`  int(11) NOT NULL default '0',
+        `jitemid`   int(11) NOT NULL default '0',
+        `ditemid`   int(11) NOT NULL default '0',
+        `started`   int(11) NOT NULL default '0',
+        `exptime`   int(11) NOT NULL default '0',
+        `cost`      int(11) NOT NULL default '0',
+        `active`    int(11) NOT NULL default '1',
+        PRIMARY KEY  (`promoid`)
+    ) ENGINE=InnoDB
+});
+
 # *************************************************************
 register_alter(sub {
 

Added: trunk/cgi-bin/LJ/Console/Command/SelfPromoCancel.pm
===================================================================
--- trunk/cgi-bin/LJ/Console/Command/SelfPromoCancel.pm	                        (rev 0)
+++ trunk/cgi-bin/LJ/Console/Command/SelfPromoCancel.pm	2011-08-10 06:35:54 UTC (rev 10833)
@@ -0,0 +1,50 @@
+package LJ::Console::Command::SelfPromoCancel;
+use strict;
+use warnings;
+
+use LJ::Pay::SelfPromo;
+
+use base qw( LJ::Console::Command );
+
+sub cmd {
+    return 'selfpromo_cancel';
+}
+
+sub desc {
+    return 'Cancel entry promotion in SelfPromo';
+}
+
+sub usage {
+    return '<entry_url>';
+}
+
+sub args_desc {
+    return [ 'entry_url' => 'The URL of the entry to cancel', ];
+}
+
+sub can_execute {
+    my $remote = LJ::get_remote();
+    return LJ::check_priv( $remote, 'siteadmin', 'selfpromo' );
+}
+
+sub execute {
+    my ( $self, $entry_url, @args_remainder ) = @_;
+
+    return $self->error('Too many arguments') if @args_remainder;
+
+    my $entry = LJ::Entry->new_from_url($entry_url);
+    return $self->error( 'No such entry: ' . $entry_url ) unless $entry;
+
+    my $promoted_entry = LJ::Pay::SelfPromo->current_entry;
+    return $self->error('No entry currently promoted') unless $promoted_entry;
+    return $self->error( $entry_url . ' is not the entry currently promoted' )
+        unless $entry->journalid == $promoted_entry->journalid
+            && $entry->jitemid == $promoted_entry->jitemid;
+
+    LJ::Pay::SelfPromo->admin_cancel_promo( $entry, LJ::get_remote() );
+
+    $self->info( $entry_url . ' cancelled successfully' );
+    return 1;
+}
+
+1;

Added: trunk/cgi-bin/LJ/Pay/Payment/PayItem/SelfPromo.pm
===================================================================
--- trunk/cgi-bin/LJ/Pay/Payment/PayItem/SelfPromo.pm	                        (rev 0)
+++ trunk/cgi-bin/LJ/Pay/Payment/PayItem/SelfPromo.pm	2011-08-10 06:35:54 UTC (rev 10833)
@@ -0,0 +1,195 @@
+package LJ::Pay::Payment::PayItem::SelfPromo;
+use strict;
+use warnings;
+
+use LJ::Pay::SelfPromo;
+
+use base qw( LJ::Pay::Payment::PayItem );
+
+sub item {'selfpromo'}
+
+sub get_entry {
+    my ($self) = @_;
+
+    my $journalid = $self->get_prop('selfpromo_journalid');
+    my $ditemid   = $self->get_prop('selfpromo_ditemid');
+
+    return unless $journalid && $ditemid;
+
+    return LJ::Entry->new( $journalid, 'ditemid' => $ditemid );
+}
+
+sub get_entry_url {
+    my ($self) = @_;
+
+    my $journalid = $self->get_prop('selfpromo_journalid');
+    my $ditemid   = $self->get_prop('selfpromo_ditemid');
+
+    return unless $journalid && $ditemid;
+
+    my $journal = LJ::load_userid($journalid);
+
+    return $journal->journal_base . "/$ditemid.html";
+}
+
+sub get_product_name {
+    my ($self) = @_;
+
+    return LJ::Lang::ml( 'selfpromo.shop_item.name',
+        { 'entry_url' => $self->get_entry_url } );
+}
+
+sub render_cart_item {
+    my ($self) = @_;
+
+    my $amt = $self->get_amt;
+    my $qty = $self->get_qty;
+
+    return {
+        'name' => $self->get_product_name,
+        'type' => LJ::Lang::ml('selfpromo.shop_item.type'),
+    };
+}
+
+sub is_tangible {1}
+
+sub calculate_price {
+    my ($self) = @_;
+    return $self->{'amt'};
+}
+
+sub validate_user_input {1}
+
+sub can_belong_to_special_cases {
+
+    # TODO: does it get called, to begin with?
+    return 1;
+}
+
+sub _deliver_item {
+    my ( $self, $payment, $buyer_u, $rcpt_u, $note_rec_change ) = @_;
+
+    # this should be temporary until we l10n the shop
+    my $ml = sub {
+        my ( $code, $params ) = @_;
+        return LJ::Lang::get_text( $LJ::DEFAULT_LANG, $code, undef, $params );
+    };
+
+    my $lock = LJ::Pay::SelfPromo->lock;
+
+    LJ::Pay::SelfPromo->check_current_entry;
+
+    my $type = $self->get_prop('selfpromo_type');
+
+    # if there's an entry being promoted, deactivate it, and
+    # then handle any refund if need be
+    #
+    # note that it also stays valid for admin cancellations because they
+    # also only cancel the current promotion
+    my $entry_promoted;
+    if ( $entry_promoted = LJ::Pay::SelfPromo->current_entry ) {
+        LJ::Pay::SelfPromo->deactivate_entry(
+            'entry'   => $entry_promoted,
+            'reason'  => 'buyout',
+            'details' => $self->get_piid,
+        );
+
+        if ( my $refund = $self->get_prop('selfpromo_refund') ) {
+            my $promoid = $self->get_prop('selfpromo_refund_promoid');
+            my $refund_to_userid = $self->get_prop('selfpromo_refund_userid');
+            my $refund_to        = LJ::load_userid($refund_to_userid);
+
+            my $info = LJ::Pay::SelfPromo->current_entry_info($promoid);
+
+            # credit them:
+            LJ::Pay::Wallet->try_add( $refund_to, $refund );
+            LJ::Pay::Wallet::Log->log(
+                'userid'   => $refund_to_userid,
+                'action'   => LJ::Pay::Wallet::Log::ACTION_ADD,
+                'qty'      => $refund,
+                'payid'    => $self->get_payid,
+                'piid'     => $self->get_piid,
+                'status'   => LJ::Pay::Wallet::Log::STATUS_FINISHED,
+                'time_end' => time,
+            );
+
+            my $entry_url =
+                $refund_to->journal_base . '/' . $info->{'ditemid'} . '.html';
+
+            my $reason_map = {
+                'buyout'       => 'buyout',
+                'admin_cancel' => 'admin',
+            };
+
+            # email them:
+            LJ::Pay::SelfPromo->send_notification(
+                $refund_to, 'deactivate',
+                'reason'        => $reason_map->{$type},
+                'entry_url'     => $entry_url,
+                'refund_amount' => $refund,
+                'duration'      => time - $info->{'started'},
+            );
+        }
+    }
+
+    # if there's any 'remainder' tokens we need to send, go send them:
+    if ( my $remainder = $self->get_prop('selfpromo_remainder') ) {
+        my $remainder_to_userid =
+            $self->get_prop('selfpromo_remainder_userid');
+        my $remainder_to = LJ::load_userid($remainder_to_userid);
+
+        if ($remainder_to) {
+            LJ::Pay::Wallet->try_add( $remainder_to, $remainder );
+            LJ::Pay::Wallet::Log->log(
+                'userid'   => $remainder_to_userid,
+                'action'   => LJ::Pay::Wallet::Log::ACTION_ADD,
+                'qty'      => $remainder,
+                'payid'    => $self->get_payid,
+                'piid'     => $self->get_piid,
+                'status'   => LJ::Pay::Wallet::Log::STATUS_FINISHED,
+                'time_end' => time,
+            );
+        }
+
+    }
+
+    # now that we've handled all the deactivations, refunds, and remainders,
+    # we will need to determine if we need to activate anything,
+    # and what to email, which really depends on the item type
+    my ( $email_subject, $email_body );
+
+    if ( $type eq 'buyout' ) {
+        my $entry = $self->get_entry;
+
+        my $cost = $self->get_amt * LJ::Pay::Wallet::EXCHANGE_RATE();
+        LJ::Pay::SelfPromo->activate_entry( $entry, $cost );
+
+        $email_subject = $ml->('selfpromo.notification.activate.subject');
+
+        $email_body = $ml->(
+            'selfpromo.notification.activate.body',
+            {   'poster'    => $rcpt_u->display_name,
+                'entry_url' => $entry->url,
+            },
+        );
+    }
+    elsif ( $type eq 'admin_cancel' ) {
+
+        # we don't need to activate anything, but the admin
+        # gets notified that everything was cancelled
+        # successfully
+
+        $email_subject = $ml->('selfpromo.notification.admin_cancel.subject');
+
+        $email_body = $ml->(
+            'selfpromo.notification.admin_cancel.body',
+            {   'admin'     => $rcpt_u->display_name,
+                'entry_url' => $entry_promoted ? $entry_promoted->url : '',
+            },
+        );
+    }
+
+    return ( 1, $email_subject, $email_body );
+}
+
+1;

Modified: trunk/cgi-bin/LJ/Pay/Payment/PayItem.pm
===================================================================
--- trunk/cgi-bin/LJ/Pay/Payment/PayItem.pm	2011-08-10 06:25:56 UTC (rev 10832)
+++ trunk/cgi-bin/LJ/Pay/Payment/PayItem.pm	2011-08-10 06:35:54 UTC (rev 10833)
@@ -50,7 +50,7 @@
     'donate'        => 'LJ::Pay::Payment::PayItem::Donate',
     'appitem'       => 'LJ::Pay::Payment::PayItem::AppItem',
     'royalty'       => 'LJ::Pay::Payment::PayItem::Royalty',
-
+    'selfpromo'     => 'LJ::Pay::Payment::PayItem::SelfPromo',
 );
 
 my @FREEZABLE_CLASSES = qw/
@@ -350,6 +350,29 @@
     return 1;
 }
 
+sub update {
+    my ($self, %args) = @_;
+
+    my (@sets, @binds);
+
+    foreach my $k (keys %args) {
+        $self->{$k} = $args{$k};
+
+        push @sets, "$k=?";
+        push @binds, $args{$k};
+    }
+
+    my $sets = join(',', @sets);
+
+    my $dbh = _get_dbh(); $dbh->{'RaiseError'} = 1;
+    $dbh->do(
+        qq{
+            UPDATE payitems SET $sets WHERE piid=?
+        }, undef,
+        @binds, $self->get_piid
+    );
+}
+
 ##############################################################################
 # Boolean Methods
 #

Added: trunk/cgi-bin/LJ/Pay/SelfPromo.pm
===================================================================
--- trunk/cgi-bin/LJ/Pay/SelfPromo.pm	                        (rev 0)
+++ trunk/cgi-bin/LJ/Pay/SelfPromo.pm	2011-08-10 06:35:54 UTC (rev 10833)
@@ -0,0 +1,679 @@
+package LJ::Pay::SelfPromo;
+use strict;
+use warnings;
+
+use LJ::Pay::Wallet;
+use LJ::Pay::Wallet::Error qw(:codes);
+use Exporter;
+our @ISA = qw(Exporter);
+
+my $errors;
+
+BEGIN {
+    $errors = {
+        ERROR_ENTRY_INELIGIBLE          => 1,
+        ERROR_PRICE_INSUFFICIENT        => 2,
+        ERROR_INVALID_PRICE             => 3,
+        ERROR_WALLET_INSUFFICIENT_FUNDS => 4,
+    };
+
+    our @EXPORT_OK   = ( keys %$errors );
+    our %EXPORT_TAGS = ( 'codes' => [ keys %$errors ], );
+
+}
+
+use constant $errors;
+
+### HIGH LEVEL INTERFACE ###
+
+sub buyout_cost {
+    my ($class) = @_;
+
+    if ( my $info = $class->current_entry_info ) {
+
+        # there actually is a promoted entry right now, so let's
+        # return however much its author paid for it plus the bidding step
+
+        return $info->{'cost'} + $LJ::SELF_PROMO_CONF->{'step'};
+    }
+
+    return $LJ::SELF_PROMO_CONF->{'min_cost'};
+}
+
+sub is_entry_eligible {
+    my ( $class, $entry, $promoter, $reason_ref ) = @_;
+
+    my $journal = $entry->journal;
+    my $poster  = $entry->poster;
+
+    my $err = sub {
+        my ($reason) = @_;
+
+        $$reason_ref = $reason;
+        return;
+    };
+
+    ## journal checks
+    my $journal_adult = $journal->adult_content_calculated;
+    unless ( $journal->is_visible ) { return $err->('journal_invisible'); }
+    unless ( $journal_adult eq 'none' ) {
+        return $err->('journal_adult_content');
+    }
+
+    ## poster checks
+    unless ( $poster->is_visible ) { return $err->('poster_invisible'); }
+    unless ( LJ::u_equals( $poster, $promoter ) ) {
+        return $err->('promoter_poster_mismatch');
+    }
+
+    ## entry checks
+    my $entry_adult = $entry->adult_content_calculated;
+    unless ( $entry->is_visible ) { return $err->('entry_invisible'); }
+    unless ( $entry->is_public )  { return $err->('entry_not_public'); }
+    if ( $entry_adult && $entry_adult ne 'none' ) {
+        return $err->('entry_adult_content');
+    }
+
+    # TODO: check if this is the very entry being promoted right now?
+
+    return 1;
+}
+
+sub buyout {
+    my ( $class, $entry, $promoter, $price ) = @_;
+
+    my $lock = $class->lock;
+
+    ## check entry
+    do {
+        my $reason;
+        unless ( $class->is_entry_eligible( $entry, $promoter, \$reason ) ) {
+            $class->raise_error( ERROR_ENTRY_INELIGIBLE, $reason );
+        }
+    };
+
+    ## check price
+    do {
+        my $price_additional = $price - $LJ::SELF_PROMO_CONF->{'min_cost'};
+        if ( $price_additional % $LJ::SELF_PROMO_CONF->{'step'} ) {
+            $class->raise_error(ERROR_INVALID_PRICE);
+        }
+
+        my $min_price = $class->buyout_cost;
+        unless ( $price >= $min_price ) {
+            $class->raise_error( ERROR_PRICE_INSUFFICIENT, $min_price );
+        }
+
+        my $wallet_balance = LJ::Pay::Wallet->get_user_balance($promoter);
+        unless ( $wallet_balance >= $price ) {
+            $class->raise_error( ERROR_WALLET_INSUFFICIENT_FUNDS,
+                int $wallet_balance );
+        }
+    };
+
+    # create a cart and pay for it from the user's balance;
+    # this may throw a wallet exception
+    my $cart;
+    do {
+
+        # calculate how much money goes where for this item:
+        # a part of it goes to the system, a part of it goes to the
+        # user who purchased the selfpromo that was running before the
+        # buyout, and a part of it is a rounding error that
+        # goes to a special account made for these purposes
+        my ( $refund, $refund_promoid, $refund_userid ) = ( 0, 0, 0 );
+        if ( my $info = $class->current_entry_info ) {
+            $refund         = $class->calculate_refund($info);
+            $refund_promoid = $info->{'promoid'};
+            $refund_userid  = $info->{'posterid'};
+        }
+
+        my $refund_remainder =
+            $refund % $LJ::SELF_PROMO_CONF->{'refund_step'};
+        my $refund_rounded = $refund - $refund_remainder;
+        my $profit         = $price - $refund;
+
+        my $remainder_to =
+            LJ::load_user( $LJ::SELF_PROMO_CONF->{'remainder_receiver'} );
+        my $remainder_userid = $remainder_to ? $remainder_to->userid : 0;
+
+        $cart = $class->create_shop_cart(
+            {   'cart_owner'       => $promoter,
+                'entry'            => $entry,
+                'price'            => $price,
+                'profit'           => $profit,
+                'rcptid'           => $promoter->userid,
+                'refund'           => $refund_rounded,
+                'refund_userid'    => $refund_userid,
+                'refund_promoid'   => $refund_promoid,
+                'remainder'        => $refund_remainder,
+                'remainder_userid' => $remainder_userid,
+                'type'             => 'buyout',
+            }
+        );
+
+        $cart->set_method( LJ::Pay::Method::Wallet->code );
+        LJ::Pay::Wallet->pay_for_cart( $cart->get_payid );
+    };
+
+    # deliver the cart synchronously, so that changes are applied
+    # immediately; we can do that because you can't schedule
+    # a selfpromo
+    #
+    # this part isn't supposed to throw any exceptions because
+    # all the checks have been done before
+    do {
+        my ($item) = $cart->get_items;
+
+        $cart->update( 'used'   => 'Y' );
+        $item->update( 'status' => 'pend' );
+
+        my $deliver_res = $item->deliver( $cart, time, sub { } );
+        unless ($deliver_res) {
+            die "delivering item failed, see error log for details";
+        }
+    };
+
+    return $cart;
+}
+
+sub admin_cancel_promo {
+    my ( $class, $entry, $admin ) = @_;
+
+    my $info = $class->current_entry_info;
+    unless ( $info
+        && $info->{'journalid'} == $entry->journalid
+        && $info->{'jitemid'} == $entry->jitemid )
+    {
+
+        # your princess is in another castle, sorry
+        return;
+    }
+
+    # create a cart and set it as free, and then mark it as pending
+    # delivery
+    my $cart;
+    do {
+
+        # calculate how much money goes where for this item:
+        # none it goes to the system, a part of it goes to the
+        # user who purchased the selfpromo being cancelled,
+        # and a part of it is a rounding error that goes to a
+        # special account made for these purposes
+        my $refund         = $class->calculate_refund($info);
+        my $refund_promoid = $info->{'promoid'};
+        my $refund_userid  = $info->{'posterid'};
+
+        my $refund_remainder =
+            $refund % $LJ::SELF_PROMO_CONF->{'refund_step'};
+        my $refund_rounded = $refund - $refund_remainder;
+
+        my $remainder_to =
+            LJ::load_user( $LJ::SELF_PROMO_CONF->{'remainder_receiver'} );
+        my $remainder_userid = $remainder_to ? $remainder_to->userid : 0;
+
+        $cart = $class->create_shop_cart(
+            {   'cart_owner'       => $admin,
+                'entry'            => undef,
+                'price'            => 0,
+                'profit'           => 0,
+                'rcptid'           => $admin->userid,
+                'refund'           => $refund_rounded,
+                'refund_userid'    => $refund_userid,
+                'refund_promoid'   => $refund_promoid,
+                'remainder'        => $refund_remainder,
+                'remainder_userid' => $remainder_userid,
+                'type'             => 'admin_cancel',
+            }
+        );
+
+        # mark this cart as paid for
+        $cart->set_method( LJ::Pay::Method::Free->code );
+        $cart->update( 'used' => 'N', 'mailed' => 'N' );
+        $cart->set_daterecv_epoch(time);
+    };
+
+    # deliver the cart synchronously, so that changes are applied
+    # immediately; we can do that because you can't schedule
+    # a selfpromo
+    #
+    # this part isn't supposed to throw any exceptions because
+    # all the checks have been done before
+    do {
+        my ($item) = $cart->get_items;
+
+        $cart->update( 'used'   => 'Y' );
+        $item->update( 'status' => 'pend' );
+        $item->deliver( $item->get_cart, time, sub { } );
+    };
+
+    return 1;
+}
+
+sub withdraw_entry {
+    my ( $class, $entry ) = @_;
+
+    my $info = $class->current_entry_info;
+
+    die 'entry mismatch'
+        unless $info->{'journalid'} == $entry->journalid
+            && $info->{'jitemid'} == $entry->jitemid;
+
+    $class->deactivate_promo(
+        $info->{'promoid'}, $entry->poster,
+        'reason'    => 'withdraw',
+        'entry'     => $entry,
+        'entry_url' => $entry->url,
+    );
+
+    $class->send_notification(
+        $entry->poster, 'deactivate',
+        'reason'    => 'withdraw',
+        'entry_url' => $entry->url,
+        'duration'  => $info->{'exptime'} - $info->{'started'},
+    );
+
+}
+
+sub current_entry {
+    my ($class) = @_;
+
+    my $info = $class->current_entry_info;
+    return unless $info;
+
+    return LJ::Entry->new( $info->{'journalid'},
+        'jitemid' => $info->{'jitemid'}, );
+}
+
+sub check_current_entry {
+    my ($class) = @_;
+
+    my $lock = $class->lock;
+
+    my $info  = $class->current_entry_info;
+    my $entry = $class->current_entry;
+
+    return unless $info;    # everything's fine if nothing's promoted
+
+    unless ($entry) {
+        my $poster = LJ::load_userid( $info->{'posterid'} );
+
+        # we no longer have an entry because it's been deleted,
+        # so let's reconstruct the URL
+        my $entry_url =
+            $poster->journal_base . '/' . $info->{'ditemid'} . '.html';
+
+        $class->deactivate_promo(
+            $info->{'promoid'}, $poster,
+            'reason'    => 'deleted',
+            'entry_url' => $entry_url,
+            'promoinfo' => $info,
+        );
+
+        $class->send_notification(
+            $poster, 'deactivate',
+            'reason'    => 'deleted',
+            'entry_url' => $entry->url,
+            'duration'  => time - $info->{'started'},
+        );
+    }
+
+    my $reason;
+    unless ( $class->is_entry_eligible( $entry, $entry->poster, \$reason ) ) {
+        $class->deactivate_entry(
+            'entry'   => $entry,
+            'reason'  => 'ineligible',
+            'details' => $reason,
+        );
+
+        $class->send_notification(
+            $entry->poster, 'deactivate',
+            'reason'    => 'ineligible',
+            'details'   => $reason,
+            'entry'     => $entry,
+            'entry_url' => $entry->url,
+            'duration'  => time - $info->{'started'},
+        );
+    }
+
+    unless ( $info->{'exptime'} > time ) {
+        $class->deactivate_entry( 'entry' => $entry, 'reason' => 'expired' );
+
+        $class->send_notification(
+            $entry->poster, 'deactivate',
+            'reason'    => 'expired',
+            'entry'     => $entry,
+            'entry_url' => $entry->url,
+            'duration'  => time - $info->{'started'},
+        );
+    }
+}
+
+### LOW LEVEL INTERFACE ###
+
+# !$promoid => currently promoted entry
+# $promoid  => $promotion with promoid=$promoid
+sub current_entry_info {
+    my ( $class, $promoid ) = @_;
+
+    unless ($promoid) {
+        return LJ::MemCache::get_or_set(
+            $class->memcache_key,
+            sub {
+                my $dbh = LJ::get_db_writer();
+
+                my $res =
+                    $dbh->selectall_arrayref(
+                    'SELECT * FROM selfpromo WHERE active=1',
+                    { 'Slice' => {} } );
+
+                return 0 unless @$res;
+
+                unless ( @$res == 1 ) {
+                    warn 'more than 1 entry in selfpromo, data corrupt?';
+                }
+
+                return $res->[0];
+            },
+            3600
+        );
+    }
+
+    return do {
+        my $dbh = LJ::get_db_writer();
+
+        my $info =
+            $dbh->selectrow_hashref(
+            'SELECT * FROM selfpromo WHERE promoid=?',
+            undef, $promoid, );
+
+        return unless defined $info;
+        return $info;
+    };
+}
+
+# also called from elsewhere: PayItem::SelfPromo
+# this one pretty much tries to satisfy the PayItem's needs:
+#
+# * it dies if we're trying to replace an active entry in promo
+# * it doesn't send any notifications
+sub activate_entry {
+    my ( $class, $entry, $cost ) = @_;
+
+    $class->check_current_entry;
+
+    if ( my $entry_promoted = $class->current_entry ) {
+
+        # this is an abnormal case, that's why the exception is stringly-typed
+
+        my $entry_url    = $entry->url;
+        my $promoted_url = $entry_promoted->url;
+        die
+            "cannot promote $entry_url because $promoted_url is already there";
+    }
+
+    $class->db_insert_promo(
+        'journalid' => $entry->journalid,
+        'posterid'  => $entry->posterid,
+        'jitemid'   => $entry->jitemid,
+        'ditemid'   => $entry->ditemid,
+        'started'   => time,
+        'exptime'   => time + $LJ::SELF_PROMO_CONF->{'duration'},
+        'cost'      => $cost,
+    );
+
+    $class->clear_memcache;
+
+    $class->log( $entry->poster, 'activate' );
+}
+
+# supported opts: reason, details, entry, refund
+# also called from elsewhere: PayItem::SelfPromo
+sub deactivate_entry {
+    my ( $class, %opts ) = @_;
+
+    my $info  = $class->current_entry_info;
+    my $entry = $opts{'entry'};
+
+    unless ( $info
+        && $info->{'journalid'} == $entry->journalid
+        && $info->{'jitemid'} == $entry->jitemid )
+    {
+
+        # your princess is in another castle, sorry
+        return;
+    }
+
+    $class->deactivate_promo(
+        $info->{'promoid'}, $entry->poster,
+        'reason'  => $opts{'reason'},
+        'details' => $opts{'details'},
+        'entry'   => $opts{'entry'},
+        'refund'  => $opts{'refund'},
+    );
+}
+
+# supported opts: reason, details, entry, entry_url, refund
+sub deactivate_promo {
+    my ( $class, $promoid, $poster, %opts ) = @_;
+
+    $class->db_update_promo( $promoid, 'active' => 0 );
+    $class->clear_memcache;
+
+    $class->log( $poster, 'deactivate', $opts{'reason'}, $opts{'details'} );
+}
+
+# supported opts:
+#
+# * cart_owner
+# * price
+# * rcptid
+# * type
+# * profit
+# * refund
+# * refund_userid
+# * refund_promoid
+# * remainder
+# * remainder_userid
+# * entry
+sub create_shop_cart {
+    my ( $class, $opts ) = @_;
+
+    my $cart = LJ::Pay::Payment::new_cart( $opts->{'cart_owner'} );
+
+    my $it = LJ::Pay::Payment::PayItem->new_memonly(
+        'item'    => 'selfpromo',
+        'subitem' => '',
+        'amt'     => $opts->{'price'} / LJ::Pay::Wallet::EXCHANGE_RATE,
+        'qty'     => 1,
+        'anon'    => 0,
+        'rcptid'  => $opts->{'rcptid'},
+    );
+
+    $cart->add_item(%$it);
+
+    ($it) = $cart->get_items;
+
+    # set payitem props as appropriate
+    $it->set_prop( 'selfpromo_type'           => $opts->{'type'} );
+    $it->set_prop( 'selfpromo_profit'         => $opts->{'profit'} );
+    $it->set_prop( 'selfpromo_refund'         => $opts->{'refund'} );
+    $it->set_prop( 'selfpromo_refund_userid'  => $opts->{'refund_userid'} );
+    $it->set_prop( 'selfpromo_refund_promoid' => $opts->{'refund_promoid'} );
+    $it->set_prop( 'selfpromo_remainder'      => $opts->{'remainder'} );
+    $it->set_prop(
+        'selfpromo_remainder_userid' => $opts->{'remainder_userid'} );
+
+    if ( my $entry = $opts->{'entry'} ) {
+        $it->set_prop( 'selfpromo_journalid' => $entry->journal->userid );
+        $it->set_prop( 'selfpromo_ditemid'   => $entry->ditemid );
+    }
+
+    return $cart;
+}
+
+# supported opts: reason, details, entry, refund_amount, exptime
+sub send_notification {
+    my ( $class, $poster, $action, %opts ) = @_;
+
+    my ( $ml_var, %ml_params );
+
+    if ( $action eq 'deactivate' ) {
+        my $reason = $opts{'reason'};
+        my $entry  = $opts{'entry'};    # note that it may be undefined
+
+        %ml_params = (
+            'refund_amount'  => $opts{'refund_amount'},
+            'entry_url'      => $opts{'entry_url'},
+            'duration_hours' => int( $opts{'duration'} / 3600 ),
+            'poster'         => $poster->display_name,
+        );
+
+        if ( $reason eq 'admin' ) {
+            $ml_var = 'selfpromo.notification.deactivate.admin';
+        }
+
+        if ( $reason eq 'buyout' ) {
+            $ml_var = 'selfpromo.notification.deactivate.buyout';
+        }
+
+        if ( $reason eq 'deleted' ) {
+            $ml_var = 'selfpromo.notification.deactivate.deleted';
+        }
+
+        if ( $reason eq 'expired' ) {
+            $ml_var = 'selfpromo.notification.deactivate.expired';
+        }
+
+        if ( $reason eq 'ineligible' ) {
+            my $details = $opts{'details'};
+            $ml_var = "selfpromo.notification.deactivate.ineligible.$details";
+            $ml_params{'entry_subject'} = $entry->subject_text;
+        }
+
+        if ( $reason eq 'withdraw' ) {
+            $ml_var = 'selfpromo.notification.deactivate.withdraw';
+        }
+
+    }
+
+    my $subject =
+        LJ::Lang::get_text( $LJ::DEFAULT_LANG, "$ml_var.subject", undef,
+        \%ml_params, );
+
+    my $body = LJ::Lang::get_text( $LJ::DEFAULT_LANG, "$ml_var.body", undef,
+        \%ml_params, );
+
+    LJ::send_mail(
+        {   'to'       => $poster->email_raw,
+            'from'     => $LJ::DONOTREPLY_EMAIL,
+            'fromname' => $LJ::SITENAME,
+            'subject'  => $subject,
+            'body'     => $body,
+        }
+    );
+}
+
+sub calculate_refund {
+    my ( $class, $promoinfo ) = @_;
+
+    my $unit        = $LJ::SELF_PROMO_CONF->{'refund_time_unit'};
+    my $duration    = $LJ::SELF_PROMO_CONF->{'duration'};
+    my $refund_step = $LJ::SELF_PROMO_CONF->{'refund_step'};
+
+    my $remaining_time       = $promoinfo->{'exptime'} - time;
+    my $remaining_time_units = int( $remaining_time / $unit );
+
+    my $unit_cost     = $promoinfo->{'cost'} * $unit / $duration;
+    my $refund_amount = int( $remaining_time_units * $unit_cost );
+
+    return $refund_amount;
+}
+
+sub db_insert_promo {
+    my ( $class, %args ) = @_;
+
+    my $dbh = LJ::get_db_writer();
+
+    my ( @sets, @binds );
+    while ( my ( $k, $v ) = each %args ) {
+        push @sets,  "$k=?";
+        push @binds, $v;
+    }
+    my $sets = join( ',', @sets );
+
+    $dbh->do( "INSERT INTO selfpromo SET $sets", undef, @binds );
+}
+
+sub db_update_promo {
+    my ( $class, $promoid, %args ) = @_;
+
+    my $dbh = LJ::get_db_writer();
+
+    my ( @sets, @binds );
+    while ( my ( $k, $v ) = each %args ) {
+        push @sets,  "$k=?";
+        push @binds, $v;
+    }
+    my $sets = join( ',', @sets );
+
+    $dbh->do( "UPDATE selfpromo SET $sets WHERE promoid=?",
+        undef, @binds, $promoid );
+}
+
+sub clear_memcache {
+    my ($class) = @_;
+    LJ::MemCache::delete( $class->memcache_key );
+}
+
+sub memcache_key {'self_promo_active'}
+
+sub raise_error {
+    my ( $class, @args ) = @_;
+    LJ::Pay::SelfPromo::Error->raise(@args);
+}
+
+sub lock {
+    return LJ::Pay::SelfPromo::Lock->new;
+}
+
+sub log {
+    my ( $class, $poster, $event, $reason, $details ) = @_;
+
+    # TODO: log to statushistory
+}
+
+package LJ::Pay::SelfPromo::Error;
+
+sub raise {
+    my ( $class, $type, $details ) = @_;
+    die bless { 'type' => $type, 'details' => $details }, $class;
+}
+
+sub type    { shift->{'type'} }
+sub details { shift->{'details'} }
+
+package LJ::Pay::SelfPromo::Lock;
+
+my $locked = 0;
+
+sub new {
+    my ($class) = @_;
+
+    return if $locked;
+
+    LJ::get_lock( LJ::get_db_writer(), 'global', 'selfpromo' );
+
+    $locked = 1;
+    return bless {}, $class;
+}
+
+sub DESTROY {
+    my ($self) = @_;
+
+    LJ::release_lock( LJ::get_db_writer(), 'global', 'selfpromo' );
+
+    $locked = 0;
+}
+
+1;

Modified: trunk/cgi-bin/LJ/Widget/Shop/PaymentMethods.pm
===================================================================
--- trunk/cgi-bin/LJ/Widget/Shop/PaymentMethods.pm	2011-08-10 06:25:56 UTC (rev 10832)
+++ trunk/cgi-bin/LJ/Widget/Shop/PaymentMethods.pm	2011-08-10 06:35:54 UTC (rev 10833)
@@ -13,35 +13,43 @@
 sub render_body {
     my ($self, %opts) = @_;
     my $ret = '';
+
+    my $ml_title = LJ::Lang::ml('/shop/index.bml.storefront.paymentmethods.title');
     $ret .= qq{
             <div class="right-mod"><div class="mod-tl"><div class="mod-tr"><div class="mod-br"><div class="mod-bl">
             <div class="w-head">
-                <h2><span class='w-head-in'><?_ml /shop/index.bml.storefront.paymentmethods.title _ml?></span></h2><i class="w-head-corner"></i>
+                <h2><span class='w-head-in'>$ml_title</span></h2><i class="w-head-corner"></i>
             </div>
             <div class="w-body">
     };
     if (LJ::SUP->is_remote_sup) {
-         $ret .= qq{
-                 <a href="<?SITEROOT?>/support/faqbrowse.bml?faqid=21#methods" class="paymentmethods-logos">
-                     <img src="<?IMGPREFIX?>/shop/payment/paypal.png" alt="PayPal" />
-                     <img src="<?IMGPREFIX?>/shop/payment/yad.png" alt="<?_ml /shop/index.bml.storefront.paymentmethods.yad _ml?>" />
-                     <img src="<?IMGPREFIX?>/shop/payment/webmoney.png" alt="WebMoney" /><br />
-                     <img src="<?IMGPREFIX?>/shop/payment/visa.png" alt="VISA" />
-                     <img src="<?IMGPREFIX?>/shop/payment/mastercard.png" alt="MasterCard" />
-                     <img src="<?IMGPREFIX?>/shop/payment/aexpress.png" alt="American Expess" />
-                     <img src="<?IMGPREFIX?>/shop/payment/dnovus.png" alt="Discover Novus" />
+        my $ml_yandex_money
+            = LJ::Lang::ml('/shop/index.bml.storefront.paymentmethods.yad');
+
+        my $ml_sup_text
+            = LJ::Lang::ml('/shop/index.bml.storefront.paymentmethods.sup_text');
+
+        $ret .= qq{
+                 <a href="$LJ::SITEROOT/support/faqbrowse.bml?faqid=21#methods" class="paymentmethods-logos">
+                     <img src="$LJ::IMGPREFIX/shop/payment/paypal.png" alt="PayPal" />
+                     <img src="$LJ::IMGPREFIX/shop/payment/yad.png" alt="$ml_yandex_money" />
+                     <img src="$LJ::IMGPREFIX/shop/payment/webmoney.png" alt="WebMoney" /><br />
+                     <img src="$LJ::IMGPREFIX/shop/payment/visa.png" alt="VISA" />
+                     <img src="$LJ::IMGPREFIX/shop/payment/mastercard.png" alt="MasterCard" />
+                     <img src="$LJ::IMGPREFIX/shop/payment/aexpress.png" alt="American Expess" />
+                     <img src="$LJ::IMGPREFIX/shop/payment/dnovus.png" alt="Discover Novus" />
                 </a>
-                 <span class="shop-payment-text-link"><a href="<?SITEROOT?>/sup/sms_tos.bml"><?_ml /shop/index.bml.storefront.paymentmethods.sup_text _ml?></a></span>
+                 <span class="shop-payment-text-link"><a href="$LJ::SITEROOT/sup/sms_tos.bml">$ml_sup_text</a></span>
         };
     } else {
         $ret .= qq{ 
-            <a href="<?SITEROOT?>/support/faqbrowse.bml?faqid=21#methods" class="paymentmethods-logos">
-                 <img src="<?IMGPREFIX?>/shop/payment/paypal.png" alt="PayPal" />
-                 <img src="<?IMGPREFIX?>/shop/payment/webmoney.png" alt="WebMoney" /><br />
-                 <img src="<?IMGPREFIX?>/shop/payment/visa.png" alt="VISA" />
-                 <img src="<?IMGPREFIX?>/shop/payment/mastercard.png" alt="MasterCard" />
-                 <img src="<?IMGPREFIX?>/shop/payment/aexpress.png" alt="American Expess" />
-                 <img src="<?IMGPREFIX?>/shop/payment/dnovus.png" alt="Discover Novus" />
+            <a href="$LJ::SITEROOT/support/faqbrowse.bml?faqid=21#methods" class="paymentmethods-logos">
+                 <img src="$LJ::IMGPREFIX/shop/payment/paypal.png" alt="PayPal" />
+                 <img src="$LJ::IMGPREFIX/shop/payment/webmoney.png" alt="WebMoney" /><br />
+                 <img src="$LJ::IMGPREFIX/shop/payment/visa.png" alt="VISA" />
+                 <img src="$LJ::IMGPREFIX/shop/payment/mastercard.png" alt="MasterCard" />
+                 <img src="$LJ::IMGPREFIX/shop/payment/aexpress.png" alt="American Expess" />
+                 <img src="$LJ::IMGPREFIX/shop/payment/dnovus.png" alt="Discover Novus" />
             </a>
         };
     }

Added: trunk/cgi-bin/LJ/Widget/Shop/View/SelfPromo.pm
===================================================================
--- trunk/cgi-bin/LJ/Widget/Shop/View/SelfPromo.pm	                        (rev 0)
+++ trunk/cgi-bin/LJ/Widget/Shop/View/SelfPromo.pm	2011-08-10 06:35:54 UTC (rev 10833)
@@ -0,0 +1,255 @@
+package LJ::Widget::Shop::View::SelfPromo;
+use strict;
+use warnings;
+
+use base qw( LJ::Widget::Shop::View );
+
+use LJ::Pay::SelfPromo qw(:codes);
+
+sub require_remote {1}
+sub require_cart   {0}
+
+sub get_subpage       {'selfpromo'}
+sub get_template_file {'SelfPromo'}
+
+sub get_page_params {
+    my ($self) = @_;
+
+    my $remote = LJ::get_remote();
+
+    my $buyout_cost      = LJ::Pay::SelfPromo->buyout_cost;
+    my $ml_current_price = LJ::Lang::ml( '/shop/selfpromo.bml.current_price',
+        { 'price' => $buyout_cost } );
+
+    my $data_selfpromo = LJ::PersonalStats->get_selfpromo_data;
+    foreach my $row (@$data_selfpromo) {
+        my $entry =
+            LJ::Entry->new( $row->{'journal_id'},
+            'ditemid' => $row->{'post_id'}, );
+
+        $row->{'can_withdraw'} = $entry->poster->equals($remote);
+    }
+
+    my $hide_buyout = 0;
+
+    # if ( my $entry = LJ::Pay::SelfPromo->current_entry ) {
+    # $hide_buyout = ( $entry->posterid == $remote->userid );
+    # }
+
+    return {
+        'ml_current_price' => $ml_current_price,
+        'buyout_cost'    => LJ::Request->post_param('price') || $buyout_cost,
+        'data_selfpromo' => $data_selfpromo,
+        'hide_buyout'    => $hide_buyout,
+    };
+}
+
+sub process_post_request {
+    my ($self) = @_;
+
+    unless ( LJ::check_form_auth() ) {
+        $self->ml_error('error.invalidform');
+    }
+
+    my $action = LJ::Request->post_param('action');
+
+    if ( $action eq 'promote' ) {
+        return $self->process_promote_request;
+    }
+
+    if ( $action eq 'withdraw' ) {
+        return $self->process_withdraw_request;
+    }
+
+    $self->ml_error('error.invalidform');
+
+    return;
+
+}
+
+sub process_preview_request {
+    my ($self) = @_;
+
+    my $format_result = sub {
+        my ( $result, $html ) = @_;
+
+        return LJ::JSON->to_json( { 'result' => $result, 'html' => $html } );
+    };
+
+    my $entry_url = LJ::Request->post_param('entry_link');
+    my $entry     = LJ::Entry->new_from_url($entry_url);
+
+    unless ($entry) {
+        my $message =
+            LJ::Lang::ml(
+            '/shop/selfpromo.bml.error.entry_ineligible.not_found',
+            { 'entry_url' => LJ::ehtml($entry_url) } );
+        return $format_result->( 'error', $message );
+    }
+
+    $entry_url = $entry->url;
+
+    my $remote = LJ::get_remote();
+    my $ineligible_reason;
+    my $entry_eligible =
+        LJ::Pay::SelfPromo->is_entry_eligible( $entry, $remote,
+        \$ineligible_reason );
+
+    unless ($entry_eligible) {
+        my $ml_var = '/shop/selfpromo.bml.error.entry_ineligible.'
+            . $ineligible_reason;
+
+        my $message = LJ::Lang::ml(
+            $ml_var,
+            {   'entry_url'      => $entry_url,
+                'journal_ljuser' => $entry->journal->ljuser_display,
+                'poster_ljuser'  => $entry->poster->ljuser_display,
+            },
+        );
+
+        return $format_result->( 'error', $message );
+    }
+
+    # seems eligible, let's format an html for it
+    # and return it
+    my $template = LJ::HTML::Template->new(
+        { use_expr => 1 },    # force HTML::Template::Pro with Expr support
+        filename => "$ENV{'LJHOME'}/templates/Shop/SelfPromoPreview.tmpl",
+    ) or die "Can't open template: $!";
+
+    my $data =
+        LJ::PersonalStats->get_selfpromo_data( { 'fake_entry' => $entry, } );
+    $template->param( 'data_selfpromo' => $data );
+
+    return $format_result->( 'success', $template->output );
+}
+
+sub process_promote_request {
+    my ($self) = @_;
+
+    my $entry_url = LJ::Request->post_param('entry_link');
+    my $entry     = LJ::Entry->new_from_url($entry_url);
+
+    unless ($entry) {
+        $self->ml_error(
+            '/shop/selfpromo.bml.error.entry_ineligible.not_found',
+            { 'entry_url' => LJ::ehtml($entry_url) } );
+    }
+
+    $entry_url = $entry->url;
+
+    my $remote = LJ::get_remote();
+    my $ineligible_reason;
+    my $entry_eligible =
+        LJ::Pay::SelfPromo->is_entry_eligible( $entry, $remote,
+        \$ineligible_reason );
+
+    unless ($entry_eligible) {
+        my $ml_var = '/shop/selfpromo.bml.error.entry_ineligible.'
+            . $ineligible_reason;
+
+        $self->ml_error(
+            $ml_var,
+            {   'entry_url'      => $entry_url,
+                'journal_ljuser' => $entry->journal->ljuser_display,
+                'poster_ljuser'  => $entry->poster->ljuser_display,
+            },
+        );
+    }
+
+    my $price = int( LJ::Request->post_param('price') || 0 );
+    my $cart;
+
+    my $buyout_res = eval {
+        $cart = LJ::Pay::SelfPromo->buyout( $entry, $remote, $price );
+        1;
+    };
+
+    unless ($buyout_res) {
+        my $err = $@;
+
+        if ( ref $err && $err->isa('LJ::Pay::SelfPromo::Error') ) {
+            if ( $err->type == ERROR_ENTRY_INELIGIBLE ) {
+
+                # unlikely because we already checked for that, but still,
+                # let's raise it here as well
+                my $ml_var = '/shop/selfpromo.bml.error.entry_ineligible.'
+                    . $err->details;
+
+                $self->ml_error(
+                    $ml_var,
+                    {   'entry_url'      => $entry_url,
+                        'journal_ljuser' => $entry->journal->ljuser_display,
+                        'poster_ljuser'  => $entry->poster->ljuser_display,
+                    },
+                );
+            }
+
+            if ( $err->type == ERROR_PRICE_INSUFFICIENT ) {
+                $self->ml_error(
+                    '/shop/selfpromo.bml.error.price_insufficient',
+                    {   'price' => $price,
+                        'need'  => LJ::Pay::SelfPromo->buyout_cost,
+                    },
+                );
+            }
+
+            if ( $err->type == ERROR_INVALID_PRICE ) {
+                $self->ml_error(
+                    '/shop/selfpromo.bml.error.price_invalid',
+                    { 'price' => $price, },
+                );
+            }
+
+            if ( $err->type == ERROR_WALLET_INSUFFICIENT_FUNDS ) {
+                $self->ml_error(
+                    '/shop/selfpromo.bml.error.wallet_insufficient_funds',
+                    {   'need' => $price,
+                        'have' => $err->details,
+                    },
+                );
+            }
+
+            $self->raise_errors( 'Unknown error, ' . 'code='
+                    . $err->type . ','
+                    . 'details='
+                    . $err->details );
+        }
+
+        # some weird error, let's re-raise it anyway
+        $self->raise_errors("$err");
+    }
+
+    LJ::Request->redirect(
+        "$LJ::SITEROOT/shop/thankyou.bml?cart=" . $cart->get_cart_as_string );
+}
+
+sub process_withdraw_request {
+    my ($self) = @_;
+
+    LJ::Pay::SelfPromo->check_current_entry;
+
+    my $entry_url = LJ::Request->post_param('withdraw_url');
+    my $entry     = LJ::Pay::SelfPromo->current_entry;
+
+    unless ($entry) {
+
+        # apparently check_current_entry cleared it out,
+        # let's silently redirect them back
+        return LJ::Request->redirect("$LJ::SITEROOT/shop/selfpromo.bml");
+    }
+
+    unless ( $entry->url eq $entry_url ) {
+
+        # not withdrawing the correct entry, race condition?
+        # whatever, let's return an invalidform and let them
+        # figure it out
+        $self->ml_error('error.invalidform');
+    }
+
+    LJ::Pay::SelfPromo->withdraw_entry($entry);
+
+    return LJ::Request->redirect("$LJ::SITEROOT/shop/selfpromo.bml");
+}
+
+1;

Added: trunk/htdocs/shop/selfpromo.bml
===================================================================
--- trunk/htdocs/shop/selfpromo.bml	                        (rev 0)
+++ trunk/htdocs/shop/selfpromo.bml	2011-08-10 06:35:54 UTC (rev 10833)
@@ -0,0 +1,20 @@
+<?_code {
+
+use strict;
+use warnings;
+
+if ( LJ::Request->did_post ) {
+    my $action = LJ::Request->post_param('action');
+    if ( $action eq 'preview' ) {
+        my $widget = LJ::Widget::Shop::View::SelfPromo->new;
+        $widget->premature_checks;
+        return $widget->process_preview_request;
+    }
+}
+
+return BML::render_page({
+    'title' => LJ::Lang::ml('/shop/selfpromo.bml.title'),
+    'body'  => LJ::Widget::Shop::View::SelfPromo->render || undef,
+});
+
+} _code?>

Added: trunk/htdocs/shop/selfpromo.bml.text.local
===================================================================
--- trunk/htdocs/shop/selfpromo.bml.text.local	                        (rev 0)
+++ trunk/htdocs/shop/selfpromo.bml.text.local	2011-08-10 06:35:54 UTC (rev 10833)
@@ -0,0 +1,40 @@
+.btn.submit=Promote!
+
+.current_price=[[price]] [[?price|LJ Token|LJ Tokens|LJ Tokens]]
+
+.error.entry_ineligible.entry_adult_content=The entry at <a href="[[entry_url]]">[[entry_url]]</a> cannot be promoted because it is set to indicate that it contains adult content.
+
+.error.entry_ineligible.entry_invisible=The entry at <a href="[[entry_url]]">[[entry_url]]</a> cannot be promoted because it is not a visible entry.
+
+.error.entry_ineligible.entry_not_public=The entry at <a href="[[entry_url]]">[[entry_url]]</a> cannot be promoted because it is not publicly-available.
+
+.error.entry_ineligible.journal_adult_content=The entry at <a href="[[entry_url]]">[[entry_url]]</a> cannot be promoted because [[journal_ljuser]] is set to indicate that it contains adult content.
+
+.error.entry_ineligible.journal_invisible=The entry at <a href="[[entry_url]]">[[entry_url]]</a> cannot be promoted because [[journal_ljuser]] is not a visible journal.
+
+.error.entry_ineligible.not_found=We couldn't find any entry at <a href="[[entry_url]]">[[entry_url]]</a>.
+
+.error.entry_ineligible.poster_invisible=The entry at <a href="[[entry_url]]">[[entry_url]]</a> cannot be promoted because its author, [[poster_ljuser]], is not a visible user.
+
+.error.entry_ineligible.promoter_poster_mismatch=You cannot promote the entry at <a href="[[entry_url]]">[[entry_url]]</a> because you are not its author.
+
+.error.price_insufficient=[[price]] [[?price|LJ Token is|LJ Tokens are]] insufficient; you will need to pay at least [[need]] [[?need|LJ Token|LJ Tokens]].
+
+.error.price_invalid=[[price]] [[?price|LJ Token|LJ Tokens]] is an invalid price; price must be a multiple of 100.
+
+.error.wallet_insufficient_funds=You only have [[have]] [[?have|LJ Token|LJ Tokens]] in your Wallet, which is not enough to pay [[need]] [[?need|LJ Token||LJ Tokens]] for promotion.
+
+.label.current_price=Current price:
+
+.label.entry_link=Link to your post:
+
+.label.price=Your price:
+
+.placeholder.entry_link=Paste your URL here
+
+.placeholder.price=LJ Tokens
+
+.price_description=Enter number of tokens (a multiple of 100) you would like to pay.
+
+.title=Self Promo
+

Modified: trunk/templates/Shop/Error.tmpl
===================================================================
--- trunk/templates/Shop/Error.tmpl	2011-08-10 06:25:56 UTC (rev 10832)
+++ trunk/templates/Shop/Error.tmpl	2011-08-10 06:35:54 UTC (rev 10833)
@@ -1,11 +1,11 @@
 <TMPL_IF errors_occured>
-    <?errorbar
+    <div class="errorbar" style="background-image: URL('<TMPL_VAR lj_imgprefix>/message-error.gif');">
         <strong><TMPL_VAR expr="ml('error.procrequest')"></strong>
         <ul>
             <TMPL_LOOP name="errors">
                 <li><TMPL_VAR error></li>
             </TMPL_LOOP>
         </ul>
-    errorbar?>
+    </div>
 </TMPL_IF>
 

Added: trunk/templates/Shop/SelfPromo.tmpl
===================================================================
--- trunk/templates/Shop/SelfPromo.tmpl	                        (rev 0)
+++ trunk/templates/Shop/SelfPromo.tmpl	2011-08-10 06:35:54 UTC (rev 10833)
@@ -0,0 +1,122 @@
+[homer.png]
+
+<TMPL_INCLUDE name="templates/Shop/Error.tmpl">
+<div id="selfpromo-preview-errors" style="background-image: URL('<TMPL_VAR lj_imgprefix>/message-error.gif'); display: none;"  class="errorbar"></div>
+<TMPL_INCLUDE name="templates/Shop/Warnings.tmpl">
+
+<TMPL_LOOP data_selfpromo>
+<div class="selfpromo">
+    <TMPL_IF can_withdraw>
+        <form action="" method="post">
+        <TMPL_VAR form_auth>
+        <input type="hidden" name="action" value="withdraw">
+        <input type="hidden" name="withdraw_url" value="<TMPL_VAR post_url>">
+
+            <p>You already promote an entry</p>
+            <dl class="rate-posts-item">
+                <dt>
+                    <em><a href="<TMPL_VAR post_url>"><TMPL_IF subject><TMPL_VAR subject><TMPL_ELSE>no subject</TMPL_IF></a></em>
+                    <TMPL_VAR ljuser>
+                </dt>
+                <TMPL_IF body><dd><TMPL_VAR body></dd></TMPL_IF>
+            </dl>
+
+            <button type="submit">Withdraw</button>
+        </form>
+    <TMPL_ELSE>
+        <p>Somebody already promotes an entry</p>
+        <dl class="rate-posts-item">
+            <dt>
+                <em><a href="<TMPL_VAR post_url>"><TMPL_IF subject><TMPL_VAR subject><TMPL_ELSE>no subject</TMPL_IF></a></em>
+                <TMPL_VAR ljuser>
+            </dt>
+            <TMPL_IF body><dd><TMPL_VAR body></dd></TMPL_IF>
+        </dl>
+    </TMPL_IF>
+</div>
+
+<hr>
+
+</TMPL_LOOP>
+
+<TMPL_UNLESS hide_buyout>
+
+    <form action="" method="post">
+    <TMPL_VAR form_auth>
+    <input type="hidden" name="action" value="promote">
+
+    <table>
+        <tr>
+            <td><label for="entry_link"><TMPL_VAR expr="ml('/shop/selfpromo.bml.label.entry_link')"></label></td>
+            <td>
+                <input type="text" name="entry_link" id="entry_link" value="<TMPL_VAR form:entry_link ESCAPE=HTML>">
+                <button type="button" id="selfpromo-preview-button">Preview</button>
+            </td>
+        </tr>
+
+        <tr>
+            <td><TMPL_VAR expr="ml('/shop/selfpromo.bml.label.current_price')"></td>
+            <td><span id="current_price"><TMPL_VAR ml_current_price></span></td>
+        </tr>
+
+        <tr>
+            <td><label for="price"><TMPL_VAR expr="ml('/shop/selfpromo.bml.label.price')"></label></td>
+            <td><input type="text" name="price" id="price" value="<TMPL_VAR buyout_cost ESCAPE=HTML>"></td>
+        </tr>
+
+        <tr>
+            <td colspan="2">
+                <p id="price_description"><TMPL_VAR expr="ml('/shop/selfpromo.bml.price_description')"></p>
+            </td>
+        </tr>
+
+        <tr style="display:none;">
+            <td id="selfpromo-preview" colspan="2">&nbsp;</td>
+        </tr>
+
+        <tr>
+            <td colspan="2">
+                <button type="submit"><TMPL_VAR expr="ml('/shop/selfpromo.bml.btn.submit')"></button>
+            </td>
+        </tr>
+
+    </table>
+
+    </form>
+
+</TMPL_UNLESS>
+
+<script type="text/javascript">
+    window.shop_selfpromo_ml = {
+        'entry_link_placeholder' : "<TMPL_VAR expr="ml('/shop/selfpromo.bml.placeholder.entry_link')">",
+        'price_placeholder'      : "<TMPL_VAR expr="ml('/shop/selfpromo.bml.placeholder.price')">",
+    };
+
+    jQuery(function($) {
+        $("#selfpromo-preview-button").click(function(e) {
+            e.preventDefault();
+
+            $.post(
+                '/shop/selfpromo.bml',
+                {
+                    'action...
 (truncated)
Tags: andy, bml, dat, ljcom, local, pl, pm, tmpl, txt
Subscribe
  • 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