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"> </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)