[dw-free] Gift certificate/credits system
[commit: http://hg.dwscoalition.org/dw-free/rev/e0b4a1757c52]
http://bugs.dwscoalition.org/show_bug.cgi?id=135
Payment system updates. This adds support for points, the new currency that
the shop uses and lets people buy. Many shop updates.
This also adds most of the functionality for implementing custom credit card
processors using hooks. This will allow downstream sites to pick their own
merchants and do their own custom transaction handling.
Patch by
mark.
Files modified:
http://bugs.dwscoalition.org/show_bug.cgi?id=135
Payment system updates. This adds support for points, the new currency that
the shop uses and lets people buy. Many shop updates.
This also adds most of the functionality for implementing custom credit card
processors using hooks. This will allow downstream sites to pick their own
merchants and do their own custom transaction handling.
Patch by
Files modified:
- bin/upgrading/en.dat
- bin/upgrading/proplists.dat
- bin/upgrading/update-db-general.pl
- bin/worker/shop-creditcard-charge
- cgi-bin/DW/Controller/Shop.pm
- cgi-bin/DW/Hooks/HolidayPromotion.pm
- cgi-bin/DW/Request.pm
- cgi-bin/DW/Request/Apache2.pm
- cgi-bin/DW/Shop.pm
- cgi-bin/DW/Shop/Cart.pm
- cgi-bin/DW/Shop/Engine.pm
- cgi-bin/DW/Shop/Engine/CheckMoneyOrder.pm
- cgi-bin/DW/Shop/Engine/CreditCard.pm
- cgi-bin/DW/Shop/Engine/CreditCardPP.pm
- cgi-bin/DW/Shop/Engine/GoogleCheckout.pm
- cgi-bin/DW/Shop/Engine/PayPal.pm
- cgi-bin/DW/Shop/Item.pm
- cgi-bin/DW/Shop/Item/Account.pm
- cgi-bin/DW/Shop/Item/Points.pm
- cgi-bin/DW/Template.pm
- cgi-bin/DW/Widget/ShopCartStatusBar.pm
- cgi-bin/DW/Widget/ShopItemGroupDisplay.pm
- cgi-bin/LJ/User.pm
- cgi-bin/LJ/Widget/ShopCart.pm
- cgi-bin/LJ/Widget/ShopItemOptions.pm
- cvs/multicvs.conf
- htdocs/admin/pay/index.bml
- htdocs/admin/pay/view.bml
- htdocs/img/shop/logo_amex.gif
- htdocs/img/shop/logo_discover.gif
- htdocs/img/shop/logo_mastercard.gif
- htdocs/img/shop/logo_visa.gif
- htdocs/img/silk/site/cart.png
- htdocs/img/silk/site/cart_add.png
- htdocs/img/silk/site/cart_delete.png
- htdocs/img/silk/site/cart_edit.png
- htdocs/img/silk/site/cart_error.png
- htdocs/img/silk/site/cart_go.png
- htdocs/img/silk/site/cart_put.png
- htdocs/img/silk/site/cart_remove.png
- htdocs/js/shop/creditcard.js
- htdocs/shop.bml
- htdocs/shop.bml.text
- htdocs/shop/account.bml
- htdocs/shop/cart.bml
- htdocs/shop/cart.bml.text
- htdocs/shop/checkout.bml
- htdocs/shop/confirm.bml
- htdocs/shop/confirm.bml.text
- htdocs/shop/creditcard.bml
- htdocs/shop/creditcard_wait.bml
- htdocs/shop/creditcard_wait.bml.text
- htdocs/shop/entercc.bml
- htdocs/shop/entercc.bml.text
- htdocs/shop/history.bml
- htdocs/shop/receipt.bml.text
- htdocs/stc/blueshift/blueshift.css
- htdocs/stc/celerity/celerity.css
- htdocs/stc/gradation/gradation-vertical.css
- htdocs/stc/shop.css
- views/shop/cartdisplay.tt
- views/shop/cartdisplay.tt.text
- views/shop/index.tt
- views/shop/index.tt.text
- views/shop/points.tt
- views/shop/points.tt.text
--------------------------------------------------------------------------------
diff -r edaf4b8bc572 -r e0b4a1757c52 bin/upgrading/en.dat
--- a/bin/upgrading/en.dat Mon Apr 05 19:14:58 2010 -0700
+++ b/bin/upgrading/en.dat Tue Apr 06 03:06:20 2010 +0000
@@ -3085,6 +3085,35 @@ The [[sitename]] Team
shop.email.email.other.subject=[[sitename]] Account Purchase
+shop.email.gift.other.body<<
+Dear [[touser]],
+
+Your [[sitename]] account has received a gift from [[fromuser]]! Your account
+has been credited with:
+
+ [[gift]]
+
+
+Regards,
+The [[sitename]] Team
+.
+
+shop.email.gift.other.subject=[[sitename]] Gift Received
+
+shop.email.gift.self.body<<
+Dear [[touser]],
+
+Your [[sitename]] account has now been credited with the following:
+
+ [[gift]]
+
+
+Regards,
+The [[sitename]] Team
+.
+
+shop.email.gift.self.subject=[[sitename]] Account Credited
+
shop.email.user.anon.body<<
Dear [[touser]],
@@ -3305,10 +3334,22 @@ shop.item.account.conflicts.differentpai
shop.item.account.conflicts.multipleperms=You cannot purchase more than one seed account for the same person.
-shop.item.account.name=[[name]] ([[num]] [[?num|month|months]])
+shop.item.account.name=[[points]] points for a [[name]] ([[num]] [[?num|month|months]])
+
+shop.item.account.name.nopoints=[[name]] ([[num]] [[?num|month|months]])
+
+shop.item.account.name.perm=[[points]] points for a [[name]]
shop.item.account.randomuser=Random active free user
+shop.item.points.canbeadded.invalidjournaltype=You can only buy points for a personal journal.
+
+shop.item.points.canbeadded.notauser=You can only buy points for an active account.
+
+shop.item.points.canbeadded.outofrange=Minimum purchase is 30 points, maximum is 5000.
+
+shop.item.points.name=[[num]] [[sitename]] Points
+
sitescheme.accountlinks.account=Account
sitescheme.accountlinks.btn.login=Log in
@@ -4601,26 +4642,22 @@ widget.shopcart.header.remove=Remove?
widget.shopcart.header.to=To
-widget.shopcart.paymentmethod=Payment Method:
+widget.shopcart.paymentmethod=Select a Payment Method:
widget.shopcart.paymentmethod.checkmoneyorder=Check/Money Order
+widget.shopcart.paymentmethod.creditcard=Credit Card
+
widget.shopcart.paymentmethod.creditcardpp=Credit Card
+widget.shopcart.paymentmethod.free=Free/No Cost Order
+
widget.shopcart.paymentmethod.gco=Google Checkout Account
widget.shopcart.paymentmethod.paypal=PayPal Account
widget.shopcart.total=Total:
-widget.shopcartstatusbar.header=Shopping Cart
-
-widget.shopcartstatusbar.itemcount=[[num]] [[?num|item|items]] for a total of [[price]]
-
-widget.shopcartstatusbar.newcart=Discard Cart
-
-widget.shopcartstatusbar.viewcart=View Cart
-
widget.shopitemgroupdisplay.paidaccounts.header=Buy a Paid Account
widget.shopitemgroupdisplay.paidaccounts.item.circleaccount=<a [[aopts]]>For an account in your Circle</a>
@@ -4655,9 +4692,9 @@ widget.shopitemoptions.header.seed=Seed
widget.shopitemoptions.highlight.seed=- Fewer than [[num]] remaining!
-widget.shopitemoptions.price=[[num]] [[?num|month|months]] for [[price]]
-
-widget.shopitemoptions.price.seed=Forever for [[price]]
+widget.shopitemoptions.price=[[num]] [[?num|month|months]] for [[points]] points ([[price]])
+
+widget.shopitemoptions.price.seed=Forever for [[points]] points ([[price]])
widget.sitemessages.title=[[sitename]] Plugs In
diff -r edaf4b8bc572 -r e0b4a1757c52 bin/upgrading/proplists.dat
--- a/bin/upgrading/proplists.dat Mon Apr 05 19:14:58 2010 -0700
+++ b/bin/upgrading/proplists.dat Tue Apr 06 03:06:20 2010 +0000
@@ -974,6 +974,14 @@ userproplist.safe_search:
multihomed: 0
prettyname: Safe Search Filtering
+userproplist.shop_points:
+ cldversion: 0
+ datatype: num
+ des: current points balance
+ indexed: 0
+ multihomed: 0
+ prettyname: How many shop points the user has available
+
userproplist.sidx_bdate:
cldversion: 0
datatype: char
diff -r edaf4b8bc572 -r e0b4a1757c52 bin/upgrading/update-db-general.pl
--- a/bin/upgrading/update-db-general.pl Mon Apr 05 19:14:58 2010 -0700
+++ b/bin/upgrading/update-db-general.pl Tue Apr 06 03:06:20 2010 +0000
@@ -3015,6 +3015,60 @@ CREATE TABLE sitekeywords (
)
EOC
+# this table is included, even though it's not used in the stock dw-free
+# installation. but if you want to use it, you can, or you can ignore it
+# and make your own which you might have to do.
+register_tablecreate('cc_trans', <<'EOC');
+CREATE TABLE cc_trans (
+ cctransid int unsigned not null auto_increment,
+ cartid int unsigned not null,
+
+ gctaskref varchar(255),
+ dispatchtime int unsigned,
+ jobstate varchar(255),
+ joberr varchar(255),
+
+ response char(1),
+ responsetext varchar(255),
+ authcode varchar(255),
+ transactionid varchar(255),
+ avsresponse char(1),
+ cvvresponse char(1),
+ responsecode mediumint unsigned,
+
+ ccnumhash varchar(32) not null,
+ expmon tinyint not null,
+ expyear smallint not null,
+ firstname varchar(25) not null,
+ lastname varchar(25) not null,
+ street1 varchar(100) not null,
+ street2 varchar(100),
+ city varchar(40) not null,
+ state varchar(40) not null,
+ country char(2) not null,
+ zip varchar(20) not null,
+ phone varchar(40),
+ ipaddr varchar(15) not null,
+
+ primary key (cctransid),
+ index (cartid)
+)
+EOC
+
+# same as the above
+register_tablecreate('cc_log', <<'EOC');
+CREATE TABLE cc_log (
+ cartid int unsigned not null,
+ ip varchar(15),
+ transtime int unsigned not null,
+ req_content text not null,
+ res_content text not null,
+
+ index (cartid)
+)
+EOC
+
+
# NOTE: new table declarations go ABOVE here ;)
diff -r edaf4b8bc572 -r e0b4a1757c52 bin/worker/shop-creditcard-charge
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/bin/worker/shop-creditcard-charge Tue Apr 06 03:06:20 2010 +0000
@@ -0,0 +1,74 @@
+#!/usr/bin/perl
+#
+# bin/worker/shop-creditcard-charge
+#
+# This Gearman worker handles attempting to charge a user for their credit card
+# transaction. Goal is to be quick and useful.
+#
+# Authors:
+# Mark Smith <mark@dreamwidth.org>
+#
+# Copyright (c) 2010 by Dreamwidth Studios, LLC.
+#
+# This file is not licensed and is part of the private repository of code for
+# Dreamwidth. If you somehow get this file and you aren't DW staff, please
+# let us know. Thanks.
+#
+
+use strict;
+use lib "$ENV{LJHOME}/cgi-bin";
+require "ljlib.pl";
+
+use Gearman::Worker;
+use Storable qw/ thaw /;
+
+use LJ::Worker::Gearman;
+use DW::Pay;
+use DW::Shop;
+
+gearman_decl( 'dw_creditcard_charge' => \&worker );
+gearman_work();
+
+sub worker {
+ my $job = $_[0];
+
+ my %in = %{ thaw( $job->arg ) || {} };
+ return
+ unless exists $in{cctransid} && $in{cctransid} > 0 &&
+ exists $in{cartid} && $in{cartid} > 0;
+
+ my $dbh = DW::Pay::get_db_writer()
+ or return undef;
+
+ # at this point, we can save the error since we have a cctransid
+ my $err = sub {
+ $dbh->do( 'UPDATE cc_trans SET jobstate = ?, joberr = ?, gctaskref = NULL WHERE cctransid = ?',
+ undef, 'internal_failure', sprintf( shift(), @_ ), $in{cctransid} );
+ return undef;
+ };
+
+ my $cart = DW::Shop::Cart->get_from_cartid( $in{cartid} )
+ or return $err->( 'Failed to get cart %d.', $in{cartid} );
+ return $err->( 'Cart is not in valid state PEND_PAID, is in state %d.', $cart->state )
+ unless $cart->state == $DW::Shop::STATE_PEND_PAID;
+
+ # now attempt to charge the user for this purchase
+ my ( $res, $msg ) = $cart->engine->try_capture( %in );
+
+ # update to say that this has been paid, if it has
+ if ( $res ) {
+ $cart->state( $DW::Shop::STATE_PAID );
+ $msg = 'charged' . ( $msg ? " [$msg]" : '' );
+ $dbh->do( 'UPDATE cc_trans SET jobstate = ?, joberr = ?, gctaskref = NULL WHERE cctransid = ?',
+ undef, 'paid', $msg, $in{cctransid} );
+
+ } else {
+ $cart->state( $DW::Shop::STATE_OPEN );
+ $msg = 'declined' . ( $msg ? " [$msg]" : '' );
+ $dbh->do( 'UPDATE cc_trans SET jobstate = ?, joberr = ?, gctaskref = NULL WHERE cctransid = ?',
+ undef, 'failed', $msg, $in{cctransid} );
+ }
+
+ return 1;
+}
+
diff -r edaf4b8bc572 -r e0b4a1757c52 cgi-bin/DW/Controller/Shop.pm
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/cgi-bin/DW/Controller/Shop.pm Tue Apr 06 03:06:20 2010 +0000
@@ -0,0 +1,126 @@
+#!/usr/bin/perl
+#
+# DW::Controller::Shop
+#
+# This controller is for shop handlers.
+#
+# Authors:
+# Mark Smith <mark@dreamwidth.org>
+#
+# Copyright (c) 2010 by Dreamwidth Studios, LLC.
+#
+# This program is free software; you may redistribute it and/or modify it under
+# the same terms as Perl itself. For a copy of the license, please reference
+# 'perldoc perlartistic' or 'perldoc perlgpl'.
+#
+
+package DW::Controller::Shop;
+
+use strict;
+use warnings;
+use DW::Controller;
+use DW::Routing;
+use DW::Template;
+use DW::Shop;
+use JSON;
+
+# routing directions
+DW::Routing->register_string( '/shop', \&shop_index_handler, app => 1 );
+DW::Routing->register_string( '/shop/points', \&shop_points_handler, app => 1 );
+
+# our basic shop controller, this does setup that is unique to all shop
+# pages and everybody should call this first. returns the same tuple as
+# the controller method.
+sub _shop_controller {
+ my %args = ( @_ );
+ my $r = DW::Request->get;
+
+ # if payments are disabled, do nothing
+ unless ( LJ::is_enabled( 'payments' ) ) {
+ $r->redirect( "$LJ::SITEROOT/" );
+ return ( 0, 'The shop is currently disabled.' );
+ }
+
+ # if they're banned ...
+ my $err = DW::Shop->remote_sysban_check;
+ return ( 0, $err ) if $err;
+
+ # basic controller setup
+ my ( $ok, $rv ) = controller( %args );
+ return ( $ok, $rv ) unless $ok;
+
+ # the entire shop uses these files
+ LJ::need_res( 'stc/shop.css' );
+ LJ::set_active_resource_group( 'jquery' );
+
+ # figure out what shop/cart to use
+ $rv->{shop} = DW::Shop->get;
+ $rv->{cart} = $r->get_args->{newcart} ? DW::Shop::Cart->new_cart( $rv->{u} ) : $rv->{shop}->cart;
+
+ # populate vars with cart display template
+ $rv->{cart_display} = DW::Template->template_string( 'shop/cartdisplay.tt', $rv );
+ return ( 1, $rv );
+}
+
+# handles the shop index page
+sub shop_index_handler {
+ my ( $ok, $rv ) = _shop_controller( anonymous => 1 );
+ return $rv unless $ok;
+
+ return DW::Template->render_template( 'shop/index.tt', $rv );
+}
+
+# handles the shop buy points page
+sub shop_points_handler {
+ my ( $ok, $rv ) = _shop_controller();
+ return $rv unless $ok;
+
+ my %errs;
+ $rv->{errs} = \%errs;
+
+ my $r = DW::Request->get;
+ if ( LJ::did_post() ) {
+ my $args = $r->post_args;
+
+ # error check the user
+ my $u = LJ::load_user( $args->{foruser} )
+ or $errs{foruser} = 'Invalid account.';
+ if ( $u ) {
+ if ( $u->is_visible && $u->is_person ) {
+ $rv->{foru} = $u;
+ } else {
+ $errs{foruser} = 'Account must be active and a personal account.';
+ }
+ }
+
+ # error check the points
+ my $points = $args->{points} + 0;
+ $errs{points} = 'Points must be in range 30 to 5,000.'
+ unless $points >= 30 && $points <= 5000;
+ $rv->{points} = $points;
+
+ # looks good, add it!
+ unless ( keys %errs ) {
+ $rv->{cart}->add_item(
+ DW::Shop::Item::Points->new( target_userid => $u->id, from_userid => $rv->{remote}->id, points => $points )
+ );
+
+ return $r->redirect( "$LJ::SITEROOT/shop" );
+ }
+
+ } else {
+ my $for = $r->get_args->{for};
+
+ if ( ! $for || $for eq 'self' ) {
+ $rv->{foru} = $rv->{remote};
+ } elsif ( $for ) {
+ my $fu = LJ::load_user( $for );
+ $rv->{foru} = $fu if $fu;
+ }
+ }
+
+ return DW::Template->render_template( 'shop/points.tt', $rv );
+}
+
+
+1;
diff -r edaf4b8bc572 -r e0b4a1757c52 cgi-bin/DW/Hooks/HolidayPromotion.pm
--- a/cgi-bin/DW/Hooks/HolidayPromotion.pm Mon Apr 05 19:14:58 2010 -0700
+++ b/cgi-bin/DW/Hooks/HolidayPromotion.pm Tue Apr 06 03:06:20 2010 +0000
@@ -55,7 +55,8 @@ LJ::Hooks::register_hook( 'shop_cart_add
# looks good, build a new object and stick it on the cart
my $new = bless {
- cost => 0.00,
+ cost_cash => 0.00,
+ cost_points => 0,
months => int( $item->months / 6 ) * 2,
class => $type,
target_userid => $cart->userid,
diff -r edaf4b8bc572 -r e0b4a1757c52 cgi-bin/DW/Request.pm
--- a/cgi-bin/DW/Request.pm Mon Apr 05 19:14:58 2010 -0700
+++ b/cgi-bin/DW/Request.pm Tue Apr 06 03:06:20 2010 +0000
@@ -195,6 +195,10 @@ Return the response as a string.
Spawn off an external program.
+=head2 C<< $r->redirect( $url ) >>
+
+Redirect to a different URL.
+
=head1 AUTHORS
=over
diff -r edaf4b8bc572 -r e0b4a1757c52 cgi-bin/DW/Request/Apache2.pm
--- a/cgi-bin/DW/Request/Apache2.pm Mon Apr 05 19:14:58 2010 -0700
+++ b/cgi-bin/DW/Request/Apache2.pm Tue Apr 06 03:06:20 2010 +0000
@@ -236,6 +236,13 @@ sub call_bml {
return Apache::BML::handler($self->{r});
}
+# simply sets the location header and returns REDIRECT
+sub redirect {
+ my $self = $_[0];
+ $self->header_out( Location => $_[1] );
+ return $self->REDIRECT;
+}
+
# constants
sub OK {
my DW::Request::Apache2 $self = $_[0];
diff -r edaf4b8bc572 -r e0b4a1757c52 cgi-bin/DW/Shop.pm
--- a/cgi-bin/DW/Shop.pm Mon Apr 05 19:14:58 2010 -0700
+++ b/cgi-bin/DW/Shop.pm Tue Apr 06 03:06:20 2010 +0000
@@ -8,7 +8,7 @@
# Authors:
# Mark Smith <mark@dreamwidth.org>
#
-# Copyright (c) 2009 by Dreamwidth Studios, LLC.
+# Copyright (c) 2009-2010 by Dreamwidth Studios, LLC.
#
# This program is free software; you may redistribute it and/or modify it under
# the same terms as Perl itself. For a copy of the license, please reference
@@ -23,6 +23,12 @@ use DW::Shop::Cart;
use DW::Shop::Cart;
use DW::Shop::Engine;
use DW::Shop::Item::Account;
+use DW::Shop::Item::Points;
+
+# constants across the site
+our $MIN_ORDER_COST = 3.00; # cost in USD minimum. this only comes into affect if
+ # a user is trying to check out an order that costs
+ # less than this.
# variables we maintain
our $STATE_OPEN = 1; # open carts - user can still modify
@@ -89,6 +95,10 @@ our %PAYMENTMETHODS = (
id => 4,
class => 'GoogleCheckout',
},
+ creditcard => {
+ id => 5,
+ class => 'CreditCard',
+ },
);
diff -r edaf4b8bc572 -r e0b4a1757c52 cgi-bin/DW/Shop/Cart.pm
--- a/cgi-bin/DW/Shop/Cart.pm Mon Apr 05 19:14:58 2010 -0700
+++ b/cgi-bin/DW/Shop/Cart.pm Tue Apr 06 03:06:20 2010 +0000
@@ -9,7 +9,7 @@
# Mark Smith <mark@dreamwidth.org>
# Janine Smith <janine@netrophic.com>
#
-# Copyright (c) 2009 by Dreamwidth Studios, LLC.
+# Copyright (c) 2009-2010 by Dreamwidth Studios, LLC.
#
# This program is free software; you may redistribute it and/or modify it under
# the same terms as Perl itself. For a copy of the license, please reference
@@ -164,7 +164,8 @@ sub new_cart {
ip => BML::get_remote_ip(),
state => $DW::Shop::STATE_OPEN,
items => [],
- total => 0.00,
+ total_cash => 0.00,
+ total_points => 0,
nextscan => 0,
authcode => LJ::make_auth_code( 20 ),
paymentmethod => 0, # we don't have a payment method yet
@@ -240,6 +241,14 @@ sub save {
}
+# returns an engine for this cart
+sub engine {
+ my $self = $_[0];
+
+ return $self->{_engine} ||= DW::Shop::Engine->get( $self->paymentmethod => $self );
+}
+
+
# returns the number of items in this cart
sub num_items {
my $self = $_[0];
@@ -259,6 +268,10 @@ sub has_items {
# add an item to the shopping cart, returns 1/0
sub add_item {
my ( $self, $item ) = @_;
+ return unless $self && $item;
+
+ die "Attempted to alter cart not in OPEN state.\n"
+ unless $self->state == $DW::Shop::STATE_OPEN;
# tell the item who we are
$item->cartid( $self->id );
@@ -284,7 +297,7 @@ sub add_item {
# looks good, so let's add it...
push @{$self->items}, $item;
- $self->{total} += $item->cost;
+ $self->recalculate_costs;
# now call out to the hook system in case anybody wants to munge with us
LJ::Hooks::run_hooks( 'shop_cart_added_item', $self, $item );
@@ -298,6 +311,10 @@ sub add_item {
# removes an item from this cart by id
sub remove_item {
my ( $self, $id, %opts ) = @_;
+ return unless $self && $id;
+
+ die "Attempted to alter cart not in OPEN state.\n"
+ unless $self->state == $DW::Shop::STATE_OPEN;
my ( $removed, $out ) = ( undef, [] );
foreach my $it ( @{$self->items} ) {
@@ -309,19 +326,94 @@ sub remove_item {
}
# advise that we removed an item from the cart
+ die "Attempted to remove two items in one pass with id $id.\n"
+ if defined $removed;
$removed = $it;
- $self->{total} -= $it->cost;
} else {
push @$out, $it;
}
}
$self->{items} = $out;
+
+ # now recalculate the costs and save
+ $self->recalculate_costs;
$self->save;
# now run the hook, this is later so that we've updated the cart already
LJ::Hooks::run_hooks( 'shop_cart_removed_item', $self, $removed );
return 1;
+}
+
+
+sub recalculate_costs {
+ my $self = $_[0];
+
+ # if we're not in the OPEN state, do not recalculate. the prices are fixed.
+ return unless $self->state == $DW::Shop::STATE_OPEN;
+
+ my ( $has_points, $max_points ) = ( 0, 0 );
+ if ( $self->userid ) {
+ my $u = LJ::load_userid( $self->userid );
+ $has_points = $u->shop_points;
+ }
+
+ # we have to determine the total cost of the order first so we can do the
+ # minimum order size calculations later
+ ( $self->{total_points}, $self->{total_cash} ) = ( 0, 0.00 );
+ foreach my $item ( @{$self->items} ) {
+ $self->{total_cash} += $item->paid_cash( $item->cost_cash );
+ $item->paid_points( 0 );
+ $max_points += $item->cost_points;
+ }
+
+ # if the user has no points, we're done
+ return unless $has_points;
+
+ # now, if we're short on points, the maximum we can use is based on the
+ # minimum cash order size
+ if ( $has_points < $max_points ) {
+ # x10 to convert from USD to points
+ my $cutoff = $max_points - ( $DW::Shop::MIN_ORDER_COST * 10 );
+
+ # now we effectively constrain the ceiling of how many points the user
+ # has to the point that makes the cash equivalent $3.00
+ $has_points = $cutoff
+ if $has_points > $cutoff;
+ }
+
+ # second loop has to iterate and actually adjust the point/cash balances
+ foreach my $item ( @{$self->items} ) {
+ # in some cases, we have items that cost no points, those items
+ # we can just ignore and skip
+ next unless $item->cost_points;
+
+ # start deducting items from points until one goes negative
+ $has_points -= $item->cost_points;
+
+ # if positive, the item was paid for by points entirely
+ if ( $has_points >= 0 ) {
+ $item->paid_cash( 0.00 );
+ $item->paid_points( $item->cost_points );
+
+ $self->{total_cash} -= $item->cost_cash;
+ $self->{total_points} += $item->cost_points;
+
+ # and last if we're at 0 points left
+ last if $has_points == 0;
+
+ } else {
+ my $cash = -$has_points;
+ $item->paid_cash( $cash / 10 );
+ $item->paid_points( $item->cost_points - $cash );
+
+ $self->{total_cash} -= $item->cost_cash - $item->paid_cash;
+ $self->{total_points} += $item->paid_points;
+
+ # and this means we're done
+ last;
+ }
+ }
}
@@ -378,6 +470,7 @@ sub email {
return $self->{email};
}
+
################################################################################
## read-only accessor methods
################################################################################
@@ -392,13 +485,33 @@ sub uniq { $_[0]->{uniq}
sub uniq { $_[0]->{uniq} }
sub nextscan { $_[0]->{nextscan} }
sub authcode { $_[0]->{authcode} }
-sub total { $_[0]->{total}+0.00 }
+sub total_points { $_[0]->{total_points}+0 }
+sub ordernum { $_[0]->{cartid} . '-' . $_[0]->{authcode} }
+
+# this has to work for both old items (pre-points) and new ones
+sub total_cash {
+ my $self = $_[0];
+ return $self->{total} + 0.00 if exists $self->{total};
+ return $self->{total_cash} + 0.00;
+}
# returns the total in a displayed format
-sub display_total { sprintf( '%0.2f', $_[0]->total ) }
+sub display_total {
+ my $self = $_[0];
+ if ( $self->total_cash && $self->total_points ) {
+ return sprintf( '$%0.2f USD and %d points', $self->total_cash, $self->total_points );
+ } elsif ( $self->total_cash ) {
+ return sprintf( '$%0.2f USD', $self->total_cash );
+ } elsif ( $self->total_points ) {
+ return sprintf( '%d points', $self->total_points );
+ } else {
+ return 'free';
+ }
+}
-# and our order number
-sub ordernum { $_[0]->{cartid} . '-' . $_[0]->{authcode} }
+sub display_total_cash { sprintf( '$%0.2f USD', $_[0]->total_cash ) }
+sub display_total_points { sprintf( '%d points', $_[0]->total_points ) }
+
################################################################################
diff -r edaf4b8bc572 -r e0b4a1757c52 cgi-bin/DW/Shop/Engine.pm
--- a/cgi-bin/DW/Shop/Engine.pm Mon Apr 05 19:14:58 2010 -0700
+++ b/cgi-bin/DW/Shop/Engine.pm Tue Apr 06 03:06:20 2010 +0000
@@ -7,7 +7,7 @@
# Authors:
# Mark Smith <mark@dreamwidth.org>
#
-# Copyright (c) 2009 by Dreamwidth Studios, LLC.
+# Copyright (c) 2009-2010 by Dreamwidth Studios, LLC.
#
# This program is free software; you may redistribute it and/or modify it under
# the same terms as Perl itself. For a copy of the license, please reference
@@ -21,6 +21,7 @@ use DW::Shop::Engine::CreditCardPP;
use DW::Shop::Engine::CreditCardPP;
use DW::Shop::Engine::PayPal;
use DW::Shop::Engine::GoogleCheckout;
+use DW::Shop::Engine::CreditCard;
# get( $method, $cart )
#
@@ -30,6 +31,7 @@ sub get {
return DW::Shop::Engine::GoogleCheckout->new( $_[2] ) if $_[1] eq 'gco';
return DW::Shop::Engine::CreditCardPP->new( $_[2] ) if $_[1] eq 'creditcardpp';
return DW::Shop::Engine::CheckMoneyOrder->new( $_[2] ) if $_[1] eq 'checkmoneyorder';
+ return DW::Shop::Engine::CreditCard->new( $_[2] ) if $_[1] eq 'creditcard';
warn "Payment method '$_[1]' not supported.\n";
return undef;
@@ -94,4 +96,44 @@ sub fail_transaction {
}
+# called when someone wants us to try to capture the points
+# FIXME: should move the 'cart' accessor and logic up to this base class ...
+sub try_capture_points {
+ my $self = $_[0];
+
+ # if the order costs no points, we're done and successful
+ return 1 unless $self->cart->total_points > 0;
+
+ # else, we need to try to capture them
+ my $u = LJ::load_userid( $self->cart->userid )
+ or die "Failed to load user to deduct points from.\n";
+ $u->give_shop_points( amount => -$self->cart->total_points,
+ reason => sprintf( 'order %d confirmed', $self->cart->id ) )
+ or die "Failed to deduct points from account.\n";
+
+ # we're a happy clam
+ return 1;
+}
+
+
+# called to give back the points that we took from the user in case another
+# part of the transaction has failed
+sub refund_captured_points {
+ my $self = $_[0];
+
+ # if the order costs no points, we're done and successful
+ return 1 unless $self->cart->total_points > 0;
+
+ # else, we need to try to capture them
+ my $u = LJ::load_userid( $self->cart->userid )
+ or die "Failed to load user to restore points to; contact site administrators.\n";
+ $u->give_shop_points( amount => $self->cart->total_points,
+ reason => sprintf( 'order %d failed', $self->cart->id ) )
+ or die "Failed to add points to account.\n";
+
+ # we're a happy clam
+ return 1;
+}
+
+
1;
diff -r edaf4b8bc572 -r e0b4a1757c52 cgi-bin/DW/Shop/Engine/CheckMoneyOrder.pm
--- a/cgi-bin/DW/Shop/Engine/CheckMoneyOrder.pm Mon Apr 05 19:14:58 2010 -0700
+++ b/cgi-bin/DW/Shop/Engine/CheckMoneyOrder.pm Tue Apr 06 03:06:20 2010 +0000
@@ -51,8 +51,8 @@ sub checkout_url {
# make sure that the cart contains something that costs something. since
# this check should have been done above, we die hardcore here.
my $cart = $self->cart;
- die "Constraints not met: cart && cart->has_items && cart->has_total > 0.00.\n"
- unless $cart && $cart->has_items && $cart->total > 0.00;
+ die "Constraints not met: cart && cart->has_items && ( cart->total_cash > 0.00 || cart->total_points > 0 ).\n"
+ unless $cart && $cart->has_items && ( $cart->total_cash > 0.00 || $cart->total_points > 0 );
# and, just in case something terrible happens, make sure our state is good
die "Cart not in valid state!\n"
@@ -76,6 +76,17 @@ sub confirm_order {
# or something, we can't touch it.
return $self->error( 'cmo.engbadstate' )
unless $cart->state == $DW::Shop::STATE_CHECKOUT;
+
+ # and now, if this order is free (paid on points) then try to deduct the points
+ # from the user and if that works, mark it paid
+ if ( $cart->total_cash == 0.00 && $cart->total_points > 0 ) {
+ $self->try_capture_points
+ or die "Unknown error capturing points for sale.\n";
+
+ # if the above succeeded the order is paid and done
+ $cart->state( $DW::Shop::STATE_PAID );
+ return 1;
+ }
# now set it pending
$cart->state( $DW::Shop::STATE_PEND_PAID );
@@ -93,7 +104,7 @@ sub confirm_order {
body => LJ::Lang::ml( 'shop.email.confirm.checkmoneyorder.body', {
touser => LJ::isu( $u ) ? $u->display_name : $cart->email,
receipturl => "$LJ::SITEROOT/shop/receipt?ordernum=" . $cart->ordernum,
- total => '$' . $cart->total . ' USD',
+ total => '$' . $cart->total_cash . ' USD',
payableto => $LJ::SITECOMPANY,
address => "$LJ::SITECOMPANY${linebreak}Order #" . $cart->id . "$linebreak$address",
sitename => $LJ::SITENAME,
diff -r edaf4b8bc572 -r e0b4a1757c52 cgi-bin/DW/Shop/Engine/CreditCard.pm
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/cgi-bin/DW/Shop/Engine/CreditCard.pm Tue Apr 06 03:06:20 2010 +0000
@@ -0,0 +1,183 @@
+#!/usr/bin/perl
+#
+# DW::Shop::Engine::CreditCard
+#
+# Interfaces to our credit card processing service.
+#
+# Authors:
+# Mark Smith <mark@dreamwidth.org>
+#
+# Copyright (c) 2010 by Dreamwidth Studios, LLC.
+#
+# This program is free software; you may redistribute it and/or modify it under
+# the same terms as Perl itself. For a copy of the license, please reference
+# 'perldoc perlartistic' or 'perldoc perlgpl'.
+#
+
+package DW::Shop::Engine::CreditCard;
+
+use strict;
+use Carp qw/ croak confess /;
+use Digest::MD5 qw/ md5_hex /;
+use Storable qw/ nfreeze thaw /;
+
+use base qw/ DW::Shop::Engine /;
+
+
+# new( $cart )
+#
+# instantiates a new engine for the given cart
+sub new {
+ return bless { cart => $_[1] }, $_[0];
+}
+
+
+# checkout_url()
+#
+# this is simple, send them to the page for entering their credit card information
+sub checkout_url {
+ my $self = $_[0];
+
+ # make sure that the cart contains something that costs something. since
+ # this check should have been done above, we die hardcore here.
+ my $cart = $self->cart;
+ die "Constraints not met: cart && cart->has_items && cart->total_cash > 0.00.\n"
+ unless $cart && $cart->has_items && $cart->total_cash > 0.00;
+
+ # and, just in case something terrible happens, make sure our state is good
+ die "Cart not in valid state!\n"
+ unless $cart->state == $DW::Shop::STATE_OPEN;
+
+ # we absolutely must be using SSL on the site...
+ die "This site is not configured to use SSL, we refuse to allow credit cards.\n"
+ unless $LJ::IS_DEV_SERVER || $LJ::USE_SSL;
+
+ # return URL to cc entry
+ return ( $LJ::IS_DEV_SERVER ? $LJ::SITEROOT : $LJ::SSLROOT ) . '/shop/entercc';
+}
+
+
+# setup_transaction( ...many options... )
+#
+# sets up a transaction row in the database, also dispatches the gearman task to
+# actually attempt to charge the user.
+#
+# THIS MUST NOT SAVE THE CREDIT CARD NUMBER TO ANY DATABASE.
+#
+sub setup_transaction {
+ my ( $self, %in ) = @_;
+
+ # insert the data we can in the db
+ my $dbh = DW::Pay::get_db_writer()
+ or die "Unable to get database handle.\n";
+ $dbh->do(
+ q{INSERT INTO cc_trans (cctransid, cartid, firstname, lastname,
+ street1, street2, city, state, country, zip, phone, ipaddr, expmon, expyear, ccnumhash)
+ VALUES (NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)},
+ undef, ( map { $in{$_} } qw/ cartid firstname lastname street1 street2 city state country zip phone ip expmon expyear / ),
+ md5_hex( $in{ccnum} . $in{cvv2} )
+ );
+ die "Database error: " . $dbh->errstr . "\n"
+ if $dbh->err;
+
+ # now we have a basic row
+ $in{cctransid} = $dbh->{mysql_insertid};
+
+ # dispatch this item...
+ my $gc = LJ::gearman_client()
+ or die "Unable to get gearman client.\n";
+ my $ref = $gc->dispatch_background( 'dw_creditcard_charge', nfreeze( \%in ), { uniq => $in{cctransid} } );
+ die "Unable to insert Gearman job.\n"
+ unless $ref;
+
+ # update our row from above so we have it later
+ $dbh->do( 'UPDATE cc_trans SET gctaskref = ?, dispatchtime = UNIX_TIMESTAMP(), jobstate = ? WHERE cctransid = ?',
+ undef, $ref, 'queued', $in{cctransid} );
+ return $in{cctransid};
+}
+
+
+# get_transaction( cctransid )
+#
+# returns a transaction row for a given id.
+sub get_transaction {
+ my ( $self, $cctransid ) = @_;
+
+ my $dbh = DW::Pay::get_db_writer()
+ or die "Unable to get database handle.\n";
+
+ # FIXME: "SELECT *" is for sad making :(
+ my $row = $dbh->selectrow_hashref( 'SELECT * FROM cc_trans WHERE cctransid = ?', undef, $cctransid );
+ die "Database error: " . $dbh->errstr . "\n"
+ if $dbh->err;
+
+ # if this task has no gctaskref it's already finished or never got one, so
+ # just return the row as is
+ return $row unless $row->{gctaskref};
+
+ # now, if it's queued, try to get some state on it
+ my $gc = LJ::gearman_client()
+ or return $row;
+ my $js = $gc->get_status( $row->{gctaskref} );
+
+ # if the job is known to the server, that means it's in the queue somewhere, so we are
+ # okay to just return whatever state the row has (which should be 'queued')
+ return $row if $js && $js->known;
+
+ # now, if the job is not known, and we are still 'queued', something terrible happened
+ # like the worker crashed or the gearman server crashed
+ if ( $row->{jobstate} eq 'queued' ) {
+ $row->{jobstate} = 'internal_failure';
+ $row->{joberr} = 'Task no longer known to Gearman.';
+ $dbh->do( 'UPDATE cc_trans SET jobstate = ?, joberr = ?, gctaskref = NULL WHERE cctransid = ?',
+ undef, $row->{jobstate}, $row->{joberr}, $cctransid );
+ die $dbh->errstr if $dbh->err;
+ }
+
+ return $row;
+}
+
+# try_capture( ...many values... )
+#
+# given an input of some values, try to capture funds from the processor. this
+# uses a hook so that local sites can implement their own payment processing
+# logic...
+#
+# note that it is important that you don't actually save the credit card number
+# anywhere on your servers unless you are doing PCI compliance.
+#
+# to repeat: DO NOT SAVE CREDIT CARD NUMBERS TO DISK. well, at least not in
+# the US. if you're in another country, your own rules apply.
+#
+sub try_capture {
+ my ( $self, %in ) = @_;
+
+ die "Unable to capture funds: no credit card processor loaded.\n"
+ unless LJ::Hooks::are_hooks( 'creditcard_try_capture' );
+
+ # first capture the points if we have any to do
+ return ( 0, 'Failed to capture points to complete order.' )
+ unless $self->try_capture_points;
+
+ # this hook is supposed to try to capture the funds. return value is a
+ # list: ( $code, $message ). code is one of 0 (declined), 1 (success).
+ # message is optional and if saved will be recorded as the status message.
+ my ( $res, $msg ) = LJ::Hooks::run_hook( creditcard_try_capture => ( $self, \%in ) );
+
+ # if the capture failed, refund the points
+ $self->refund_captured_points
+ unless $res;
+
+ # the person who called us is responsible for setting up cart and engine
+ # status based on the results. improvement: maybe move all that logic
+ # up to here so the workers are really small?
+ return ( $res, $msg );
+}
+
+
+# accessors
+sub cart { $_[0]->{cart} }
+sub cctransid { $_[0]->cart->{cctransid} }
+
+
+1;
diff -r edaf4b8bc572 -r e0b4a1757c52 cgi-bin/DW/Shop/Engine/CreditCardPP.pm
--- a/cgi-bin/DW/Shop/Engine/CreditCardPP.pm Mon Apr 05 19:14:58 2010 -0700
+++ b/cgi-bin/DW/Shop/Engine/CreditCardPP.pm Tue Apr 06 03:06:20 2010 +0000
@@ -42,8 +42,8 @@ sub checkout_url {
# make sure that the cart contains something that costs something. since
# this check should have been done above, we die hardcore here.
my $cart = $self->cart;
- die "Constraints not met: cart && cart->has_items && cart->has_total > 0.00.\n"
- unless $cart && $cart->has_items && $cart->total > 0.00;
+ die "Constraints not met: cart && cart->has_items && cart->total_cash > 0.00.\n"
+ unless $cart && $cart->has_items && $cart->total_cash > 0.00;
# and, just in case something terrible happens, make sure our state is good
die "Cart not in valid state!\n"
diff -r edaf4b8bc572 -r e0b4a1757c52 cgi-bin/DW/Shop/Engine/GoogleCheckout.pm
--- a/cgi-bin/DW/Shop/Engine/GoogleCheckout.pm Mon Apr 05 19:14:58 2010 -0700
+++ b/cgi-bin/DW/Shop/Engine/GoogleCheckout.pm Tue Apr 06 03:06:20 2010 +0000
@@ -116,8 +116,8 @@ sub checkout_url {
# make sure that the cart contains something that costs something. since
# this check should have been done above, we die hardcore here.
my $cart = $self->cart;
- die "Constraints not met: cart && cart->has_items && cart->has_total > 0.00.\n"
- unless $cart && $cart->has_items && $cart->total > 0.00;
+ die "Constraints not met: cart && cart->has_items && cart->total_cash > 0.00.\n"
+ unless $cart && $cart->has_items && $cart->total_cash > 0.00;
# and, just in case something terrible happens, make sure our state is good
die "Cart not in valid state!\n"
@@ -142,7 +142,7 @@ sub checkout_url {
my $gitem = Google::Checkout::General::DigitalContent->new(
name => $item->class_name,
description => $item->short_desc,
- price => $item->cost,
+ price => $item->cost_cash,
quantity => 1,
private => $item->id,
delivery_method => $DW::Shop::Engine::GoogleCheckout::EMAIL_DELIVERY,
@@ -180,7 +180,7 @@ sub charge_order {
my $charge_order = Google::Checkout::Command::ChargeOrder->new(
order_number => $self->gcoid,
- amount => $self->cart->display_total,
+ amount => $self->cart->total_cash,
);
my $response = $self->gco->command( $charge_order );
@@ -222,7 +222,7 @@ sub process_notification {
unless $cart &&
$cart->state == $DW::Shop::STATE_OPEN &&
$cart->paymentmethod eq 'gco' &&
- $cart->display_total == $form->{'order-total'};
+ $cart->total_cash == $form->{'order-total'};
$dbh->do(
q{INSERT INTO gco_map (gcoid, cartid, email, contactname)
@@ -247,7 +247,7 @@ sub process_notification {
return 1
unless $eng->cart->state == $DW::Shop::STATE_PEND_PAID &&
$eng->cart->paymentmethod eq 'gco' &&
- $eng->cart->display_total == $form->{'total-charge-amount'};
+ $eng->cart->total_cash == $form->{'total-charge-amount'};
# looks good, mark it paid so it gets processed
$eng->cart->state( $DW::Shop::STATE_PAID );
diff -r edaf4b8bc572 -r e0b4a1757c52 cgi-bin/DW/Shop/Engine/PayPal.pm
--- a/cgi-bin/DW/Shop/Engine/PayPal.pm Mon Apr 05 19:14:58 2010 -0700
+++ b/cgi-bin/DW/Shop/Engine/PayPal.pm Tue Apr 06 03:06:20 2010 +0000
@@ -98,8 +98,8 @@ sub checkout_url {
# make sure that the cart contains something that costs something. since
# this check should have been done above, we die hardcore here.
my $cart = $self->cart;
- die "Constraints not met: cart && cart->has_items && cart->has_total > 0.00.\n"
- unless $cart && $cart->has_items && $cart->total > 0.00;
+ die "Constraints not met: cart && cart->has_items && cart->total_cash > 0.00.\n"
+ unless $cart && $cart->has_items && $cart->total_cash > 0.00;
# and, just in case something terrible happens, make sure our state is good
die "Cart not in valid state!\n"
@@ -116,8 +116,8 @@ sub checkout_url {
paymentaction => 'Sale',
# how much it costs. no tax or shipping.
- amt => $cart->display_total,
- itemamt => $cart->display_total,
+ amt => $cart->total_cash,
+ itemamt => $cart->total_cash,
taxamt => '0.00',
noshipping => 1,
@@ -129,7 +129,7 @@ sub checkout_url {
returnurl => "$LJ::SITEROOT/shop/confirm",
# custom data we send to reference this cart
- custom => join( ';', ( $cart->ordernum, $cart->display_total ) ),
+ custom => join( ';', ( $cart->ordernum, $cart->total_cash ) ),
);
# now we have to stick in data for each of the items in the cart
@@ -138,7 +138,7 @@ sub checkout_url {
push @req, "L_NAME$cur" => $item->class_name,
"L_NUMBER$cur" => $cart->id . $item->id,
"L_DESC$cur" => $item->short_desc,
- "L_AMT$cur" => $item->cost,
+ "L_AMT$cur" => $item->cost_cash,
"L_QTY$cur" => 1;
$cur++;
}
@@ -209,7 +209,7 @@ sub confirm_order {
'DoExpressCheckoutPayment',
token => $self->token,
payerid => $self->payerid,
- amt => $cart->display_total,
+ amt => $cart->total_cash,
paymentaction => 'Sale',
);
return $self->temp_error(
@@ -408,7 +408,7 @@ sub process_ipn {
unless $cart &&
$cart->state == $DW::Shop::STATE_PEND_PAID &&
$cart->paymentmethod eq 'creditcardpp' &&
- $cart->display_total == $form->{payment_gross};
+ $cart->total_cash == $form->{payment_gross};
# looks good, mark it paid so it gets processed
$cart->state( $DW::Shop::STATE_PAID );
diff -r edaf4b8bc572 -r e0b4a1757c52 cgi-bin/DW/Shop/Item.pm
--- a/cgi-bin/DW/Shop/Item.pm Mon Apr 05 19:14:58 2010 -0700
+++ b/cgi-bin/DW/Shop/Item.pm Tue Apr 06 03:06:20 2010 +0000
@@ -98,7 +98,8 @@ sub new {
# looks good
return bless {
# user supplied arguments (close enough)
- cost => $LJ::SHOP{$args{type}}->[0] + 0.00,
+ cost_cash => $LJ::SHOP{$args{type}}->[0] + 0.00,
+ cost_points => $LJ::SHOP{$args{type}}->[3] + 0,
%args,
# internal things we use to track the state of this item,
@@ -337,15 +338,80 @@ sub from_html {
return $from_u->ljuser_display;
}
-# simple accessors
+=head2 C<< $self->paid_cash >>
+
+Returns the amount paid for this item in USD. This varies from cart to cart
+and item to item and is a reflection of the actual amount of cash paid for
+this item. paid_points may also be non-zero.
+
+=cut
+
+# this method has to be aware of old items
+sub paid_cash {
+ my $self = $_[0];
+
+ # we try to promote the item to a new style. we don't know if this is
+ # going to get saved in the cart or not ...
+ if ( exists $self->{cost} ) {
+ $self->{paid_cash} = delete( $self->{cost} ) + 0.00;
+ $self->{paid_points} = 0;
+ }
+
+ return $_[0]->{paid_cash} unless defined $_[1];
+ return $_[0]->{paid_cash} = $_[1];
+}
+
+=head2 C<< $self->paid_points >>
+
+Returns the amount paid in points for this item. This varies just like the
+paid_cash item, which may also be non-zero for items that a user paid both
+cash and points for.
+
+=cut
+
+sub paid_points {
+ return $_[0]->{paid_points} unless defined $_[1];
+ return $_[0]->{paid_points} = $_[1];
+}
+
+=head2 C<< $self->display_paid >>
+
+Displays how much cash and/or points this item costs right now.
+
+=cut
+
+sub display_paid {
+ my $self = $_[0];
+ if ( $self->paid_cash && $self->paid_points ) {
+ return sprintf( '$%0.2f USD and %d points', $self->paid_cash, $self->paid_points );
+ } elsif ( $self->paid_cash ) {
+ return sprintf( '$%0.2f USD', $self->paid_cash );
+ } elsif ( $self->paid_points ) {
+ return sprintf( '%d points', $self->paid_points );
+ } else {
+ return 'free';
+ }
+}
+
+=head2 C<< $self->display_paid_cash >>
+
+Display how much cash this item costs right now.
+
+=head2 C<< $self->display_paid_points >>
+
+Display how many points this item costs right now.
=head2 C<< $self->applied >>
Returns whether the item which was bought has been already applied
-=head2 C<< $self->cost >>
+=head2 C<< $self->cost_cash >>
-Returns the cost of the item, as configured for this site.
+Returns the cost in USD of the item, as configured for this site.
+
+=head2 C<< $self->cost_points >>
+
+Returns the cost in points of the item, as configured for this site.
=head2 C<< $self->t_email >>
@@ -376,14 +442,23 @@ promotions. Not exposed/settable via the
=cut
+sub display_paid_cash { sprintf( '$%0.2f USD', $_[0]->paid_cash ) }
+sub display_paid_points { sprintf( '%d points', $_[0]->paid_points ) }
sub applied { return $_[0]->{applied}; }
-sub cost { return $_[0]->{cost}; }
+sub cost_points { return $_[0]->{cost_points}; }
sub t_email { return $_[0]->{target_email}; }
sub from_userid { return $_[0]->{from_userid}; }
sub deliverydate { return $_[0]->{deliverydate}; }
sub anonymous { return $_[0]->{anonymous}; }
sub noremove { return $_[0]->{noremove}; }
sub from_name { return $_[0]->{from_name}; }
+
+# this has to work with old items (pre-points) too
+sub cost_cash {
+ my $self = $_[0];
+ return $self->{cost} + 0.00 if exists $self->{cost};
+ return $self->{cost_cash} + 0.00;
+}
=head2 C<< $self->cannot_conflict >>
diff -r edaf4b8bc572 -r e0b4a1757c52 cgi-bin/DW/Shop/Item/Account.pm
--- a/cgi-bin/DW/Shop/Item/Account.pm Mon Apr 05 19:14:58 2010 -0700
+++ b/cgi-bin/DW/Shop/Item/Account.pm Tue Apr 06 03:06:20 2010 +0000
@@ -295,6 +295,10 @@ sub conflicts {
return if
$self->cannot_conflict || $item->cannot_conflict;
+ # we can only conflict with other items of our own type
+ return if
+ ref $self ne ref $item;
+
# first see if we're talking about the same target
# note that we're not checking email here because they may want to buy
# multiple paid accounts and send them to all to the same email address
@@ -343,8 +347,15 @@ sub name_html {
my $self = $_[0];
my $name = $self->class_name;
- return $name if $self->permanent;
- return LJ::Lang::ml( 'shop.item.account.name', { name => $name, num => $self->months } );
+
+ if ( $self->cost_points > 0 ) {
+ return LJ::Lang::ml( 'shop.item.account.name.perm', { name => $name, points => $self->cost_points } )
+ if $self->permanent;
+ return LJ::Lang::ml( 'shop.item.account.name', { name => $name, num => $self->months, points => $self->cost_points } );
+ } else {
+ return $name if $self->permanent;
+ return LJ::Lang::ml( 'shop.item.account.name.nopoints', { name => $name, num => $self->months } );
+ }
}
diff -r edaf4b8bc572 -r e0b4a1757c52 cgi-bin/DW/Shop/Item/Points.pm
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/cgi-bin/DW/Shop/Item/Points.pm Tue Apr 06 03:06:20 2010 +0000
@@ -0,0 +1,184 @@
+#!/usr/bin/perl
+#
+# DW::Shop::Item::Points
+#
+# Represents Dreamwidth Points that someone is buying.
+#
+# Authors:
+# Mark Smith <mark@dreamwidth.org>
+#
+# Copyright (c) 2010 by Dreamwidth Studios, LLC.
+#
+# This program is free software; you may redistribute it and/or modify it under
+# the same terms as Perl itself. For a copy of the license, please reference
+# 'perldoc perlartistic' or 'perldoc perlgpl'.
+#
+
+package DW::Shop::Item::Points;
+
+use base 'DW::Shop::Item';
+
+use strict;
+use DW::InviteCodes;
+use DW::Pay;
+
+=head1 NAME
+
+DW::Shop::Item::Points - Represents a block of points that someone is purchasing. See
+the documentation for DW::Shop::Item for usage examples and description of methods
+inherited from that base class.
+
+=head1 API
+
+=head2 C<< $class->new( [ %args ] ) >>
+
+Instantiates a block of points to be purchased.
+
+Arguments:
+=item ( see DW::Shop::Item ),
+=item points => number of points to buy,
+
+=cut
+
+# override
+sub new {
+ my ( $class, %args ) = @_;
+
+ my $self = $class->SUPER::new( %args, type => 'points' );
+ return unless $self;
+
+ if ( $self ) {
+ $self->{cost_cash} = $self->{points} / 10;
+ $self->{cost_points} = 0;
+ }
+
+ # for now, we can only apply to a user. in the future this is an obvious way
+ # to do gift certificates by allowing an email address here...
+ die "Can only give points to an account.\n"
+ unless $self->t_userid;
+
+ return $self;
+}
+
+
+# override
+sub _apply {
+ my $self = $_[0];
+
+ return $self->_apply_userid if $self->t_userid;
+
+ # something weird, just kill this item!
+ $self->{applied} = 1;
+ return 1;
+}
+
+
+# internal application sub, do not call
+sub _apply_userid {
+ my $self = $_[0];
+ return 1 if $self->applied;
+
+ # will need this later
+ my $fu = LJ::load_userid( $self->from_userid );
+ unless ( $fu ) {
+ warn "Failed to apply: invalid from_userid!\n";
+ return 0;
+ }
+
+ # need this user
+ my $u = LJ::load_userid( $self->t_userid )
+ or return 0;
+
+ # now try to add the points
+ $u->give_shop_points( amount => $self->points, reason => 'ordered; item #' . $self->id );
+
+ # we're applied now, regardless of what happens with the email
+ $self->{applied} = 1;
+
+ # now we have to mail this code
+ my $word = $fu->equals( $u ) ? 'self' : 'other';
+ my $body = LJ::Lang::ml( "shop.email.gift.$word.body",
+ {
+ touser => $u->display_name,
+ fromuser => $fu->display_name,
+ sitename => $LJ::SITENAME,
+ gift => sprintf( '%d %s Points', $self->points, $LJ::SITENAMESHORT ),
+ }
+ );
+ my $subj = LJ::Lang::ml( "shop.email.gift.$word.subject", { sitename => $LJ::SITENAME } );
+
+ # send the email to the user
+ LJ::send_mail( {
+ to => $u->email_raw,
+ from => $LJ::ACCOUNTS_EMAIL,
+ fromname => $LJ::SITENAME,
+ subject => $subj,
+ body => $body
+ } );
+
+ # tell the caller we're happy
+ return 1;
+}
+
+
+# override
+sub unapply {
+ my $self = $_[0];
+ return unless $self->applied;
+
+ # unapplying is not coded yet, as we don't have good automatic support for orders being
+ # reverted and refunded.
+ $self->{applied} = 0;
+ die "Unable to unapply right now.\n";
+
+ return 1;
+}
+
+
+# override
+sub can_be_added {
+ my ( $self, %opts ) = @_;
+
+ my $errref = $opts{errref};
+
+ # if not a valid account, error
+ my $target_u = LJ::load_userid( $self->t_userid );
+ if ( ! LJ::isu( $target_u ) ) {
+ $$errref = LJ::Lang::ml( 'shop.item.points.canbeadded.notauser' );
+ return 0;
+ }
+
+ # the receiving user must be a person for now
+ unless ( $target_u->is_personal && $target_u->is_visible ) {
+ $$errref = LJ::Lang::ml( 'shop.item.points.canbeadded.invalidjournaltype' );
+ return 0;
+ }
+
+ # sanity check that the points are in-range and less than 5000?
+ if ( $self->points < 30 || $self->points > 5000 ) {
+ $$errref = LJ::Lang::ml( 'shop.item.points.canbeadded.outofrange' );
+ return 0;
+ }
+
+ return 1;
+}
+
+
+# override
+sub name_html {
+ my $self = $_[0];
+
+ return LJ::Lang::ml( 'shop.item.points.name', { num => $self->points, sitename => $LJ::SITENAMESHORT } );
+}
+
+
+=head2 C<< $self->points >>
+
+Return how many points this item is worth.
+
+=cut
+
+sub points { $_[0]->{points} }
+
+
+1;
diff -r edaf4b8bc572 -r e0b4a1757c52 cgi-bin/DW/Template.pm
--- a/cgi-bin/DW/Template.pm Mon Apr 05 19:14:58 2010 -0700
+++ b/cgi-bin/DW/Template.pm Tue Apr 06 03:06:20 2010 +0000
@@ -42,6 +42,7 @@ my $site_constants = Template::Namespace
my $roots_constants = Template::Namespace::Constants->new({
site => $LJ::SITEROOT,
+ img => $LJ::IMGPREFIX,
});
# precreating this
@@ -50,6 +51,7 @@ my $view_engine = Template->new({
NAMESPACE => {
site => $site_constants,
roots => $roots_constants,
+ help => Template::Namespace::Constants->new( \%LJ::HELPURL ),
},
CACHE_SIZE => $LJ::TEMPLATE_CACHE_SIZE, # this can be undef, and that means cache everything.
STAT_TTL => $LJ::IS_DEV_SERVER ? 1 : 3600,
@@ -71,15 +73,21 @@ Render a template to a string.
=cut
sub template_string {
- my ($class, $filename, $opts, $extra ) = @_;
+ my ( $class, $filename, $opts, $extra ) = @_;
my $r = DW::Request->get;
$opts->{sections} = $extra;
- $r->note('ml_scope',"/$filename") unless $r->note('ml_scope');
+
+ # now we have to save the scope and update it for this rendering
+ my $oldscope = $r->note( 'ml_scope' );
+ $r->note( ml_scope => ( $extra->{ml_scope} || "/$filename" ) );
my $out;
$view_engine->process( $filename, $opts, \$out )
or die Template->error();
+
+ # now revert the scope if we had one
+ $r->note( ml_scope => $oldscope ) if $oldscope;
return $out;
}
diff -r edaf4b8bc572 -r e0b4a1757c52 cgi-bin/DW/Widget/ShopCartStatusBar.pm
--- a/cgi-bin/DW/Widget/ShopCartStatusBar.pm Mon Apr 05 19:14:58 2010 -0700
+++ b/cgi-bin/DW/Widget/ShopCartStatusBar.pm Tue Apr 06 03:06:20 2010 +0000
@@ -41,7 +41,7 @@ sub render_body {
if ( $cart->has_items ) {
$ret .= "<div class='shop-cart-status'>";
$ret .= "<strong>" . $class->ml( 'widget.shopcartstatusbar.header' ) . "</strong><br />";
- $ret .= $class->ml( 'widget.shopcartstatusbar.itemcount', { num => $cart->num_items, price => '$' . $cart->display_total . " USD" } );
+ $ret .= $class->ml( 'widget.shopcartstatusbar.itemcount', { num => $cart->num_items, price => $cart->display_total } );
$ret .= "<br />";
$ret .= "<ul>";
diff -r edaf4b8bc572 -r e0b4a1757c52 cgi-bin/DW/Widget/ShopItemGroupDisplay.pm
--- a/cgi-bin/DW/Widget/ShopItemGroupDisplay.pm Mon Apr 05 19:14:58 2010 -0700
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,52 +0,0 @@
-#!/usr/bin/perl
-#
-# DW::Widget::ShopItemGroupDisplay
-#
-# Renders a group of shop items for display on the first page of the shop.
-#
-# Authors:
-# Janine Smith <janine@netrophic.com>
-#
-# Copyright (c) 2009 by Dreamwidth Studios, LLC.
-#
-# This program is free software; you may redistribute it and/or modify it under
-# the same terms as Perl itself. For a copy of the license, please reference
-# 'perldoc perlartistic' or 'perldoc perlgpl'.
-#
-
-package DW::Widget::ShopItemGroupDisplay;
-
-use strict;
-use base qw/ LJ::Widget /;
-use Carp qw/ croak /;
-
-sub need_res { qw( stc/shop.css ) }
-
-sub render_body {
- my ( $class, %opts ) = @_;
-
- my $remote = LJ::get_remote();
- my $ret;
-
- if ( $opts{group} eq 'paidaccounts' ) {
- $ret .= "<h2>" . $class->ml( 'widget.shopitemgroupdisplay.paidaccounts.header' ) . "</h2>";
- $ret .= "<ul>";
- if ( $remote && $remote->is_personal && DW::Pay::get_account_type( $remote ) ne 'seed' ) {
- $ret .= "<li>" . $class->ml( 'widget.shopitemgroupdisplay.paidaccounts.item.self', { aopts => "href='$LJ::SITEROOT/shop/account?for=self'", user => $remote->ljuser_display } ) . "</li>";
- }
- if ( $remote && $remote->is_personal ) {
- $ret .= "<li>" . $class->ml( 'widget.shopitemgroupdisplay.paidaccounts.item.circleaccount', { aopts => "href='$LJ::SITEROOT/shop/gifts'" } ) . "</li>";
- $ret .= "<li>" . $class->ml( 'widget.shopitemgroupdisplay.paidaccounts.item.differentaccount', { aopts => "href='$LJ::SITEROOT/shop/account?for=gift'" } ) . "</li>";
- } else {
- $ret .= "<li>" . $class->ml( 'widget.shopitemgroupdisplay.paidaccounts.item.existingaccount', { aopts => "href='$LJ::SITEROOT/shop/account?for=gift'" } ) . "</li>";
- }
- $ret .= "<li>" . $class->ml( 'widget.shopitemgroupdisplay.paidaccounts.item.newaccount', { aopts => "href='$LJ::SITEROOT/shop/account?for=new'" } ) . "</li>";
- $ret .= "<li>" . $class->ml( 'widget.shopitemgroupdisplay.paidaccounts.item.randomaccount.show', { aopts => "href='$LJ::SITEROOT/shop/randomgift'" } ) . "</li>";
- $ret .= "<li>" . $class->ml( 'widget.shopitemgroupdisplay.paidaccounts.item.randomaccount.noshow', { aopts => "href='$LJ::SITEROOT/shop/account?for=random'" } ) . "</li>";
- $ret .= "</ul>";
- }
-
- return $ret;
-}
-
-1;
diff -r edaf4b8bc572 -r e0b4a1757c52 cgi-bin/LJ/User.pm
--- a/cgi-bin/LJ/User.pm Mon Apr 05 19:14:58 2010 -0700
+++ b/cgi-bin/LJ/User.pm Tue Apr 06 03:06:20 2010 +0000
@@ -2007,6 +2007,7 @@ sub clear_prop {
}
=head3 C<< $self->clear_daycounts( @security ) >>
+
Clears the day counts relevant to the entry security
security is an array of strings: "public", a number (allowmask), "private"
@@ -2178,6 +2179,75 @@ sub get_cap {
}
+=head3 C<< $self->give_shop_points( %options ) >>
+
+The options hash MUST contain the following keys:
+
+=over 4
+
+=item amount
+
+How many points to give the user. May be positive or negative, s
+use a negative number to deduct points from a user's balance. Will not allow
+a user's balance to go negative.
+
+=item reason
+
+A short description of why this transaction is happening. For
+example: 'purchase of cart 9883463'.
+
+=back
+
+The options hash MAY contain these keys, as well:
+
+=over 4
+
+=item admin
+
+If this action was being done by an administrator, pass their userid
+or user object here. This helps us record when admins make things happen.
+
+=back
+
+Example usage:
+
+C<< $self->give_shop_points( amount => 50, reason => 'purchased' ); >>
+
+This gives 50 points to the user as a routine purchase.
+
+C<< $self->give_shop_points( amount => -100, reason => 'refund', admin => $remote ); >>
+
+Admin processed refund, remove 100 points from the user's balance.
+
+Returns a true value on success, undef on error.
+
+=cut
+
+sub give_shop_points {
+ my ( $self, %opts ) = @_;
+ return unless LJ::isu( $self );
+
+ # do some cleanup on our input parameters
+ $opts{amount} += 0;
+ $opts{reason} = LJ::trim( $opts{reason} );
+ return unless $opts{amount} && $opts{reason};
+
+ # ensure we're not going negative ...
+ my $old = $self->shop_points;
+ die "Unable to set points balance to negative value.\n"
+ if $old + $opts{amount} < 0;
+
+ # log the change first so we know what's going on
+ my $admin = $opts{admin} ? LJ::want_user( $opts{admin} ) : undef;
+ my $msg = sprintf( 'old balance: %d, adjust amount: %d, reason: %s', $old, $opts{amount}, $opts{reason} );
+ LJ::statushistory_add( $self->id, $admin ? $admin->id : undef, 'shop_points', $msg);
+
+ # finally set the value
+ $self->set_prop( shop_points => $old + $opts{amount } );
+ return 1;
+}
+
+
# get/set the gizmo account of a user
sub gizmo_account {
my $u = shift;
@@ -2549,6 +2619,18 @@ sub share_contactinfo {
return 0 if $u->opt_showcontact eq 'R' && !$remote;
return 0 if $u->opt_showcontact eq 'F' && !$u->trusts( $remote );
return 1;
+}
+
+
+=head3 C<< $self->shop_points >>
+
+Returns how many points this user currently has available for spending in the
+shop. For adjusting points on a user, please see C<<$self->give_shop_points>>.
+
+=cut
+
+sub shop_points {
+ return $_[0]->prop( 'shop_points' )+0;
}
diff -r edaf4b8bc572 -r e0b4a1757c52 cgi-bin/LJ/Widget/ShopCart.pm
--- a/cgi-bin/LJ/Widget/ShopCart.pm Mon Apr 05 19:14:58 2010 -0700
+++ b/cgi-bin/LJ/Widget/ShopCart.pm Tue Apr 06 03:06:20 2010 +0000
@@ -42,13 +42,20 @@ sub render_body {
$opts{receipt} = 1
if $opts{admin};
+ # if we're not doing a receipt load, then we should balance the points. this
+ # fixes situations where the user gets gifted points while they're shopping.
+ unless ( $opts{receipt} ) {
+ $cart->recalculate_costs;
+ $cart->save;
+ }
+
my $colspan = $opts{receipt} ? 5 : 6;
$ret .= $class->start_form
unless $opts{receipt};
$ret .= "<table class='shop-cart'>";
- $ret .= "<tr><th>" . $class->ml( 'widget.shopcart.header.remove' ) . "</th>"
+ $ret .= "<tr><th></th>"
unless $opts{receipt};
$ret .= "<th>" . $class->ml( 'widget.shopcart.header.item' ) . "</th>";
$ret .= "<th>" . $class->ml( 'widget.shopcart.header.deliverydate' ) . "</th>";
@@ -58,6 +65,7 @@ sub render_body {
$ret .= "<th>" . $class->ml( 'widget.shopcart.header.price' ) . "</th>";
$ret .= "<th>ADMIN</th>" if $opts{admin};
$ret .= "</tr>";
+
foreach my $item ( @{$cart->items} ) {
$ret .= "<tr>";
if ( $opts{receipt} ) {
@@ -72,7 +80,8 @@ sub render_body {
$ret .= "<td>" . $item->t_html( admin => $opts{admin} ) . "</td>";
$ret .= "<td>" . $item->from_html . "</td>";
$ret .= "<td>" . ( $item->random ? 'Y' : 'N' ) . "</td>" if $opts{admin};
- $ret .= "<td>\$" . sprintf( "%.2f" , $item->cost ) . " USD</td>";
+ $ret .= "<td>" . $item->display_paid . "</td>\n";
+
if ( $opts{admin} ) {
$ret .= "<td>";
if ( $item->t_email ) {
@@ -106,24 +115,35 @@ sub render_body {
$ret .= "<tr><td class='total' style='border-right: none; text-align: left;' colspan='3'>$buttons</td>";
$ret .= "<td style='border-left: none;' colspan='" . ($colspan-3) .
- "' class='total'>" . $class->ml( 'widget.shopcart.total' ) . " \$" . $cart->display_total . " USD</td></tr>";
+ "' class='total'>" . $class->ml( 'widget.shopcart.total' ) . " " . $cart->display_total . "</td></tr>";
$ret .= "</table>";
unless ( $opts{receipt} ) {
- $ret .= "<div class='shop-cart-btn'><p>";
+ $ret .= "<div class='shop-cart-btn'><p><strong>" . $class->ml( 'widget.shopcart.paymentmethod' ) . "</strong> ";
- # google has very specific rules about where the buttons go and how to display them
- # ... so we have to abide by that
- if ( LJ::is_enabled( 'googlecheckout' ) ) {
- $ret .= '<input type="image" name="' . $class->input_prefix . '_checkout_gco" src="https://checkout.google.com/buttons/checkout.gif?' .
- 'merchant_id=&w=180&h=46&style=trans&variant=text&loc=en_US" alt="Use Google Checkout" style="vertical-align: middle;" /> or use ';
+ # if the cart is zero cost, then we can just let them check out
+ if ( $cart->total_cash == 0.00 ) {
+ $ret .= $class->html_submit( checkout_free => $class->ml( 'widget.shopcart.paymentmethod.free' ) );
+
+ } else {
+ # google has very specific rules about where the buttons go and how to display them
+ # ... so we have to abide by that
+ if ( LJ::is_enabled( 'googlecheckout' ) ) {
+ $ret .= '<input type="image" name="' . $class->input_prefix . '_checkout_gco" src="https://checkout.google.com/buttons/checkout.gif?' .
+ 'merchant_id=&w=180&h=46&style=trans&variant=text&loc=en_US" alt="Use Google Checkout" style="vertical-align: middle;" /> or use ';
+ $ret .= " ";
+ }
+
+ # and now any hooks that want to add to this...
+ $ret .= $class->html_submit( checkout_creditcard => $class->ml( 'widget.shopcart.paymentmethod.creditcard' ) );
+ $ret .= " ";
+
+ # check or money order button
+ $ret .= $class->html_submit( checkout_cmo => $class->ml( 'widget.shopcart.paymentmethod.checkmoneyorder' ) );
}
- # check or money order button
- $ret .= $class->html_submit( checkout_cmo => $class->ml( 'widget.shopcart.paymentmethod.checkmoneyorder' ) );
-
$ret .= "</p></div>";
- $ret .= $class->end_form
+ $ret .= $class->end_form;
}
return $ret;
@@ -135,7 +155,8 @@ sub handle_post {
# checkout methods depend on which button was clicked
my $cm;
$cm = 'gco' if LJ::is_enabled( 'googlecheckout' ) && ( $post->{checkout_gco} || $post->{'checkout_gco.x'} );
- $cm = 'checkmoneyorder' if $post->{checkout_cmo};
+ $cm = 'creditcard' if $post->{checkout_creditcard};
+ $cm = 'checkmoneyorder' if $post->{checkout_cmo} || $post->{checkout_free};
# check out?
return BML::redirect( "$LJ::SITEROOT/shop/checkout?method=$cm" )
diff -r edaf4b8bc572 -r e0b4a1757c52 cgi-bin/LJ/Widget/ShopItemOptions.pm
--- a/cgi-bin/LJ/Widget/ShopItemOptions.pm Mon Apr 05 19:14:58 2010 -0700
+++ b/cgi-bin/LJ/Widget/ShopItemOptions.pm Tue Apr 06 03:06:20 2010 +0000
@@ -57,8 +57,8 @@ sub render_body {
foreach my $month_value ( sort { $b <=> $a } @month_values ) {
my $full_item = $given_item . $month_value;
if ( ref $LJ::SHOP{$full_item} eq 'ARRAY' ) {
- my $price_string = $class->ml( "widget.shopitemoptions.price.$full_item", { price => "\$".sprintf( "%.2f" , $LJ::SHOP{$full_item}->[0] )." USD" } );
- $price_string = $class->ml( 'widget.shopitemoptions.price', { num => $month_value, price => "\$".sprintf( "%.2f" , $LJ::SHOP{$full_item}->[0] )." USD" } )
+ my $price_string = $class->ml( "widget.shopitemoptions.price.$full_item", { price => "\$".sprintf( "%.2f" , $LJ::SHOP{$full_item}->[0] )." USD", points => $LJ::SHOP{$full_item}->[3] } );
+ $price_string = $class->ml( 'widget.shopitemoptions.price', { num => $month_value, price => "\$".sprintf( "%.2f" , $LJ::SHOP{$full_item}->[0] )." USD", points => $LJ::SHOP{$full_item}->[3] } )
if $price_string eq 'ShopItemOptions';
$ret .= $class->html_check(
diff -r edaf4b8bc572 -r e0b4a1757c52 cvs/multicvs.conf
--- a/cvs/multicvs.conf Mon Apr 05 19:14:58 2010 -0700
+++ b/cvs/multicvs.conf Tue Apr 06 03:06:20 2010 +0000
@@ -86,6 +86,12 @@ mogilefs/utils/mogadm
mogilefs/utils/mogadm bin/mogadm
mogilefs/utils/mogtool bin/mogtool
+dw-free/src/DSMS/lib/DSMS cgi-bin/DSMS/
+
+djabberd/ src/djabberd/
+
+LJ-UserSearch/ src/LJ-UserSearch/
+
dw-free/htdocs/mobile/login.bml ssldocs/mobile/login.bml
dw-free/htdocs/login.bml ssldocs/login.bml
dw-free/htdocs/tools/endpoints/checkforusername.bml ssldocs/tools/endpoints/checkforusername.bml
@@ -101,15 +107,8 @@ dw-free/htdocs/tools/importer.bml
dw-free/htdocs/tools/importer.bml ssldocs/tools/importer.bml
dw-free/htdocs/manage/externalaccount.bml ssldocs/manage/externalaccount.bml
dw-free/htdocs/admin/impersonate.bml ssldocs/admin/impersonate.bml
-dw-free/htdocs/shop/gco_notify.bml ssldocs/shop/gco_notify.bml
-
-dw-free/src/DSMS/lib/DSMS cgi-bin/DSMS/
-
-djabberd/ src/djabberd/
-
-LJ-UserSearch/ src/LJ-UserSearch/
-
-local? .
+dw-free/htdocs/manage/circle/invite.bml ssldocs/manage/circle/invite.bml
+dw-free/htdocs/shop/ ssldocs/shop/
js/core.js ssldocs/js/core.js
js/devel.js ssldocs/js/devel.js
@@ -119,59 +118,9 @@ js/hourglass.js
js/hourglass.js ssldocs/js/hourglass.js
js/common/AdEngine.js ssldocs/js/common/AdEngine.js
-dw-free/htdocs/_config.bml ssldocs/_config.bml
-dw-free/htdocs/img/celerity ssldocs/img/celerity
-dw-free/htdocs/img/community.gif ssldocs/img/community.gif
-dw-free/htdocs/img/corners/bl-on-a7c4d8.gif ssldocs/img/corners/bl-on-a7c4d8.gif
-dw-free/htdocs/img/corners/bl-on-f0f5f9.gif ssldocs/img/corners/bl-on-f0f5f9.gif
-dw-free/htdocs/img/corners/bl-on-ffffff.gif ssldocs/img/corners/bl-on-ffffff.gif
-dw-free/htdocs/img/corners/br-on-a7c4d8.gif ssldocs/img/corners/br-on-a7c4d8.gif
-dw-free/htdocs/img/corners/br-on-f0f5f9.gif ssldocs/img/corners/br-on-f0f5f9.gif
-dw-free/htdocs/img/corners/br-on-ffffff.gif ssldocs/img/corners/br-on-ffffff.gif
-dw-free/htdocs/img/corners/tl-on-a7c4d8.gif ssldocs/img/corners/tl-on-a7c4d8.gif
-dw-free/htdocs/img/corners/tl-on-f0f5f9.gif ssldocs/img/corners/tl-on-f0f5f9.gif
-dw-free/htdocs/img/corners/tl-on-ffffff.gif ssldocs/img/corners/tl-on-ffffff.gif
-dw-free/htdocs/img/corners/tr-on-a7c4d8.gif ssldocs/img/corners/tr-on-a7c4d8.gif
-dw-free/htdocs/img/corners/tr-on-f0f5f9.gif ssldocs/img/corners/tr-on-f0f5f9.gif
-dw-free/htdocs/img/corners/tr-on-ffffff.gif ssldocs/img/corners/tr-on-ffffff.gif
-dw-free/htdocs/img/create/check.png ssldocs/img/create/check.png
-dw-free/htdocs/img/create/numbers-active/1.gif ssldocs/img/create/numbers-active/1.gif
-dw-free/htdocs/img/create/numbers-active/2.gif ssldocs/img/create/numbers-active/2.gif
-dw-free/htdocs/img/create/numbers-active/3.gif ssldocs/img/create/numbers-active/3.gif
-dw-free/htdocs/img/create/numbers-active/4.gif ssldocs/img/create/numbers-active/4.gif
-dw-free/htdocs/img/create/numbers-inactive/1.gif ssldocs/img/create/numbers-inactive/1.gif
-dw-free/htdocs/img/create/numbers-inactive/2.gif ssldocs/img/create/numbers-inactive/2.gif
-dw-free/htdocs/img/create/numbers-inactive/3.gif ssldocs/img/create/numbers-inactive/3.gif
-dw-free/htdocs/img/create/numbers-inactive/4.gif ssldocs/img/create/numbers-inactive/4.gif
-dw-free/htdocs/img/create/tip-arrow.png ssldocs/img/create/tip-arrow.png
-dw-free/htdocs/img/fffccc-gradient.gif ssldocs/img/fffccc-gradient.gif
-dw-free/htdocs/img/help.gif ssldocs/img/help.gif
-dw-free/htdocs/img/input-bg.gif ssldocs/img/input-bg.gif
-dw-free/htdocs/img/message-error.gif ssldocs/img/message-error.gif
-dw-free/htdocs/img/message-warning.gif ssldocs/img/message-warning.gif
-dw-free/htdocs/img/newsinfo.gif ssldocs/img/newsinfo.gif
-dw-free/htdocs/img/nouserpic.png ssldocs/img/nouserpic.png
-dw-free/htdocs/img/openid-profile.gif ssldocs/img/openid-profile.gif
-dw-free/htdocs/img/padlocked.gif ssldocs/img/padlocked.gif
-dw-free/htdocs/img/silk ssldocs/img/silk
-dw-free/htdocs/img/syndicated.gif ssldocs/img/syndicated.gif
-dw-free/htdocs/js/browserdetect.js ssldocs/js/browserdetect.js
-dw-free/htdocs/js/contextualhover.js ssldocs/js/contextualhover.js
-dw-free/htdocs/js/esn.js ssldocs/js/esn.js
-dw-free/htdocs/js/livejournal.js ssldocs/js/livejournal.js
-dw-free/htdocs/js/livejournal-devel.js ssldocs/js/livejournal-devel.js
-dw-free/htdocs/js/lj_ippu.js ssldocs/js/lj_ippu.js
-dw-free/htdocs/js/widgets/createaccount.js ssldocs/js/widgets/createaccount.js
-dw-free/htdocs/stc/celerity ssldocs/stc/celerity
-dw-free/htdocs/stc/contextualhover.css ssldocs/stc/contextualhover.css
-dw-free/htdocs/stc/esn.css ssldocs/stc/esn.css
-dw-free/htdocs/stc/importer.css ssldocs/stc/importer.css
-dw-free/htdocs/stc/lj_base.css ssldocs/stc/lj_base.css
-dw-free/htdocs/stc/lj_base-app.css ssldocs/stc/lj_base-app.css
-dw-free/htdocs/stc/lj_settings.css ssldocs/stc/lj_settings.css
-dw-free/htdocs/stc/widgets/createaccount.css ssldocs/stc/widgets/createaccount.css
-dw-free/htdocs/stc/widgets/createaccountprogressmeter.css ssldocs/stc/widgets/createaccountprogressmeter.css
-dw-free/htdocs/js/jobstatus.js ssldocs/js/jobstatus.js
-dw-free/htdocs/manage/circle/invite.bml ssldocs/manage/circle/invite.bml
-dw-free/htdocs/js/ljwidget.js ssldocs/js/ljwidget.js
-dw-free/htdocs/js/externalaccount.js ssldocs/js/externalaccount.js
+dw-free/htdocs/_config.bml ssldocs/_config.bml
+dw-free/htdocs/img/ ssldocs/img/
+dw-free/htdocs/js/ ssldocs/js/
+dw-free/htdocs/stc/ ssldocs/stc/
+
+local? .
diff -r edaf4b8bc572 -r e0b4a1757c52 htdocs/admin/pay/index.bml
--- a/htdocs/admin/pay/index.bml Mon Apr 05 19:14:58 2010 -0700
+++ b/htdocs/admin/pay/index.bml Tue Apr 06 03:06:20 2010 +0000
@@ -123,7 +123,7 @@ EOF
$body .= "<tr>";
$body .= "<td>" . $cart->id . "</td>";
$body .= "<td>" . $date->strftime( "%F %r %Z" ) . "</td>";
- $body .= "<td>\$" . $cart->total . " USD</td>";
+ $body .= "<td>" . $cart->display_total . "</td>";
$body .= "<td>$paystr</td>";
$body .= "<td>$ML{\"/shop/receipt.bml.cart.status.$state\"}</td>";
$body .= "<td><a href='$LJ::SITEROOT/admin/pay/view?cartid=" . $cart->id . "'>$detailstext</a></td>";
diff -r edaf4b8bc572 -r e0b4a1757c52 htdocs/admin/pay/view.bml
--- a/htdocs/admin/pay/view.bml Mon Apr 05 19:14:58 2010 -0700
+++ b/htdocs/admin/pay/view.bml Tue Apr 06 03:06:20 2010 +0000
@@ -109,7 +109,7 @@ EOF
$body .= "<table border='1'>";
$body .= "<tr><th>From:</th><td>$from</td></tr>";
$body .= "<tr><th>Date:</th><td>" . DateTime->from_epoch( epoch => $cart->starttime )->strftime( "%F %r %Z" ) . "</td></tr>";
- $body .= "<tr><th>Total:</th><td>\$" . $cart->total . " USD</td></tr>";
+ $body .= "<tr><th>Total:</th><td>\$" . $cart->display_total . " USD</td></tr>";
$body .= "<tr><th>Payment Method:</th><td>$paystr</td></tr>";
$body .= "<tr><th>Uniq:</th><td>" . $cart->uniq . "</td></tr>";
$body .= "<tr><th>IP:</th><td>" . $cart->ip . "</td></tr>";
diff -r edaf4b8bc572 -r e0b4a1757c52 htdocs/img/shop/logo_amex.gif
Binary file htdocs/img/shop/logo_amex.gif has changed
diff -r edaf4b8bc572 -r e0b4a1757c52 htdocs/img/shop/logo_discover.gif
Binary file htdocs/img/shop/logo_discover.gif has changed
diff -r edaf4b8bc572 -r e0b4a1757c52 htdocs/img/shop/logo_mastercard.gif
Binary file htdocs/img/shop/logo_mastercard.gif has changed
diff -r edaf4b8bc572 -r e0b4a1757c52 htdocs/img/shop/logo_visa.gif
Binary file htdocs/img/shop/logo_visa.gif has changed
diff -r edaf4b8bc572 -r e0b4a1757c52 htdocs/img/silk/site/cart.png
Binary file htdocs/img/silk/site/cart.png has changed
diff -r edaf4b8bc572 -r e0b4a1757c52 htdocs/img/silk/site/cart_add.png
Binary file htdocs/img/silk/site/cart_add.png has changed
diff -r edaf4b8bc572 -r e0b4a1757c52 htdocs/img/silk/site/cart_delete.png
Binary file htdocs/img/silk/site/cart_delete.png has changed
diff -r edaf4b8bc572 -r e0b4a1757c52 htdocs/img/silk/site/cart_edit.png
Binary file htdocs/img/silk/site/cart_edit.png has changed
diff -r edaf4b8bc572 -r e0b4a1757c52 htdocs/img/silk/site/cart_error.png
Binary file htdocs/img/silk/site/cart_error.png has changed
diff -r edaf4b8bc572 -r e0b4a1757c52 htdocs/img/silk/site/cart_go.png
Binary file htdocs/img/silk/site/cart_go.png has changed
diff -r edaf4b8bc572 -r e0b4a1757c52 htdocs/img/silk/site/cart_put.png
Binary file htdocs/img/silk/site/cart_put.png has changed
diff -r edaf4b8bc572 -r e0b4a1757c52 htdocs/img/silk/site/cart_remove.png
Binary file htdocs/img/silk/site/cart_remove.png has changed
diff -r edaf4b8bc572 -r e0b4a1757c52 htdocs/js/shop/creditcard.js
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/htdocs/js/shop/creditcard.js Tue Apr 06 03:06:20 2010 +0000
@@ -0,0 +1,34 @@
+/*
+ js/shop/creditcard.js
+
+ Credit Card page JavaScript
+
+ Authors:
+ Mark Smith <mark@dreamwidth.org>
+
+ Copyright (c) 2010 by Dreamwidth Studios, LLC.
+
+ This program is free software; you may redistribute it and/or modify it under
+ the same terms as Perl itself. For a copy of the license, please reference
+ 'perldoc perlartistic' or 'perldoc perlgpl'.
+*/
+
+// called when we need to update the state of the various
+// selection boxes and widgets we use... nothing amazing
+function shop_cc_ShowHideBoxes() {
+
+ if ( $('#country').val() == 'US' ) {
+ $('#usstate').show();
+ $('#otherstate').hide();
+
+ } else {
+ $('#usstate').hide();
+ $('#otherstate').show();
+ }
+}
+
+DW.whenPageLoaded( function() {
+ shop_cc_ShowHideBoxes();
+
+ $('#country').change( shop_cc_ShowHideBoxes );
+} );
diff -r edaf4b8bc572 -r e0b4a1757c52 htdocs/shop.bml
--- a/htdocs/shop.bml Mon Apr 05 19:14:58 2010 -0700
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,55 +0,0 @@
-<?_c
-#
-# shop.bml
-#
-# This is the main storefront for the shop. Gives people a page they can browse
-# around looking for something interesting to buy.
-#
-# Authors:
-# Mark Smith <mark@dreamwidth.org>
-# Janine Smith <janine@netrophic.com>
-#
-# Copyright (c) 2009 by Dreamwidth Studios, LLC.
-#
-# This program is free software; you may redistribute it and/or modify it under
-# the same terms as Perl itself. For a copy of the license, please reference
-# 'perldoc perlartistic' or 'perldoc perlgpl'.
-#
-_c?><?page
-body<=
-<?_code
-{
- use strict;
- use vars qw/ %GET %POST $title /;
-
- return BML::redirect( "$LJ::SITEROOT/" )
- unless LJ::is_enabled( 'payments' );
-
- # this page uses new style JS
- LJ::need_res( 'stc/shop.css' );
- LJ::set_active_resource_group( 'jquery' );
-
- $title = BML::ml( '.title', { sitename => $LJ::SITENAMESHORT } );
-
- my $err = DW::Shop->remote_sysban_check;
- return $err if $err;
-
- my $ret;
-
- $ret .= DW::Widget::ShopCartStatusBar->render( %GET );
- $ret .= "<p>" . BML::ml( '.intro', { sitename => $LJ::SITENAMESHORT } ) . "</p>";
-
- $ret .= DW::Widget::ShopItemGroupDisplay->render( group => 'paidaccounts' );
-
- $ret .= "<div class='shopbox'>";
- $ret .= "<p>" . BML::ml( '.sideblurb', { sitename => $LJ::SITENAMESHORT, aopts => "href='$LJ::HELPURL{paidaccountinfo}'" } ) . "</p>";
- $ret .= "<p>" . BML::ml( '.viewhistory', { aopts => "href='$LJ::SITEROOT/shop/history'" } ). "</p>"
- if LJ::get_remote();
- $ret .= "</div>";
-
- return $ret;
-}
-_code?>
-<=body
-title=><?_code return $title; _code?>
-page?>
diff -r edaf4b8bc572 -r e0b4a1757c52 htdocs/shop.bml.text
--- a/htdocs/shop.bml.text Mon Apr 05 19:14:58 2010 -0700
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,11 +0,0 @@
-;; -*- coding: utf-8 -*-
-
-.intro=Welcome to the [[sitename]] shop! If you are interested in supporting [[sitename]] or are just looking for more features for your account, you have come to the right place.</p><p>To purchase something from our shop, build your order and then select your preferred method of payment. If you select PayPal/Credit Card, you'll be forwarded to PayPal, our merchant processor. If you select Check/Money Order, you will be provided instructions on where to send it after you confirm your order.
-
-.paymentblock=We're sorry, but your [[blocktype]] has been blocked from making payments. For more information about this block, please contact [[email]].
-
-.sideblurb=You can learn about paid accounts <a [[aopts]]>here</a>.
-
-.title=[[sitename]] Shop
-
-.viewhistory=To view your past orders, go <a [[aopts]]>here</a>.
diff -r edaf4b8bc572 -r e0b4a1757c52 htdocs/shop/account.bml
--- a/htdocs/shop/account.bml Mon Apr 05 19:14:58 2010 -0700
+++ b/htdocs/shop/account.bml Tue Apr 06 03:06:20 2010 +0000
@@ -48,9 +48,13 @@ body<=
return $ML{'.error.invalidself.perm'}
if $for eq 'self' && $account_type eq 'seed';
- my $ret = DW::Widget::ShopCartStatusBar->render( %GET );
+ my $ret = "<p><a href='$LJ::SITEROOT/shop'><< " . BML::ml( '.backlink', { sitename => $LJ::SITENAMESHORT } ) . "</a></p>";
- $ret .= "<p><a href='$LJ::SITEROOT/shop'><< " . BML::ml( '.backlink', { sitename => $LJ::SITENAMESHORT } ) . "</a></p>";
+ # hack in cart status here ... will be easy once this page is TTd
+ {
+ my $shop = DW::Shop->get;
+ $ret .= DW::Template->template_string( 'shop/cartdisplay.tt', { shop => $shop, cart => $shop->cart, remote => $remote } );
+ }
if ( $for eq 'self' ) {
$ret .= "<div class='leftybox'>" . BML::ml( '.intro.self', { user => $remote->ljuser_display, aopts => "href='$LJ::HELPURL{paidaccountinfo}'" } ) . "</div>";
@@ -81,7 +85,7 @@ body<=
if ( $error ) {
$ret .= qq{<div class="shop-error">$error</div>};
} else {
- return BML::redirect( "$LJ::SITEROOT/shop/cart" );
+ return BML::redirect( "$LJ::SITEROOT/shop" );
}
}
diff -r edaf4b8bc572 -r e0b4a1757c52 htdocs/shop/cart.bml
--- a/htdocs/shop/cart.bml Mon Apr 05 19:14:58 2010 -0700
+++ b/htdocs/shop/cart.bml Tue Apr 06 03:06:20 2010 +0000
@@ -34,6 +34,10 @@ body<=
return $err if $err;
my $ret;
+ $ret .= "<p><a href='$LJ::SITEROOT/shop'><< $ML{'.backlink'}</a> • <a href='$LJ::SITEROOT/shop/gifts'>$ML{'.gift'}</a></p>";
+
+ $ret .= "<?standout <strong>$ML{'.payment.failed.title'}</strong><br /><br />$ML{'.payment.failed'} standout?>\n"
+ if $GET{failed};
if ( LJ::did_post() ) {
my %from_post = LJ::Widget->handle_post( \%POST, ( 'ShopCart' ) );
@@ -41,7 +45,6 @@ body<=
if $from_post{error};
}
- $ret .= "<p><a href='$LJ::SITEROOT/shop'><< $ML{'.backlink'}</a> • <a href='$LJ::SITEROOT/shop/gifts'>$ML{'.gift'}</a></p>";
$ret .= LJ::Widget::ShopCart->render;
diff -r edaf4b8bc572 -r e0b4a1757c52 htdocs/shop/cart.bml.text
--- a/htdocs/shop/cart.bml.text Mon Apr 05 19:14:58 2010 -0700
+++ b/htdocs/shop/cart.bml.text Tue Apr 06 03:06:20 2010 +0000
@@ -4,4 +4,8 @@
.gift=Give a Gift
+.payment.failed=Unfortunately, your method of payment was declined by our merchant processor. If you would like to choose an alternate form of payment, or make sure you entered everything correctly, you will need to initiate the checkout process again.
+
+.payment.failed.title=Payment Failed
+
.title=Your Shopping Cart
diff -r edaf4b8bc572 -r e0b4a1757c52 htdocs/shop/checkout.bml
--- a/htdocs/shop/checkout.bml Mon Apr 05 19:14:58 2010 -0700
+++ b/htdocs/shop/checkout.bml Tue Apr 06 03:06:20 2010 +0000
@@ -42,7 +42,7 @@ body<=
# FIXME: if they have a $0 cart, we don't support that yet
return $ML{'.error.zerocart'}
- if $cart->total == 0.00;
+ if $cart->total_cash == 0.00 && $cart->total_points == 0;
# establish the engine they're trying to use
my $eng = DW::Shop::Engine->get( $GET{method}, $cart );
diff -r edaf4b8bc572 -r e0b4a1757c52 htdocs/shop/confirm.bml
--- a/htdocs/shop/confirm.bml Mon Apr 05 19:14:58 2010 -0700
+++ b/htdocs/shop/confirm.bml Tue Apr 06 03:06:20 2010 +0000
@@ -9,7 +9,7 @@
# Mark Smith <mark@dreamwidth.org>
# Janine Smith <janine@netrophic.com>
#
-# Copyright (c) 2009 by Dreamwidth Studios, LLC.
+# Copyright (c) 2009-2010 by Dreamwidth Studios, LLC.
#
# This program is free software; you may redistribute it and/or modify it under
# the same terms as Perl itself. For a copy of the license, please reference
@@ -73,8 +73,12 @@ body<=
$ret .= LJ::Widget::ShopCart->render( receipt => 1, cart => $cart );
$ret .= "<form method='post' action='$LJ::SITEROOT/shop/confirm?ordernum=$ordernum'>\n";
$ret .= LJ::form_auth();
- $ret .= "<?p " . BML::ml( '.confirm', { sitename => $LJ::SITENAME, total => '<strong>$' . $cart->display_total . ' USD</strong>' } ) . " p?>";
- $ret .= "<?p $ML{\".confirm.$paymentmethod\"} p?>";
+ if ( $cart->total_cash > 0.00 ) {
+ $ret .= "<?p " . BML::ml( '.confirm', { sitename => $LJ::SITENAME, total => '<strong>' . $cart->display_total_cash . '</strong>' } ) . " p?>";
+ $ret .= "<?p $ML{\".confirm.$paymentmethod\"} p?>";
+ } elsif ( $cart->total_points > 0 ) {
+ $ret .= "<?p " . BML::ml( '.confirm.onlypoints', { sitename => $LJ::SITENAME } ) . " p?>";
+ }
# get their email address if they're not logged in
unless ( $cart->userid ) {
diff -r edaf4b8bc572 -r e0b4a1757c52 htdocs/shop/confirm.bml.text
--- a/htdocs/shop/confirm.bml.text Mon Apr 05 19:14:58 2010 -0700
+++ b/htdocs/shop/confirm.bml.text Tue Apr 06 03:06:20 2010 +0000
@@ -4,7 +4,7 @@
.btn.cancel=Cancel Payment
-.btn.confirm=Confirm Payment
+.btn.confirm=Confirm Order
.btn.viewreceipt=View Receipt
@@ -16,6 +16,8 @@
.confirm.email.label=Email Address:
+.confirm.onlypoints=Please confirm your order from [[sitename]]. This order will be charged to your account's points balance, you do not need to send us money for this order.
+
.confirm.paypal=Once confirmed, we will process your purchase as soon as PayPal notifies us of a successful transaction.
.error.invalidcart=Your cart is invalid.
@@ -26,6 +28,8 @@
.error.needtoken=You did not provide an order number or token.
+.success.checkmoneyorder.immediate=Your order has been successfully placed. We will process your order shortly. Thank you for your purchase!
+
.success.checkmoneyorder.processing=Your order has been successfully placed. It will be processed once we receive your check or money order. p?><?p Please make it out to [[sitecompany]] and mail it to: [[address]] p?><?p <strong>Be sure to include the apartment and order numbers!</strong> p?><?p Thank you for your purchase!
.success.paypal.immediate=Your order has been successfully placed. We will process your order shortly. Thank you for your purchase!
diff -r edaf4b8bc572 -r e0b4a1757c52 htdocs/shop/creditcard.bml
--- a/htdocs/shop/creditcard.bml Mon Apr 05 19:14:58 2010 -0700
+++ b/htdocs/shop/creditcard.bml Tue Apr 06 03:06:20 2010 +0000
@@ -49,7 +49,7 @@ body<=
# FIXME: if they have a $0 cart, we don't support that yet
return $ML{'.error.zerocart'}
- if $cart->total == 0.00;
+ if $cart->total_cash == 0.00;
# looks good, set the payment method and state
$cart->paymentmethod( 'creditcardpp' );
@@ -57,7 +57,7 @@ body<=
# values we need
my $cartid = $cart->id;
- my $cost = $cart->display_total;
+ my $cost = $cart->display_total_cash;
# okay, render out the button :-)
return qq|
diff -r edaf4b8bc572 -r e0b4a1757c52 htdocs/shop/creditcard_wait.bml
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/htdocs/shop/creditcard_wait.bml Tue Apr 06 03:06:20 2010 +0000
@@ -0,0 +1,74 @@
+<?_c
+#
+# shop/creditcard_wait.bml
+#
+# This page is a fairly simple "please wait while we try to charge you" page that
+# refreshes every 5 seconds until we have gotten a result.
+#
+# Authors:
+# Mark Smith <mark@dreamwidth.org>
+#
+# Copyright (c) 2010 by Dreamwidth Studios, LLC.
+#
+# This program is free software; you may redistribute it and/or modify it under
+# the same terms as Perl itself. For a copy of the license, please reference
+# 'perldoc perlartistic' or 'perldoc perlgpl'.
+#
+_c?><?page
+body<=
+<?_code
+{
+ use strict;
+ use vars qw/ %GET $title $head /;
+
+ return BML::redirect( "$LJ::SITEROOT/" )
+ unless LJ::is_enabled( 'payments' );
+
+ $head = '';
+ $title = $ML{'.title'};
+
+ # this page uses new style JS
+ LJ::need_res( 'stc/shop.css' );
+ LJ::set_active_resource_group( 'jquery' );
+
+ # see they got here ok
+ my $onum = $GET{ordernum};
+ my $cctransid = $GET{cctransid}+0;
+
+ # get their shop/cart
+ my $cart = DW::Shop::Cart->get_from_ordernum( $onum );
+ return $ML{'.error.nocart'}
+ unless $cart;
+
+ # establish the engine they're trying to use
+ my $eng = DW::Shop::Engine->get( creditcard => $cart );
+ return $ML{'.error.invalidpaymentmethod'}
+ unless $eng;
+
+ # get the row
+ my $row = $eng->get_transaction( $cctransid );
+ die "Row not for cart, that's no good!\n" # trying to spoof us?
+ unless $row && $row->{cctransid} == $cctransid &&
+ $row->{cartid} == $cart->id;
+
+ # if it failed, redirect back to the cart
+ return BML::redirect( "$LJ::SITEROOT/shop/cart?failed=1" )
+ if $row->{jobstate} eq 'failed';
+
+ # if we're still queued, set the refresh
+ $head = q{<meta http-equiv="refresh" content="3" />}
+ if $row->{jobstate} eq 'queued';
+
+ # looks good, see what status things are in...
+ $title = $ML{'.title.' . $row->{jobstate}} || $ML{'Error'};
+ return $ML{'.state.' . $row->{jobstate}}
+ if $row->{jobstate} =~ /^(?:paid|queued|internal_failure)$/;
+
+ # another state??
+ die "Sorry, unknown state: $row->{jobstate}.\n";
+}
+_code?>
+<=body
+title=><?_code return $title; _code?>
+head=><?_code return $head; _code?>
+page?>
diff -r edaf4b8bc572 -r e0b4a1757c52 htdocs/shop/creditcard_wait.bml.text
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/htdocs/shop/creditcard_wait.bml.text Tue Apr 06 03:06:20 2010 +0000
@@ -0,0 +1,17 @@
+;; -*- coding: utf-8 -*-
+
+.state.failed=We're sorry, but your payment has been declined by our credit card processor. You may wish to try an alternative payment method, or ensure that you accurately entered your billing information.
+
+.state.internal_failure=We're sorry, but something failed internally on our end attempting to process your payment. Your credit card has probably not been charged.
+
+.state.paid=Your payment has been successful. We will process your order in the next few minutes. Please keep an eye on your email for a confirmation.
+
+.state.queued=We are attempting to process your payment request, please wait...
+
+.title=Please Wait
+
+.title.internal_failure=Internal Failure
+
+.title.paid=Payment Received
+
+.title.queued=Please Wait
diff -r edaf4b8bc572 -r e0b4a1757c52 htdocs/shop/entercc.bml
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/htdocs/shop/entercc.bml Tue Apr 06 03:06:20 2010 +0000
@@ -0,0 +1,218 @@
+<?_c
+#
+# shop/entercc.bml
+#
+# Checkout page for letting the user enter credit card details. WARNING: this
+# page ABSOLUTELY requires SSL, unless we're in a development environment, and
+# MUST NOT store credit card information ANYWHERE. There are legal ramifications
+# if we were to store the information on our servers, or pass it around in any
+# sort of unencrypted manner!
+#
+# Authors:
+# Mark Smith <mark@dreamwidth.org>
+#
+# Copyright (c) 2010 by Dreamwidth Studios, LLC.
+#
+# This program is free software; you may redistribute it and/or modify it under
+# the same terms as Perl itself. For a copy of the license, please reference
+# 'perldoc perlartistic' or 'perldoc perlgpl'.
+#
+_c?><?page
+body<=
+<?_code
+{
+ use strict;
+ use vars qw/ %GET %POST $title /;
+ use Business::CreditCard;
+
+ return BML::redirect( "$LJ::SITEROOT/" )
+ unless LJ::is_enabled( 'payments' );
+
+ # VERY IMPORTANT
+ die "SSL must be enabled."
+ unless $LJ::IS_DEV_SERVER || $LJ::USE_SSL;
+
+ # now, redirect them to the SSL page just in case they got here without SSL
+ # actually being on...
+ return "$LJ::SSLROOT/shop/entercc"
+ unless $LJ::IS_DEV_SERVER || $LJ::IS_SSL;
+
+ $title = $ML{'.title'};
+
+ # this page uses new style JS
+ LJ::need_res( 'stc/shop.css' );
+ LJ::set_active_resource_group( 'jquery' );
+
+ # get their shop/cart
+ my $cart = DW::Shop->get->cart;
+ return $ML{'.error.nocart'}
+ unless $cart;
+ return $ML{'.error.emptycart'}
+ unless $cart->has_items;
+
+ # if state is NOT open, then just redirect them to the wait page,
+ # which will do the Right Thing. this typically is used in the case
+ # that the user double clicks on the form, or hits back and clicks
+ # submit again...
+ return BML::redirect( "$LJ::SITEROOT/shop/creditcard_wait?ordernum=" . $cart->ordernum . "&cctransid=" & $cart->{cctransid} )
+ unless $cart->state == $DW::Shop::STATE_OPEN;
+
+ # FIXME: if they have a $0 cart, we don't support that yet
+ return $ML{'.error.zerocart'}
+ if $cart->total_cash == 0.00;
+
+ # general purpose form renderer
+ my %err;
+ my $render_cc_form = sub {
+ # if any error, set the rest of the CC errors so the person can retype
+ if ( %err ) {
+ $err{ccnum} ||= $ML{'.error.reinput'};
+ $err{cvv2} ||= $ML{'.error.reinput'};
+ }
+
+ # need res...
+ LJ::need_res( {group => 'jquery'}, 'js/shop/creditcard.js' );
+
+ # load country codes, and US states
+ my ( %countries, %usstates );
+ LJ::load_codes( { country => \%countries } );
+ LJ::load_codes( { state => \%usstates } );
+
+ # accepted credit card list. this should be populated by the hooks.
+ my $accepted_ccs = '(failed to get list of accepted credit cards)';
+ LJ::Hooks::run_hook( 'creditcard_accepted_ccs', \$accepted_ccs );
+
+ # now sort the above appropriately
+ my @countries = ( '--' => '', US => 'United States', map { $_ => $countries{$_} } sort { $countries{$a} cmp $countries{$b} } keys %countries );
+ my @usstates = ( '--' => '(select state)', map { $_ => $usstates{$_} } sort { $usstates{$a} cmp $usstates{$b} } keys %usstates );
+
+ my @form = (
+ firstname => LJ::html_text({ name => 'firstname', maxlength => 25, value => $POST{firstname} }),
+ lastname => LJ::html_text({ name => 'lastname', maxlength => 25, value => $POST{lastname} }),
+ '--' => '',
+ country => LJ::html_select({ name => 'country', id => 'country', selected => $POST{country} }, @countries ),
+ '--' => '',
+ street1 => LJ::html_text({ name => 'street1', maxlength => 100, value => $POST{street1}, size => 40 }),
+ street2 => LJ::html_text({ name => 'street2', maxlength => 100, value => $POST{street2}, size => 40 }),
+ city => LJ::html_text({ name => 'city', maxlength => 40, value => $POST{city} }),
+ state => LJ::html_select({ name => 'usstate', id => 'usstate', selected => $POST{usstate} }, @usstates ) . ' ' .
+ LJ::html_text({ name => 'otherstate', id => 'otherstate', maxlength => 40, value => $POST{otherstate} }),
+ zip => LJ::html_text({ name => 'zip', maxlength => 20, value => $POST{zip} }),
+ phone => LJ::html_text({ name => 'phone', maxlength => 40, value => $POST{phone} }),
+ '--' => '',
+ ccnum => LJ::html_text({ name => 'ccnum', maxlength => 19 }) . $accepted_ccs,
+ cvv2 => LJ::html_text({ name => 'cvv2', maxlength => 3, size => 5 }),
+ expmon => LJ::html_select({ name => 'expmon', selected => $POST{expmon} }, map { $_ => LJ::Lang::month_long( $_ ) } 1..12 ),
+ expyear => LJ::html_select({ name => 'expyear', selected => $POST{expyear} }, map { $_ => $_ } 2010..2019 ),
+ );
+
+ my $out = "<?p $ML{'.about'} p?><?p <strong>" . LJ::Lang::ml( '.about.security', { sitename => $LJ::SITENAME } ) . "</strong> p?>";
+ $out .= "<form method='post'>" . LJ::form_auth() . "<table>";
+ while ( my ( $name, $edit ) = splice( @form, 0, 2 ) ) {
+ if ( $name eq '--' ) {
+ $out .= "<tr><td colspan='2'> </td></tr>";
+ next;
+ }
+
+ my $class = $err{$name} ? 'ccrow ccrowerr' : 'ccrow';
+ $out .= "<tr><td class='$class'>" . BML::ml( ".form.$name" ) . "</td>";
+ $out .= "<td class='$class'>" . $edit . "</td><td class='$class'>";
+ $out .= " <span style='font-color: red; font-weight: bold;'>Error: " . $err{$name} . "</span>"
+ if $err{$name};
+ $out .= "</td></tr>\n";
+ }
+ $out .= "</table><br /><?p ";
+ $out .= BML::ml( '.confirm.para', { amount => $cart->display_total_cash, sitename => $LJ::SITECOMPANY } );
+ $out .= " p?><br /><input type='submit' value='";
+ $out .= BML::ml( '.confirm.button', { amount => $cart->display_total_cash } );
+ $out .= "' /></form>";
+ };
+
+ # no paste, just return the form...
+ return $render_cc_form->()
+ unless LJ::did_post();
+
+ # check form auth
+ die "Invalid form auth!\n"
+ unless LJ::check_form_auth();
+
+ # check for errors... first, make sure we get everything that is required
+ my %in;
+ foreach my $name ( qw/ firstname lastname street1 street2 city country zip phone ccnum cvv2 expmon expyear / ) {
+ my $val = LJ::trim( $POST{$name} );
+ $val =~ s/\s+/ /; # canonicalize to single spaces
+
+ # double hyphens are special
+ $val = '' if $val eq '--';
+
+ # everything is required...except street2
+ unless ( $val || $name eq 'street2' || $name eq 'phone' ) {
+ $err{$name} = $ML{'.error.required'};
+ next;
+ }
+
+ # okay, we know we got something, validate the numerics
+ $in{$name} = $val;
+ }
+
+ # if US, then US state must be selected
+ $err{state} = $ML{'.error.required'}
+ if $in{country} eq 'US' && $POST{usstate} !~ /^\w\w$/;
+ $err{state} = $ML{'.error.required'}
+ if $in{country} ne 'US' && $POST{otherstate} !~ /\S/;
+
+ # if there are any errors in fields (some required but not given) then
+ # we return to handle the form now
+ return $render_cc_form->()
+ if %err;
+
+ # must be valid states by now
+ $in{state} = LJ::trim( $in{country} eq 'US' ? $POST{usstate} : $POST{otherstate} );
+
+ # now do some more checking
+ $err{cvv2} = $ML{'.error.cvv2.invalid'}
+ unless $in{cvv2} =~ /^\d\d\d\d?$/;
+ $err{expmon} = $ML{'.error.required'}
+ unless $in{expmon} >= 1 && $in{expmon} <= 12;
+ $err{expyear} = $ML{'.error.required'}
+ unless $in{expyear} >= 2010 && $in{expmon} <= 2019;
+
+ # validating the credit card is more intense
+ $in{ccnum} =~ s/\D//g; # remove all non-numerics
+ $err{ccnum} = $ML{'.error.ccnum.invalid'}
+ unless validate( $in{ccnum} );
+
+ # verify that the zip code is right for US
+ $err{zip} = $ML{'.error.zip.invalidus'}
+ if $in{country} eq 'US' && $in{zip} !~ /^(?:\d\d\d\d\d)(?:-?\d\d\d\d)?$/;
+
+ # final error check
+ return $render_cc_form->()
+ if %err;
+
+ # establish the engine they're trying to use
+ my $eng = DW::Shop::Engine->get( creditcard => $cart );
+ return $ML{'.error.invalidpaymentmethod'}
+ unless $eng;
+
+ # set the payment method on the cart
+ $cart->paymentmethod( 'creditcard' );
+ $cart->state( $DW::Shop::STATE_PEND_PAID );
+
+ # stuff a new row in the database
+ my $cctransid = $eng->setup_transaction( %in, cartid => $cart->id, ip => BML::get_remote_ip() );
+ return $ML{'.error.megafail'}
+ unless $cctransid && $cctransid > 0;
+
+ # FIXME: mega hack, we're depending on the storable state of the cart here
+ # and this should really be in a db row somewhere so we can reverse it
+ $cart->{cctransid} = $cctransid;
+ $cart->save;
+
+ # redirect to the waiting page now
+ return BML::redirect( "$LJ::SITEROOT/shop/creditcard_wait?ordernum=" . $cart->ordernum . "&cctransid=" . $cctransid );
+}
+_code?>
+<=body
+title=><?_code return $title; _code?>
+page?>
diff -r edaf4b8bc572 -r e0b4a1757c52 htdocs/shop/entercc.bml.text
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/htdocs/shop/entercc.bml.text Tue Apr 06 03:06:20 2010 +0000
@@ -0,0 +1,63 @@
+;; -*- coding: utf-8 -*-
+
+.about=This page allows you to complete your payment using an accepted major credit card.
+
+.about.security=[[sitename]] does not store your credit card information. Your payment details are passed to our payment processor using industry standard encryption and will never be stored on our servers.
+
+.confirm.button=Confirm Payment of [[amount]]
+
+.confirm.para=By filling out the above form and clicking the confirmation button below, you authorize [[sitename]] to charge the given credit card in the amount of [[amount]] to complete this purchase.
+
+.error.badcartstate=Your shopping cart has already been checked out. If your payment failed, you will need to build a new shopping cart to try again. We're sorry for the inconvenience.
+
+.error.ccnum.invalid=Credit card number appears invalid. Try again.
+
+.error.cctype.invalid=Invalid credit card type selected.
+
+.error.cvv2.invalid=CVV invalid. Must be a three or four digit number such as 123 or 4567.
+
+.error.emptycart=Your cart is empty.
+
+.error.invalidpaymentmethod=The payment method you chose is not currently supported.
+
+.error.megafail=Internal failure setting up payment. Please try again.
+
+.error.nocart=You do not have a shopping cart.
+
+.error.reinput=Please reinput this data.
+
+.error.required=Required field.
+
+.error.zerocart=Sorry, we don't support $0 carts yet.
+
+.error.zip.invalidus=Invalid US zip code. Must be in the form 12345 or 12345-5678.
+
+.form.ccnum=Credit Card Number:
+
+.form.cctype=Credit Card Type:
+
+.form.city=City:
+
+.form.country=Country:
+
+.form.cvv2=CVV Number:
+
+.form.expmon=Expiration (Month):
+
+.form.expyear=Expiration (Year):
+
+.form.firstname=First Name:
+
+.form.lastname=Last Name:
+
+.form.phone=Phone Number:
+
+.form.state=State/Province:
+
+.form.street1=Address line 1:
+
+.form.street2=Address line 2:
+
+.form.zip=Zip/Postal Code:
+
+.title=Purchase by Credit Card
diff -r edaf4b8bc572 -r e0b4a1757c52 htdocs/shop/history.bml
--- a/htdocs/shop/history.bml Mon Apr 05 19:14:58 2010 -0700
+++ b/htdocs/shop/history.bml Tue Apr 06 03:06:20 2010 +0000
@@ -49,7 +49,7 @@ body<=
$ret .= "<tr>";
$ret .= "<td>" . $cart->id . "</td>";
$ret .= "<td>" . $date->strftime( "%F %r %Z" ) . "</td>";
- $ret .= "<td>\$" . $cart->total . " USD</td>";
+ $ret .= "<td>" . $cart->display_total . "</td>";
$ret .= "<td>$ML{\"/shop/receipt.bml.cart.paymentmethod.$paymentmethod\"}</td>";
$ret .= "<td>$ML{\"/shop/receipt.bml.cart.status.$state\"}</td>";
$ret .= "<td><a href='$LJ::SITEROOT/shop/receipt?ordernum=" . $cart->ordernum . "'>$ML{'.cart.details'}</a></td>";
diff -r edaf4b8bc572 -r e0b4a1757c52 htdocs/shop/receipt.bml.text
--- a/htdocs/shop/receipt.bml.text Mon Apr 05 19:14:58 2010 -0700
+++ b/htdocs/shop/receipt.bml.text Tue Apr 06 03:06:20 2010 +0000
@@ -23,11 +23,15 @@
.cart.status.8|note=This status should never be possible on the receipt page!
.cart.status.8=Closed before completion
+.cart.status.9=Charge declined
+
.cart.paymentmethod=<strong>Payment Method:</strong> [[paymentmethod]]
.cart.paymentmethod.checkmoneyorder=Check/Money Order
.cart.paymentmethod.checkmoneyorder.extra=Please make your check or money order out to: [[sitecompany]]</p><p>Mail it to: [[address]]
+
+.cart.paymentmethod.creditcard=Credit Card
.cart.paymentmethod.creditcardpp=Credit Card
diff -r edaf4b8bc572 -r e0b4a1757c52 htdocs/stc/blueshift/blueshift.css
--- a/htdocs/stc/blueshift/blueshift.css Mon Apr 05 19:14:58 2010 -0700
+++ b/htdocs/stc/blueshift/blueshift.css Tue Apr 06 03:06:20 2010 +0000
@@ -361,12 +361,14 @@ table#table_yourlayers td { padding: .15
text-align: center;
}
.standout .standout-inner {
+ margin-top: 0.5em;
+ margin-left: auto;
+ margin-right: auto;
+}
+.standout .standout-inner, .standout-colors {
background-color: #E6ECF9;
color: #000;
border: 1px solid #D6DCE9;
- margin-top: 0.5em;
- margin-left: auto;
- margin-right: auto;
}
.standout .standout-inner td {
padding: 0.5em;
diff -r edaf4b8bc572 -r e0b4a1757c52 htdocs/stc/celerity/celerity.css
--- a/htdocs/stc/celerity/celerity.css Mon Apr 05 19:14:58 2010 -0700
+++ b/htdocs/stc/celerity/celerity.css Tue Apr 06 03:06:20 2010 +0000
@@ -396,12 +396,14 @@ table#table_yourlayers td { padding: .15
text-align: center;
}
.standout .standout-inner {
+ margin-top: 0.5em;
+ margin-left: auto;
+ margin-right: auto;
+}
+.standout .standout-inner, .standout-colors {
background-color: #DDDDAA;
color: #000;
border: 1px solid #999966;
- margin-top: 0.5em;
- margin-left: auto;
- margin-right: auto;
}
.standout .standout-inner td {
padding: 0.5em;
diff -r edaf4b8bc572 -r e0b4a1757c52 htdocs/stc/gradation/gradation-vertical.css
--- a/htdocs/stc/gradation/gradation-vertical.css Mon Apr 05 19:14:58 2010 -0700
+++ b/htdocs/stc/gradation/gradation-vertical.css Tue Apr 06 03:06:20 2010 +0000
@@ -409,12 +409,14 @@ table#table_yourlayers td { padding: .15
text-align: center;
}
.standout .standout-inner {
+ margin-top: 0.5em;
+ margin-left: auto;
+ margin-right: auto;
+}
+.standout .standout-inner, .standout-colors {
background-color: #333333;
color: #eee;
border: 1px solid #555555;
- margin-top: 0.5em;
- margin-left: auto;
- margin-right: auto;
}
.standout .standout-inner td {
padding: 0.5em;
diff -r edaf4b8bc572 -r e0b4a1757c52 htdocs/stc/shop.css
--- a/htdocs/stc/shop.css Mon Apr 05 19:14:58 2010 -0700
+++ b/htdocs/stc/shop.css Tue Apr 06 03:06:20 2010 +0000
@@ -115,3 +115,67 @@
.shop-footnote {
font-size: smaller;
}
+
+.ccrow {
+ padding: 3px;
+}
+
+.ccrowerr {
+ background-color: #ccc;
+}
+
+.shop-points-status {
+ margin: 20px;
+ border: 1px solid #c1272d;
+ padding: 0.5em;
+ font-size: larger;
+}
+
+.status-bar-options {
+ float: right;
+}
+
+.status-bar-option {
+ margin-left: 1em;
+ font-weight: bold;
+}
+
+.shop-category {
+ margin-top: 20px;
+}
+
+.shop-category-title {
+ font-size: larger;
+ font-weight: bold;
+}
+
+.shop-category-items {
+ margin-left: 10px;
+}
+
+.shop-category-item {
+ margin-left: 30px;
+}
+
+#shop-status-bar {
+ margin: 20px;
+ padding: 0.5em;
+ font-size: larger;
+}
+
+.shop-status-left {
+ width: 25%;
+ float: left;
+}
+
+.shop-status-right {
+ width: 25%;
+ float: right;
+ text-align: right;
+}
+
+.shop-status-middle {
+ float: left;
+ width: 50%;
+ text-align: center;
+}
diff -r edaf4b8bc572 -r e0b4a1757c52 views/shop/cartdisplay.tt
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/views/shop/cartdisplay.tt Tue Apr 06 03:06:20 2010 +0000
@@ -0,0 +1,30 @@
+<div id='shop-status-bar' class='standout-colors'>
+ <div class='shop-status-left'>
+ [% IF remote %]
+ [% IF remote.is_person %]
+ You have <strong>[% remote.shop_points %] points</strong>.<br />
+ [% IF help.shoppoints %]<a href="[% help.shoppoints %]">What is this?</a> |[% END %]
+ <a href="[% roots.site %]/shop/points">Buy More</a>
+ [% ELSE %]
+ Only personal accounts can carry a point balance.
+ [% END %]
+ [% ELSE %]
+ Log in to access your [% site.nameshort %] Points.
+ [% END %]
+ </div>
+ <div class='shop-status-middle'>
+ [% IF cart AND cart.has_items %]
+ <img src="[% roots.img %]/silk/site/cart.png" /> Order Cost: <strong>[% cart.display_total %]</strong><br />
+ <a href='[% roots.site %]/shop/cart'>View/Check Out Order</a> |
+ <a href='[% roots.site %]/shop?newcart=1'>Cancel Order</a>
+ [% ELSE %]
+ <img src="[% roots.img %]/silk/site/cart.png" /> Your shopping cart is empty.
+ [% END %]
+ </div>
+ <div class='shop-status-right'>
+ [%# Also use this section for sales links and stuff. %]
+ [% IF help.paidaccountinfo %]<a href="[% help.paidaccountinfo %]">About the Shop</a><br />[% END %]
+ <a href="[% roots.site %]/shop/history">Order History</a>
+ </div>
+ <div style='clear: both;'></div>
+</div>
diff -r edaf4b8bc572 -r e0b4a1757c52 views/shop/cartdisplay.tt.text
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/views/shop/cartdisplay.tt.text Tue Apr 06 03:06:20 2010 +0000
@@ -0,0 +1,7 @@
+.header=Shopping Cart
+
+.itemcount=[[num]] [[?num|item|items]] for a total of [[price]]
+
+.newcart=Discard Cart
+
+.viewcart=View Cart
diff -r edaf4b8bc572 -r e0b4a1757c52 views/shop/index.tt
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/views/shop/index.tt Tue Apr 06 03:06:20 2010 +0000
@@ -0,0 +1,58 @@
+[%- sections.title = '.title' | ml(sitename = site.name) -%]
+
+[% cart_display %]
+
+<p>[% '.about' | ml %]</p>
+
+<div class='shop-category'>
+ <div class='shop-category-title'>[% '.title.paidacc' | ml %]</div>
+ <div class='shop-category-items'>
+[% IF remote AND remote.is_personal AND NOT remote.is_perm %]
+ <span class='shop-category-item'><a href="[% roots.site %]/shop/account?for=self">[% '.for.self' | ml %]</a> ([% remote.ljuser_display %])</span>
+[% END %]
+[% IF remote AND remote.is_personal %]
+ <span class='shop-category-item'><a href="[% roots.site %]/shop/gifts?type=account">[% '.for.circle' | ml %]</a></span>
+ <span class='shop-category-item'><a href="[% roots.site %]/shop/account?for=gift">[% '.for.different' | ml %]</a></span>
+[% ELSE %]
+ <span class='shop-category-item'><a href="[% roots.site %]/shop/account?for=gift">[% '.for.existing' | ml %]</a></span>
+[% END %]
+ <span class='shop-category-item'><a href="[% roots.site %]/shop/account?for=new">[% '.for.new' | ml %]</a></span>
+ <span class='shop-category-item'><a href="[% roots.site %]/shop/randomgift">[% '.for.random' | ml %]</a></span>
+ </div>
+</div>
+
+<div class='shop-category'>
+ <div class='shop-category-title'>[% '.title.points' | ml(site=site.nameshort) %]</div>
+ <div class='shop-category-items'>
+[% IF remote AND remote.is_personal %]
+ <span class='shop-category-item'><a href="[% roots.site %]/shop/points?for=self">[% '.for.self' | ml %]</a> ([% remote.ljuser_display %])</span>
+ <span class='shop-category-item'><a href="[% roots.site %]/shop/points?for=gift">[% '.for.different' | ml %]</a></span>
+[% ELSE %]
+ <span class='shop-category-item'>[% '.points.login' | ml %]</span>
+[% END %]
+ </div>
+</div>
+
+
+[%#
+
+Here for future expansion ...
+
+<div class='shop-category'>
+ <div class='shop-category-title'>Virtual Gifts for ...</div>
+ <div class='shop-category-items'>
+ <span class='shop-category-item'><a href="...">yourself</a> (username)</span>
+ <span class='shop-category-item'><a href="...">your circle</a></span>
+ <span class='shop-category-item'><a href="...">a random user</a></span>
+ </div>
+</div>
+
+
+<div class='shop-category'>
+ <div class='shop-category-title'>Rename Tokens for ...</div>
+ <div class='shop-category-items'>
+ <span class='shop-category-item'><a href="...">yourself</a> (username)</span>
+ <span class='shop-category-item'><a href="...">your circle</a></span>
+ </div>
+</div>
+%]
diff -r edaf4b8bc572 -r e0b4a1757c52 views/shop/index.tt.text
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/views/shop/index.tt.text Tue Apr 06 03:06:20 2010 +0000
@@ -0,0 +1,21 @@
+.about=This is the shop where we sell things to help pay for our expenses to keep the site running, develop new features, and fix bugs.
+
+.for.circle=someone in your circle
+
+.for.different=a different account
+
+.for.existing=an existing account
+
+.for.new=a new account
+
+.for.random=a random user
+
+.for.self=yourself
+
+.points.login=You must log in to a personal account to buy points.
+
+.title=[[sitename]] Shop
+
+.title.paidacc=Buy a Paid Account for...
+
+.title.points=Buy [[site]] Points for...
diff -r edaf4b8bc572 -r e0b4a1757c52 views/shop/points.tt
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/views/shop/points.tt Tue Apr 06 03:06:20 2010 +0000
@@ -0,0 +1,36 @@
+[%- sections.title = '.title' | ml(sitename = site.nameshort) -%]
+
+[% cart_display %]
+
+<p>[% '.about' | ml(sitename = site.nameshort) %]</p>
+
+<form method='post'>
+<table class='shop-table-gift'>
+[% IF foru %]
+ <tr><td>[% '.buying.for' | ml %]</td><td>[% foru.ljuser_display %]
+ <input type='hidden' name='foruser' value='[% foru.user %]' />
+ </td></tr>
+[% ELSE %]
+ <tr><td>[% '.buying.for' | ml %]</td><td><input type='text' name='foruser' maxlength='25' size='15' />
+ [% IF errs.foruser %]<br /><strong>[% errs.foruser %][% END %]
+ </td></tr>
+[% END %]
+<tr><td>[% '.buying.points' | ml %]</td><td><input type='text' name='points' id='points' maxlength='4' size='10' value='[% points %]' />
+ [% '.buying.points.range' | ml %]
+ [% IF errs.points %]<br /><strong>[% errs.points %][% END %]
+</td></tr>
+<tr><td><span id='points-cost'></span></td><td><input type='submit' value='[% '.addtocart' | ml %]' /></td></tr>
+</table>
+</form>
+
+<p id='points-about'>[% '.about2' | ml %]</p>
+
+[%# FIXME: move this to shop.js or something %]
+<script type='text/javascript'>
+ DW.whenPageLoaded( function() {
+ setInterval(
+ function() {
+ $('#points-cost').html( 'Cost: <strong>$' + ($('#points').val() / 10).toFixed(2) + ' USD</strong>' );
+ }, 250 );
+ } );
+</script>
diff -r edaf4b8bc572 -r e0b4a1757c52 views/shop/points.tt.text
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/views/shop/points.tt.text Tue Apr 06 03:06:20 2010 +0000
@@ -0,0 +1,13 @@
+.about=This page allows you to buy [[sitename]] Points, the on-site currency accepted in our store and elsewhere on the site.
+
+.about2=Points cost 10 cents ($0.10 USD) each and can be used in place of cash in the site store. For example, you can purchase paid time for yourself or someone else using points without needing to mail in a check or use a credit card.
+
+.addtocart=Add To Cart
+
+.buying.for=Buying for Account:
+
+.buying.points=Points to Purchase:
+
+.buying.points.range=(30 to 5,000)
+
+.title=[[sitename]] Points Shop
--------------------------------------------------------------------------------

no subject
Yay Mark!