fu: Close-up of Fu, bringing a scoop of water to her mouth (Default)
fu ([personal profile] fu) wrote in [site community profile] changelog2010-10-14 05:13 am

[dw-free] allow promo codes to have attached paid time

[commit: http://hg.dwscoalition.org/dw-free/rev/53fc4397240f]

http://bugs.dwscoalition.org/show_bug.cgi?id=3128

Allow promo invite codes to come preloaded with paid time.

Patch by [personal profile] exor674.

Files modified:
  • bin/upgrading/update-db-general.pl
  • cgi-bin/DW/InviteCodes.pm
  • cgi-bin/DW/InviteCodes/Promo.pm
  • cgi-bin/LJ/User.pm
  • cgi-bin/LJ/Widget/CreateAccount.pm
  • cgi-bin/LJ/Widget/CreateAccountProgressMeter.pm
  • htdocs/admin/invites/promo.bml
  • htdocs/admin/invites/promo.bml.text
--------------------------------------------------------------------------------
diff -r 88ea0f26d63f -r 53fc4397240f bin/upgrading/update-db-general.pl
--- a/bin/upgrading/update-db-general.pl	Thu Oct 14 11:47:13 2010 +0800
+++ b/bin/upgrading/update-db-general.pl	Thu Oct 14 13:12:05 2010 +0800
@@ -2908,6 +2908,8 @@ CREATE TABLE acctcode_promo (
     current_count int(10) unsigned not null default 0,
     active enum('1','0') not null default 1,
     suggest_journalid int unsigned,
+    paid_class varchar(100),
+    paid_months tinyint unsigned,
 
     PRIMARY KEY ( code )
 )
@@ -3854,6 +3856,14 @@ EOF
         do_alter( 'externalaccount',
                   "ALTER TABLE externalaccount ADD COLUMN options blob");
     }
+
+    unless ( column_type( 'acctcode_promo', 'paid_class' ) ) {
+        do_alter( 'acctcode_promo', "ALTER TABLE acctcode_promo ADD COLUMN paid_class varchar(10000)" );
+    }
+
+    unless ( column_type( 'acctcode_promo', 'paid_months' ) ) {
+        do_alter( 'acctcode_promo', "ALTER TABLE acctcode_promo ADD COLUMN paid_months tinyint unsigned" );
+    }
 });
 
 
diff -r 88ea0f26d63f -r 53fc4397240f cgi-bin/DW/InviteCodes.pm
--- a/cgi-bin/DW/InviteCodes.pm	Thu Oct 14 11:47:13 2010 +0800
+++ b/cgi-bin/DW/InviteCodes.pm	Thu Oct 14 13:12:05 2010 +0800
@@ -67,6 +67,8 @@ use constant DIGITS => qw(A B C D E F G 
 use constant DIGITS => qw(A B C D E F G H J K L M N P Q R S T U V W X Y Z 2 3 4 5 6 7 8 9);
 use constant { CODE_LEN => AUTH_LEN + ACID_LEN, DIGITS_LEN => scalar(DIGITS) };
 
+use DW::InviteCodes::Promo;
+
 =head1 API
 
 =head2 C<< $class->generate( [ count => $howmany, ] owner => $forwho, reason => $why >>
@@ -129,75 +131,6 @@ sub could_be_code {
     return 1;
 }
 
-=head2 C<< $class->is_promo_code( code => $code ) >>
-
-Returns if the given code is a promo code or not.
-
-=cut
-
-sub is_promo_code {
-    my ( $class, %opts ) = @_;
-
-    my $promo_code_info = $class->get_promo_code_info( code => $opts{code} );
-
-    return ref $promo_code_info ? 1 : 0;
-}
-
-=head2 C<< $class->get_promo_code_info( code => $code ) >>
-
-Return the info for this promo code in a hashref.
-
-=cut
-
-sub get_promo_code_info {
-    my ( $class, %opts ) = @_;
-    my $dbh = LJ::get_db_writer();
-    my $code = $opts{code};
-
-    return undef unless $code && $code =~ /^[a-z0-9]+$/i; # make sure the code is valid first
-    return $dbh->selectrow_hashref( "SELECT * FROM acctcode_promo WHERE code = ?", undef, $code );
-}
-
-=head2 C<< $class->get_promo_codes( state => $state ) >>
-
-Return the list of promo codes, optionally filtering by state.
-State can be:
-  * active ( active promo codes )
-  * inactive ( inactive promo codes )
-  * unused ( unused promo codes )
-  * noneleft ( no uses left )
-  * all ( all promo codes )
-
-=cut
-
-sub get_promo_codes {
-    my ( $class, %opts ) = @_;
-    my $dbh = LJ::get_db_writer();
-    my $state = $opts{state} || 'active';
-
-    my $sql = "SELECT * FROM acctcode_promo";
-    if ( $state eq 'all' ) {
-        # do nothing
-    } elsif ( $state eq 'active' ) {
-        $sql .= " WHERE active = '1' AND current_count < max_count";
-    } elsif ( $state eq 'inactive' ) {
-        $sql .= " WHERE active = '0' OR current_count >= max_count";
-    } elsif ( $state eq 'unused' ) {
-        $sql .= " WHERE current_count = 0"
-    } elsif ( $state eq 'noneleft' ) {
-        $sql .= " WHERE current_count >= max_count";
-    }
-
-    my $sth = $dbh->prepare( $sql ) or die $dbh->errstr;
-    $sth->execute() or die $dbh->errstr;
-
-    my @out;
-    while ( my $row = $sth->fetchrow_hashref ) {
-        push @out, $row;
-    }
-    return \@out;
-}
-
 =head2 C<< $class->check_code( code => $invite [, userid => $recipient] ) >>
 
 Checks whether code $invite is valid before trying to create an account. Takes
@@ -213,7 +146,7 @@ sub check_code {
 
     # check if this code is a promo code first
     # if it is, make sure it's active and we're not over the creation limit for the code
-    my $promo_code_info = $class->get_promo_code_info( code => $code );
+    my $promo_code_info = DW::InviteCodes::Promo->load( code => $code );
     if ( ref $promo_code_info ) {
         return 0 unless $promo_code_info->{active} && ( $promo_code_info->{current_count} < $promo_code_info->{max_count} );
         return 1;
@@ -277,24 +210,6 @@ sub use_code {
         undef, $opts{user}->{userid}, $self->{acid} );
 
     return 1; # 1 means success? Needs error return in that case.
-}
-
-=head2 C<< $class->use_promo_code >>
-
-Increments the current_count on the given promo code.
-
-=cut
-
-sub use_promo_code {
-    my ( $class, %opts ) = @_;
-    my $dbh = LJ::get_db_writer();
-    my $code = $opts{code};
-
-    return 0 unless $class->is_promo_code( code => $code );
-
-    $dbh->do( "UPDATE acctcode_promo SET current_count = current_count + 1 WHERE code = ?", undef, $code );
-
-    return 1;
 }
 
 =head2 C<< $object->send_code ( [ email => $email ] ) >>
diff -r 88ea0f26d63f -r 53fc4397240f cgi-bin/DW/InviteCodes/Promo.pm
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cgi-bin/DW/InviteCodes/Promo.pm	Thu Oct 14 13:12:05 2010 +0800
@@ -0,0 +1,187 @@
+#!/usr/bin/perl
+#
+# DW::InviteCodes::Promo - Represents a promotional invite code
+#
+# Authors:
+#      Andrea Nall <anall@andreanall.com>
+#
+# 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::InviteCodes::Promo;
+
+=head1 NAME
+
+DW::InviteCodes::Promo - Represents a promotional invite code
+
+=cut
+
+use strict;
+
+sub _from_row {
+    my ( $class, $row ) = @_;
+    return bless $row, $class;
+}
+
+=head1 CLASS METHODS
+
+=head2 C<< DW::InviteCodes::Promo->load( code => $code ); >>
+
+Gets a DW::InviteCode::Promo objct
+
+=cut
+# FIXME: Consider process caching and/or memcache, if this is a busy enough path
+sub load {
+    my ( $class, %opts ) = @_;
+    my $dbh = LJ::get_db_writer();
+    my $code = $opts{code};
+
+    return undef unless $code && $code =~ /^[a-z0-9]+$/i; # make sure the code is valid first
+    my $data = $dbh->selectrow_hashref( "SELECT * FROM acctcode_promo WHERE code = ?", undef, $code );
+    return undef unless $data;
+
+    return $class->_from_row( $data );
+}
+
+=head2 C<< DW::InviteCodes::Promo->load_bulk( state => $state ); >>
+
+Return the list of promo codes, optionally filtering by state.
+State can be:
+  * active ( active promo codes )
+  * inactive ( inactive promo codes )
+  * unused ( unused promo codes )
+  * noneleft ( no uses left )
+  * all ( all promo codes )
+
+=cut
+sub load_bulk {
+    my ( $class, %opts ) = @_;
+    my $dbh = LJ::get_db_writer();
+    my $state = $opts{state} || 'active';
+
+    my $sql = "SELECT * FROM acctcode_promo";
+    if ( $state eq 'all' ) {
+        # do nothing
+    } elsif ( $state eq 'active' ) {
+        $sql .= " WHERE active = '1' AND current_count < max_count";
+    } elsif ( $state eq 'inactive' ) {
+        $sql .= " WHERE active = '0' OR current_count >= max_count";
+    } elsif ( $state eq 'unused' ) {
+        $sql .= " WHERE current_count = 0"
+    } elsif ( $state eq 'noneleft' ) {
+        $sql .= " WHERE current_count >= max_count";
+    }
+
+    my $sth = $dbh->prepare( $sql ) or die $dbh->errstr;
+    $sth->execute() or die $dbh->errstr;
+
+    my @out;
+    while ( my $row = $sth->fetchrow_hashref ) {
+        push @out, $class->_from_row( $row );
+    }
+    return \@out;
+}
+
+=head2 C<< $class->is_promo_code( code => $code ) >>
+
+Returns if the given code is a promo code or not.
+
+=cut
+sub is_promo_code {
+    my ( $class, %opts ) = @_;
+
+    my $promo_code_info = $class->load( %opts );
+
+    return ref $promo_code_info ? 1 : 0;
+}
+
+=head1 INSTANCE METHODS
+
+=head2 C<< $self->apply_for_user( $u ) >>
+
+Handle any post-create operations for this user.
+
+=cut
+sub apply_for_user {
+    my ( $self, $u ) = @_;
+
+    my $code = $self->code;
+    my $paid_type = $self->paid_class;
+    my $paid_months = $self->paid_months;
+
+    LJ::statushistory_add( $u, undef, 'create_from_promo', "Created new account from promo code '$code'." );
+
+    if ( defined $paid_type ) {
+        if ( DW::Pay::add_paid_time( $u, $paid_type, $paid_months ) ) {
+            LJ::statushistory_add( $u, undef, 'paid_from_promo', "Created new '$paid_type' account from promo code '$code'." );
+        }
+    }
+}
+
+=head2 C<< $self->code >>
+
+=cut
+sub code {
+    return $_[0]->{code};
+}
+
+=head2 C<< $self->paid_class_name >>
+
+Return the display name of this account class.
+
+=cut
+sub paid_class_name {
+    my $self = $_[0];
+
+    foreach my $cap ( keys %LJ::CAP ) {
+        return $LJ::CAP{$cap}->{_visible_name}
+            if $LJ::CAP{$cap} && $LJ::CAP{$cap}->{_account_type} eq $self->paid_class;
+    }
+
+    return 'Invalid Account Class';
+}
+
+=head2 C<< $self->paid_months >>
+
+=cut
+sub paid_months {
+    return $_[0]->{paid_class} ? $_[0]->{paid_months} : 0;
+}
+
+=head2 C<< $self->paid_type >>
+
+=cut
+sub paid_class {
+    return $_[0]->{paid_class};
+}
+
+=head2 C<< $self->suggest_journal
+
+Return the LJ::User to suggest
+
+=cut
+sub suggest_journal {
+    my $id = $_[0]->{suggest_journalid};
+    return $id ? LJ::load_userid( $id ) : undef;
+}
+
+=head2 C<< $self->use_code >>
+
+Increments the current_count on the given promo code.
+
+=cut
+sub use_code {
+    my ( $self ) = @_;
+    my $dbh = LJ::get_db_writer();
+
+    my $code = $self->code;
+
+    $dbh->do( "UPDATE acctcode_promo SET current_count = current_count + 1 WHERE code = ?", undef, $code );
+
+    return 1;
+}
+
+1;
diff -r 88ea0f26d63f -r 53fc4397240f cgi-bin/LJ/User.pm
--- a/cgi-bin/LJ/User.pm	Thu Oct 14 11:47:13 2010 +0800
+++ b/cgi-bin/LJ/User.pm	Thu Oct 14 13:12:05 2010 +0800
@@ -42,6 +42,7 @@ use DW::Pay;
 use DW::Pay;
 use DW::User::ContentFilters;
 use DW::User::Edges;
+use DW::InviteCodes::Promo;
 
 use LJ::Community;
 use LJ::Subscription;
@@ -250,8 +251,9 @@ sub create_personal {
     if ( $LJ::USE_ACCT_CODES && $opts{code} ) {
         my $code = $opts{code};
         my $itemidref;
-        if ( DW::InviteCodes->is_promo_code( code => $code ) ) {
-            LJ::statushistory_add( $u, undef, 'create_from_promo', "Created new account from promo code '$code'." );
+        my $promo_code = DW::InviteCodes::Promo->load( code => $code );
+        if ( $promo_code ) {
+            $promo_code->apply_for_user( $u );
         } elsif ( my $cart = DW::Shop::Cart->get_from_invite( $code, itemidref => \$itemidref ) ) {
             my $item = $cart->get_item( $itemidref );
             if ( $item && $item->isa( 'DW::Shop::Item::Account' ) ) {
diff -r 88ea0f26d63f -r 53fc4397240f cgi-bin/LJ/Widget/CreateAccount.pm
--- a/cgi-bin/LJ/Widget/CreateAccount.pm	Thu Oct 14 11:47:13 2010 +0800
+++ b/cgi-bin/LJ/Widget/CreateAccount.pm	Thu Oct 14 13:12:05 2010 +0800
@@ -251,16 +251,24 @@ sub render_body {
     $ret .= "</label>";
     $ret .= "</td></tr>\n";
 
-    if ( $LJ::USE_ACCT_CODES && !DW::InviteCodes->is_promo_code( code => $code ) ) {
-        my $item = DW::InviteCodes->paid_status( code => $code );
-        if ( $item ) {
-            $ret .= "<tr valign='top'><td class='field-name'>&nbsp;</td>\n<td>";
-            if ( $item->permanent ) {
-                $ret .= $class->ml( 'widget.createaccount.field.paidaccount.permanent', { type => "<strong>" . $item->class_name . "</strong>" } );
-            } else {
-                $ret .= $class->ml( 'widget.createaccount.field.paidaccount', { type => "<strong>" . $item->class_name . "</strong>", nummonths => $item->months } );
+    if ( $LJ::USE_ACCT_CODES ) {
+        if ( my $pc = DW::InviteCodes::Promo->load( code => $code ) ) {
+            if ( $pc->paid_class ) {
+                $ret .= "<tr valign='top'><td class='field-name'>&nbsp;</td>\n<td>";
+                $ret .= $class->ml( 'widget.createaccount.field.paidaccount', { type => "<strong>" . $pc->paid_class_name . "</strong>", nummonths => $pc->paid_months } );
+                $ret .= "</td></tr>";
+            } 
+        } else {
+            my $item = DW::InviteCodes->paid_status( code => $code );
+            if ( $item ) {
+                $ret .= "<tr valign='top'><td class='field-name'>&nbsp;</td>\n<td>";
+                if ( $item->permanent ) {
+                    $ret .= $class->ml( 'widget.createaccount.field.paidaccount.permanent', { type => "<strong>" . $item->class_name . "</strong>" } );
+                } else {
+                    $ret .= $class->ml( 'widget.createaccount.field.paidaccount', { type => "<strong>" . $item->class_name . "</strong>", nummonths => $item->months } );
+                }
+                $ret .= "</td></tr>";
             }
-            $ret .= "</td></tr>";
         }
     }
 
@@ -319,11 +327,9 @@ sub handle_post {
             $from_post{code_valid} = 1;
 
             # and if this is a community promo code, set the inviter
-            if ( my $pc = DW::InviteCodes->get_promo_code_info( code => $code ) ) {
-                if ( $pc->{suggest_journalid} ) {
-                    my $invu = LJ::load_userid( $pc->{suggest_journalid} );
-                    $post->{from} = $invu->user if $invu;
-                }
+            if ( my $pc = DW::InviteCodes::Promo->load( code => $code ) ) {
+                my $invu = $pc->suggest_journal;
+                $post->{from} = $invu->user if $invu;
             }
 
         } else {
@@ -453,8 +459,8 @@ sub handle_post {
 
         # we're all done; mark the invite code as used
         if ( $LJ::USE_ACCT_CODES && $code ) {
-            if ( DW::InviteCodes->is_promo_code( code => $code ) ) {
-                DW::InviteCodes->use_promo_code( code => $code );
+            if ( my $pc = DW::InviteCodes::Promo->load( code => $code ) ) {
+                $pc->use_code;
             } else {
                 my $invitecode = DW::InviteCodes->new( code => $code );
                 $invitecode->use_code( user => $nu );
diff -r 88ea0f26d63f -r 53fc4397240f cgi-bin/LJ/Widget/CreateAccountProgressMeter.pm
--- a/cgi-bin/LJ/Widget/CreateAccountProgressMeter.pm	Thu Oct 14 11:47:13 2010 +0800
+++ b/cgi-bin/LJ/Widget/CreateAccountProgressMeter.pm	Thu Oct 14 13:12:05 2010 +0800
@@ -27,7 +27,7 @@ sub render_body {
 
     my $given_step = $opts{step} || 1;
     my @steps_to_show = !LJ::is_enabled( 'payments' )
-                    || ( $LJ::USE_ACCT_CODES && $given_step == 1 && !DW::InviteCodes->is_promo_code( code => $opts{code} ) && DW::InviteCodes->paid_status( code => $opts{code} ) )
+                    || ( $LJ::USE_ACCT_CODES && $given_step == 1 && !DW::InviteCodes::Promo->is_promo_code( code => $opts{code} ) && DW::InviteCodes->paid_status( code => $opts{code} ) )
                     || ( $given_step > 1 && $u && $u->is_paid )
                     ? ( 1, 2, 4 ) : ( 1..4 );
 
diff -r 88ea0f26d63f -r 53fc4397240f htdocs/admin/invites/promo.bml
--- a/htdocs/admin/invites/promo.bml	Thu Oct 14 11:47:13 2010 +0800
+++ b/htdocs/admin/invites/promo.bml	Thu Oct 14 13:12:05 2010 +0800
@@ -15,7 +15,7 @@ body<=
 {
     use strict;
     use vars qw( %GET %POST $title );
-    use DW::InviteCodes;
+    use DW::InviteCodes::Promo;
 
     $title = $ML{'.title'};
 
@@ -68,6 +68,24 @@ body<=
         $ret .= LJ::html_text( { id => 'suggest_journal', name => 'suggest_journal', value => ( $suggest_u ? $suggest_u->username : ( $data->{suggest_journal} || "" ) ), size => 28, maxlength => 25  } );
         $ret .= "  <strong>[$ML{'.error.label'} " . join(', ', @{$errors->{suggest_journal}}) . "]</strong>" if $errors->{suggest_journal};
         $ret .= '<br />';
+        
+        $ret .= LJ::labelfy( 'paid_class', "$ML{'.field.paid_class.label'} "  );
+        $ret .= LJ::html_select( {
+                id => 'paid_class',
+                name => 'paid_class',
+                selected => ( $data->{paid_class} || '' ),
+            },
+            { value => '', text => $ML{'.field.paid_class.none'} },
+            { value => 'paid', text => $ML{'.field.paid_class.paid'} },
+            { value => 'premium', text => $ML{'.field.paid_class.premium'} },
+        );
+        $ret .= '<br />';
+        
+        $ret .= LJ::labelfy( 'paid_months', "$ML{'.field.paid_months.label'} "  );
+        $ret .= LJ::html_text( { id => 'paid_months', name => 'paid_months', value => ( $data->{paid_months} || "" ), size => 10, maxlength => 2 } );
+        $ret .= '<br />';
+
+        
         $ret .= LJ::html_submit( value => $ML{ ( $data ? '.btn.save' : '.btn.create' ) } );
         $ret .= "</form>";
     };
@@ -84,6 +102,8 @@ body<=
                 current_count => 0,
                 max_count => $POST{max_count} || 0,
                 suggest_journal => $POST{suggest_journal},
+                paid_class => $POST{paid_class} || '',
+                paid_months => $POST{paid_months} || undef,
             };
             my $valid = 1;
             my $errors = {};
@@ -93,7 +113,7 @@ body<=
             } elsif ( ! ( $code =~ /^[a-z0-9]+$/i ) ) {
                 push @{$errors->{code}}, $ML{'.error.code.invalid_character'};
                 $valid = 0;
-            } elsif ( DW::InviteCodes->is_promo_code( code => $code ) ) {
+            } elsif ( DW::InviteCodes::Promo->is_promo_code( code => $code ) ) {
                 push @{$errors->{code}}, $ML{'.error.code.exists'};
                 $valid = 0;
             }
@@ -111,10 +131,14 @@ body<=
             } else {
                 $data->{suggest_journal} = undef;
             }
+            if ( $data->{paid_class} !~ /^(paid|premium)$/ ) {
+                $data->{paid_class} = undef;
+                $data->{paid_months} = undef;
+            }
             if ( $valid ) {
                 my $dbh = LJ::get_db_writer();
-                $dbh->do( "INSERT INTO acctcode_promo (code, max_count, active, suggest_journalid) VALUES (?, ?, ?, ?)", undef,
-                            $data->{code}, $data->{max_count}, $data->{active}, $data->{suggest_journalid} ) or die $dbh->errstr;
+                $dbh->do( "INSERT INTO acctcode_promo (code, max_count, active, suggest_journalid, paid_class, paid_months) VALUES (?, ?, ?, ?, ?, ?)", undef,
+                            $data->{code}, $data->{max_count}, $data->{active}, $data->{suggest_journalid}, $data->{paid_class}, $data->{paid_months} ) or die $dbh->errstr;
             } else {
                 $create_form->( $data, $errors );
                 return $ret;
@@ -128,6 +152,8 @@ body<=
                 current_count => 0,
                 max_count => $POST{max_count} || 0,
                 suggest_journal => $POST{suggest_journal},
+                paid_class => $POST{paid_class} || '',
+                paid_months => $POST{paid_months} || undef,
             };
             my $valid = 1;
             my $errors = {};
@@ -135,7 +161,7 @@ body<=
             if ( ! $code ) {
                 push @{$errors->{code}}, $ML{'.error.code.missing'};
                 $valid = 0;
-            } elsif ( ! ref ( $info = DW::InviteCodes->get_promo_code_info( code => $code ) ) ) {
+            } elsif ( ! ref ( $info = DW::InviteCodes::Promo->load( code => $code ) ) ) {
                 push @{$errors->{code}}, $ML{'.error.code.invalid'};
                 $valid = 0;
             } else {
@@ -155,10 +181,14 @@ body<=
             } else {
                 $data->{suggest_journal} = undef;
             }
+            if ( $data->{paid_class} !~ /^(paid|premium)$/ ) {
+                $data->{paid_class} = undef;
+                $data->{paid_months} = undef;
+            }
             if ( $valid ) {
                 my $dbh = LJ::get_db_writer();
-                $dbh->do( "UPDATE acctcode_promo SET max_count = ?, active = ?, suggest_journalid = ? WHERE code = ?", undef,
-                            $data->{max_count}, $data->{active}, $data->{suggest_journalid}, $data->{code} ) or die $dbh->errstr;
+                $dbh->do( "UPDATE acctcode_promo SET max_count = ?, active = ?, suggest_journalid = ?, paid_class = ?, paid_months = ? WHERE code = ?", undef,
+                            $data->{max_count}, $data->{active}, $data->{suggest_journalid}, $data->{paid_class}, $data->{paid_months}, $data->{code} ) or die $dbh->errstr;
             } else {
                 $create_form->( $data, $errors );
                 return $ret;
@@ -169,10 +199,11 @@ body<=
 
     if ( $state eq 'create' ) {
         $create_form->( );
-    } elsif ( DW::InviteCodes->is_promo_code( code => $code ) ) {
-        $create_form->( DW::InviteCodes->get_promo_code_info( code => $code ) );
+    } elsif ( DW::InviteCodes::Promo->is_promo_code( code => $code ) ) {
+        $create_form->( DW::InviteCodes::Promo->load( code => $code ) );
     } else {
-        my $codes = DW::InviteCodes->get_promo_codes( state => $state );
+        # FIXME: Do not do hash accesses on this.
+        my $codes = DW::InviteCodes::Promo->load_bulk( state => $state );
 
         $ret .= '<a href="/admin/invites/promo?state=create">' . $ML{'.state.new'} . '</a> | ';
         $ret .= '<a href="/admin/invites/promo?state=all">' . $ML{'.state.unfiltered'} . '</a> | ';
@@ -182,7 +213,7 @@ body<=
         $ret .= '<a href="/admin/invites/promo?state=noneleft">' . $ML{'.state.noneleft'} . '</a>';
 
         $ret .= "<table>";
-        $ret .= "<tr><th>$ML{'.heading.code'}</th><th>$ML{'.heading.active'}</th><th>$ML{'.heading.count'}</th><th>$ML{'.heading.suggest'}</th></tr>";
+        $ret .= "<tr><th>$ML{'.heading.code'}</th><th>$ML{'.heading.active'}</th><th>$ML{'.heading.count'}</th><th>$ML{'.heading.suggest'}</th><th>$ML{'.heading.paid'}</tr>";
         
         if ( scalar( @$codes ) ) {
             foreach my $code (@$codes) {
@@ -192,6 +223,15 @@ body<=
                 $ret .= "<td>" . ( $code->{active} ? $ML{'.active.active'} : $ML{'.active.inactive'} ) . "</td>";
                 $ret .= "<td>" . $code->{current_count} . $ML{'.count.outof'} . $code->{max_count} . "</td>";
                 $ret .= "<td>" . ( $suggest_u ? $suggest_u->ljuser_display : $ML{'.suggest.none'} ) . "</td>";
+                if ( defined $code->{paid_class} ) {
+                    $ret .= "<td>" .
+                            BML::ml('.paid', {
+                                type => $code->{paid_class},
+                                months => $code->{paid_months}
+                            } ) . "</td>";
+                } else {
+                    $ret .= "<td>$ML{'.paid.no'}</td>";
+                }
                 $ret .= "</tr>";
             }
         } else {
diff -r 88ea0f26d63f -r 53fc4397240f htdocs/admin/invites/promo.bml.text
--- a/htdocs/admin/invites/promo.bml.text	Thu Oct 14 11:47:13 2010 +0800
+++ b/htdocs/admin/invites/promo.bml.text	Thu Oct 14 13:12:05 2010 +0800
@@ -28,6 +28,16 @@
 
 .field.count.label=Count:
 
+.field.paid_class.label=Paid Account:
+
+.field.paid_class.none=(No Paid Time)
+
+.field.paid_class.paid=Paid
+
+.field.paid_class.premium=Premium Paid
+
+.field.paid_months.label=Months:
+
 .field.suggest_journal.label=Suggest Journal:
 
 .heading.active=Active
@@ -36,9 +46,15 @@
 
 .heading.count=Count
 
+.heading.paid=Paid Time
+
 .heading.suggest=Suggest Journal
 
 .nomatch=No promo codes match your criteria
+
+.paid=[[type]] for [[months]] [[?months|month|months]]
+
+.paid.no=(none)
 
 .return=Return to list
 
--------------------------------------------------------------------------------