Diff -- /var/www/vhosts/3dshawn.com/ptmatrix.3dshawn.com/admin_billing.cgi
Diff
/var/www/vhosts/3dshawn.com/ptmatrix.3dshawn.com/admin_billing.cgi
added on local at 2026-07-01 12:33:53
Added
+227
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to 96e9596cea5b
to 96e9596cea5b
Unified diff
Naive LCS-based line diff. Additions in emerald, deletions in rose. Both sides decompressed live from BLOB_STORE.| 1 | #!/usr/bin/perl | |
| 2 | ||
| 3 | ################################################################################################################################## | |
| 4 | # ______ ____ ____ ______ ______ ____ ___ __ ___ ______ _ __ ____ ____ __ __ _ __ _____ ____ | |
| 5 | # / ____// __ \ / __ \ / ____/ / ____// __ \ / | / |/ // ____/| | / // __ \ / __ \ / //_/ | | / /|__ / / __ \ | |
| 6 | # / / / / / // / / // __/ / /_ / /_/ // /| | / /|_/ // __/ | | /| / // / / // /_/ // ,< | | / / /_ < / / / / | |
| 7 | # / /___ / /_/ // /_/ // /___ / __/ / _, _// ___ | / / / // /___ | |/ |/ // /_/ // _, _// /| | | |/ / ___/ /_ / /_/ / | |
| 8 | # \____/ \____//_____//_____/ /_/ /_/ |_|/_/ |_|/_/ /_//_____/ |__/|__/ \____//_/ |_|/_/ |_| |___/ /____/(_)\____/ | |
| 9 | # | |
| 10 | # - Written by: Shawn Pickering | |
| 11 | # - shawn@shawnpickering.com | |
| 12 | ################################################################################################################################## | |
| 13 | use strict; | |
| 14 | use lib '/var/www/vhosts/3dshawn.com/ptmatrix.3dshawn.com'; | |
| 15 | ||
| 16 | ||
| 17 | ############################################################# | |
| 18 | ## Variables | |
| 19 | ############################################################# | |
| 20 | ||
| 21 | ||
| 22 | ||
| 23 | ############################################################ | |
| 24 | # Modules | |
| 25 | ############################################################ | |
| 26 | use CGI; my $query = new CGI; my $form = $query->Vars; | |
| 27 | use MODS::Login; my $login = new MODS::Login; | |
| 28 | use MODS::PTMatrix::Urls; my $getlist = new MODS::PTMatrix::Urls; my $url = $getlist->urls(); | |
| 29 | use MODS::Template; my $tfile = new MODS::Template; | |
| 30 | use MODS::PTMatrix::Wrapper; my $load = new MODS::PTMatrix::Wrapper; | |
| 31 | use MODS::DBConnect; my $db = new MODS::DBConnect; | |
| 32 | use MODS::PTMatrix::Config; my $config = new MODS::PTMatrix::Config; my $database_name = $config->settings('database_name'); | |
| 33 | use MODS::PTMatrix::PM; | |
| 34 | ||
| 35 | ||
| 36 | ||
| 37 | ############################################################# | |
| 38 | ## Get form data | |
| 39 | ############################################################# | |
| 40 | my %rawform = %$form; | |
| 41 | ||
| 42 | foreach my $line(keys %$form){ | |
| 43 | $form->{$line} =~ s/[^!-~\s]//g; | |
| 44 | $form->{$line} = quotemeta("$form->{$line}"); | |
| 45 | } | |
| 46 | ||
| 47 | ||
| 48 | ||
| 49 | ############################################################ | |
| 50 | # Program Controls | |
| 51 | ############################################################ | |
| 52 | $|=1; | |
| 53 | my $userinfo = $login->login_verify(); | |
| 54 | print qq~Content-type:text/html; Cache-Control:no-cache;\n\n~; | |
| 55 | if(!$userinfo){print qq~<script>window.location="$url->{login}";</script>~;} | |
| 56 | elsif(!($userinfo->{is_super_admin} || $userinfo->{_admin_is_super_admin})){print qq~<script>window.location="$url->{dashboard}";</script>~;} | |
| 57 | elsif(($ENV{REQUEST_METHOD}||'') eq 'POST'){&plans_action;} | |
| 58 | else{&plans_list;} | |
| 59 | ||
| 60 | ||
| 61 | ############################################################ | |
| 62 | # Subroutines | |
| 63 | ############################################################ | |
| 64 | #Convert "+ feature\n- excluded" textareas into the JSON shape billing_plans.features stores | |
| 65 | sub features_to_json{ | |
| 66 | my($included_text, $excluded_text) = @_; | |
| 67 | my @out; | |
| 68 | for my $line(split /\r?\n/, ($included_text || '')){ | |
| 69 | $line =~ s/^\s+|\s+$//g; | |
| 70 | next unless length $line; | |
| 71 | my $name = $line; | |
| 72 | $name =~ s/^[+]\s*//; | |
| 73 | $name =~ s/\\/\\\\/g; | |
| 74 | $name =~ s/"/\\"/g; | |
| 75 | push @out, qq~{"name":"$name","included":true}~; | |
| 76 | } | |
| 77 | for my $line(split /\r?\n/, ($excluded_text || '')){ | |
| 78 | $line =~ s/^\s+|\s+$//g; | |
| 79 | next unless length $line; | |
| 80 | my $name = $line; | |
| 81 | $name =~ s/^[-x]\s*//; | |
| 82 | $name =~ s/\\/\\\\/g; | |
| 83 | $name =~ s/"/\\"/g; | |
| 84 | push @out, qq~{"name":"$name","included":false}~; | |
| 85 | } | |
| 86 | return '[' . join(',', @out) . ']'; | |
| 87 | } | |
| 88 | ||
| 89 | #Split the stored features JSON back into two newline-separated lists (for the edit form) | |
| 90 | sub json_to_features{ | |
| 91 | my($json) = @_; | |
| 92 | my (@inc, @exc); | |
| 93 | return ('','') unless $json && $json =~ /^\s*\[/; | |
| 94 | while($json =~ /"name"\s*:\s*"((?:[^"\\]|\\.)*)"\s*,\s*"included"\s*:\s*(true|false)/g){ | |
| 95 | my $name = $1; | |
| 96 | my $is_inc = ($2 eq 'true'); | |
| 97 | $name =~ s/\\"/"/g; | |
| 98 | $name =~ s/\\\\/\\/g; | |
| 99 | if($is_inc){push @inc, $name;} | |
| 100 | else {push @exc, $name;} | |
| 101 | } | |
| 102 | return (join("\n", @inc), join("\n", @exc)); | |
| 103 | } | |
| 104 | ||
| 105 | sub plans_action{ | |
| 106 | my $uid = MODS::PTMatrix::PM::get_user_id($userinfo); | |
| 107 | my $act = $form->{act} || ''; | |
| 108 | my $flash = ''; | |
| 109 | ||
| 110 | my $dbh = $db->db_connect(); | |
| 111 | ||
| 112 | if($act eq 'save_plan'){ | |
| 113 | my $id = MODS::PTMatrix::PM::safe_int($form->{id}); | |
| 114 | my $tier = $form->{tier} || ''; | |
| 115 | $tier = '' unless $tier =~ /^(trial|free|starter|pro|business|enterprise)$/; | |
| 116 | ||
| 117 | my $name = MODS::PTMatrix::PM::sql_quote(substr($rawform{display_name} || '', 0, 80)); | |
| 118 | my $tagline = MODS::PTMatrix::PM::sql_quote(substr($rawform{tagline} || '', 0, 200)); | |
| 119 | my $cta_label = MODS::PTMatrix::PM::sql_quote(substr($rawform{cta_label} || 'Start free trial', 0, 60)); | |
| 120 | my $price_m = MODS::PTMatrix::PM::safe_int($form->{price_dollars_monthly}) * 100; | |
| 121 | my $price_y = MODS::PTMatrix::PM::safe_int($form->{price_dollars_yearly}) * 100; | |
| 122 | my $max_users = $form->{max_users} =~ /^\d+$/ ? MODS::PTMatrix::PM::safe_int($form->{max_users}) : 'NULL'; | |
| 123 | my $max_proj = $form->{max_projects} =~ /^\d+$/ ? MODS::PTMatrix::PM::safe_int($form->{max_projects}) : 'NULL'; | |
| 124 | my $max_gb = $form->{max_storage_gb} =~ /^\d+$/ ? MODS::PTMatrix::PM::safe_int($form->{max_storage_gb}) : 'NULL'; | |
| 125 | my $sort_order = MODS::PTMatrix::PM::safe_int($form->{sort_order}); | |
| 126 | my $is_active = $form->{is_active} ? 1 : 0; | |
| 127 | my $is_featured = $form->{is_featured} ? 1 : 0; | |
| 128 | my $stripe_m = MODS::PTMatrix::PM::sql_quote(substr($rawform{stripe_price_id} || '', 0, 80)); | |
| 129 | my $stripe_y = MODS::PTMatrix::PM::sql_quote(substr($rawform{stripe_yearly_price_id} || '', 0, 80)); | |
| 130 | ||
| 131 | my $features_json = &features_to_json($rawform{features_included}, $rawform{features_excluded}); | |
| 132 | my $features_q = MODS::PTMatrix::PM::sql_quote($features_json); | |
| 133 | ||
| 134 | my $set_max_users = ($max_users eq 'NULL') ? "max_users=NULL" : "max_users='$max_users'"; | |
| 135 | my $set_max_proj = ($max_proj eq 'NULL') ? "max_projects=NULL" : "max_projects='$max_proj'"; | |
| 136 | my $set_max_gb = ($max_gb eq 'NULL') ? "max_storage_gb=NULL" : "max_storage_gb='$max_gb'"; | |
| 137 | ||
| 138 | if($id){ | |
| 139 | my $sql = qq~UPDATE `${database_name}`.billing_plans SET tier='$tier', display_name='$name', tagline='$tagline', cta_label='$cta_label', price_cents_per_seat='$price_m', yearly_price_cents_per_seat='$price_y', $set_max_users, $set_max_proj, $set_max_gb, features='$features_q', is_active='$is_active', is_featured='$is_featured', sort_order='$sort_order', stripe_price_id='$stripe_m', stripe_yearly_price_id='$stripe_y' WHERE id='$id'~; | |
| 140 | $db->db_readwrite($dbh, $sql, $ENV{SCRIPT_NAME}, __LINE__); | |
| 141 | MODS::PTMatrix::PM::audit(0, $uid, 'admin.plan_edit', 'plan', $id, $tier); | |
| 142 | $flash = "Plan <strong>$tier</strong> saved."; | |
| 143 | }elsif($tier && length($rawform{display_name})){ | |
| 144 | my $val_users = ($max_users eq 'NULL') ? 'NULL' : "'$max_users'"; | |
| 145 | my $val_proj = ($max_proj eq 'NULL') ? 'NULL' : "'$max_proj'"; | |
| 146 | my $val_gb = ($max_gb eq 'NULL') ? 'NULL' : "'$max_gb'"; | |
| 147 | my $sql = qq~INSERT INTO `${database_name}`.billing_plans (tier, display_name, tagline, cta_label, price_cents_per_seat, yearly_price_cents_per_seat, max_users, max_projects, max_storage_gb, features, is_active, is_featured, sort_order, stripe_price_id, stripe_yearly_price_id, created_at) VALUES ('$tier','$name','$tagline','$cta_label','$price_m','$price_y', $val_users, $val_proj, $val_gb, '$features_q','$is_active','$is_featured','$sort_order','$stripe_m','$stripe_y', NOW())~; | |
| 148 | $db->db_readwrite($dbh, $sql, $ENV{SCRIPT_NAME}, __LINE__); | |
| 149 | MODS::PTMatrix::PM::audit(0, $uid, 'admin.plan_create', 'plan', 0, $tier); | |
| 150 | $flash = "Plan <strong>$tier</strong> created."; | |
| 151 | }else{ | |
| 152 | $flash = 'Missing tier or display name.'; | |
| 153 | } | |
| 154 | }elsif($act eq 'toggle_active'){ | |
| 155 | my $id = MODS::PTMatrix::PM::safe_int($form->{id}); | |
| 156 | $db->db_readwrite($dbh, qq~UPDATE `${database_name}`.billing_plans SET is_active = 1 - is_active WHERE id='$id'~, $ENV{SCRIPT_NAME}, __LINE__); | |
| 157 | $flash = 'Plan visibility toggled.'; | |
| 158 | }elsif($act eq 'delete_plan'){ | |
| 159 | my $id = MODS::PTMatrix::PM::safe_int($form->{id}); | |
| 160 | ||
| 161 | #Don't allow deleting a plan any company is still on | |
| 162 | my $in_use = $db->db_readwrite($dbh, qq~SELECT COUNT(*) AS n FROM `${database_name}`.companies WHERE plan_tier = (SELECT tier FROM `${database_name}`.billing_plans WHERE id='$id')~, $ENV{SCRIPT_NAME}, __LINE__); | |
| 163 | if($in_use && $in_use->{n}){ | |
| 164 | $flash = "Can't delete: $in_use->{n} compan(y/ies) still on this tier."; | |
| 165 | }else{ | |
| 166 | $db->db_readwrite($dbh, qq~DELETE FROM `${database_name}`.billing_plans WHERE id='$id'~, $ENV{SCRIPT_NAME}, __LINE__); | |
| 167 | MODS::PTMatrix::PM::audit(0, $uid, 'admin.plan_delete', 'plan', $id); | |
| 168 | $flash = 'Plan deleted.'; | |
| 169 | } | |
| 170 | } | |
| 171 | ||
| 172 | $db->db_disconnect($dbh); | |
| 173 | &plans_list($flash); | |
| 174 | } | |
| 175 | ||
| 176 | sub plans_list{ | |
| 177 | my($flash) = @_; | |
| 178 | $flash ||= ''; | |
| 179 | ||
| 180 | my $dbh = $db->db_connect(); | |
| 181 | ||
| 182 | #------------------------------------------------------------------------------------------------------ | |
| 183 | #All plans + how many companies are on each | |
| 184 | my $sql = qq~SELECT id, tier, display_name, tagline, cta_label, price_cents_per_seat, yearly_price_cents_per_seat, max_users, max_projects, max_storage_gb, features, is_active, is_featured, sort_order, stripe_price_id, stripe_yearly_price_id, (SELECT COUNT(*) FROM `${database_name}`.companies c WHERE c.plan_tier=billing_plans.tier) AS company_count FROM `${database_name}`.billing_plans ORDER BY sort_order, id~; | |
| 185 | my @plans = $db->db_readwrite_multiple($dbh, $sql, $ENV{SCRIPT_NAME}, __LINE__); | |
| 186 | #------------------------------------------------------------------------------------------------------ | |
| 187 | ||
| 188 | $db->db_disconnect($dbh); | |
| 189 | ||
| 190 | foreach my $p(@plans){ | |
| 191 | $p->{price_dollars_m} = int(($p->{price_cents_per_seat} || 0) / 100); | |
| 192 | $p->{price_dollars_y} = int(($p->{yearly_price_cents_per_seat} || 0) / 100); | |
| 193 | my ($inc, $exc) = &json_to_features($p->{features}); | |
| 194 | $p->{features_included} = MODS::PTMatrix::PM::h($inc); | |
| 195 | $p->{features_excluded} = MODS::PTMatrix::PM::h($exc); | |
| 196 | $p->{display_name} = MODS::PTMatrix::PM::h($p->{display_name}); | |
| 197 | $p->{tagline} = MODS::PTMatrix::PM::h($p->{tagline} || ''); | |
| 198 | $p->{cta_label} = MODS::PTMatrix::PM::h($p->{cta_label}); | |
| 199 | $p->{stripe_price_id} ||= ''; | |
| 200 | $p->{stripe_yearly_price_id} ||= ''; | |
| 201 | $p->{max_users_display} = defined $p->{max_users} ? $p->{max_users} : ''; | |
| 202 | $p->{max_projects_display} = defined $p->{max_projects} ? $p->{max_projects} : ''; | |
| 203 | $p->{max_storage_display} = defined $p->{max_storage_gb} ? $p->{max_storage_gb} : ''; | |
| 204 | $p->{is_active_attr} = $p->{is_active} ? 'checked' : ''; | |
| 205 | $p->{is_featured_attr} = $p->{is_featured} ? 'checked' : ''; | |
| 206 | for my $t(qw(trial free starter pro business enterprise)){ | |
| 207 | $p->{"sel_$t"} = ($p->{tier} eq $t) ? 'selected' : ''; | |
| 208 | } | |
| 209 | $p->{toggle_label} = $p->{is_active} ? 'Hide' : 'Show'; | |
| 210 | } | |
| 211 | ||
| 212 | #Create all template variables | |
| 213 | my $tvars; | |
| 214 | $tvars->{flash} = $flash; | |
| 215 | $tvars->{has_flash} = length($flash) ? 1 : 0; | |
| 216 | $tvars->{plans} = \@plans; | |
| 217 | $tvars->{has_plans} = scalar(@plans) ? 1 : 0; | |
| 218 | ||
| 219 | my $body = join('', $tfile->template('tf_admin_billing.html', $tvars, $userinfo)); | |
| 220 | ||
| 221 | $load->render({ | |
| 222 | userinfo => $userinfo, | |
| 223 | page_key => 'admin_billing', | |
| 224 | title => 'Platform Billing', | |
| 225 | body => $body, | |
| 226 | }); | |
| 227 | } |