Diff -- /var/www/vhosts/3dshawn.com/ptmatrix.3dshawn.com/admin_billing.cgi

O Operator
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
Restore this content →
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##################################################################################################################################
13use strict;
14use lib '/var/www/vhosts/3dshawn.com/ptmatrix.3dshawn.com';
15
16
17#############################################################
18## Variables
19#############################################################
20
21
22
23############################################################
24# Modules
25############################################################
26use CGI; my $query = new CGI; my $form = $query->Vars;
27use MODS::Login; my $login = new MODS::Login;
28use MODS::PTMatrix::Urls; my $getlist = new MODS::PTMatrix::Urls; my $url = $getlist->urls();
29use MODS::Template; my $tfile = new MODS::Template;
30use MODS::PTMatrix::Wrapper; my $load = new MODS::PTMatrix::Wrapper;
31use MODS::DBConnect; my $db = new MODS::DBConnect;
32use MODS::PTMatrix::Config; my $config = new MODS::PTMatrix::Config; my $database_name = $config->settings('database_name');
33use MODS::PTMatrix::PM;
34
35
36
37#############################################################
38## Get form data
39#############################################################
40my %rawform = %$form;
41
42foreach 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;
53my $userinfo = $login->login_verify();
54print qq~Content-type:text/html; Cache-Control:no-cache;\n\n~;
55if(!$userinfo){print qq~<script>window.location="$url->{login}";</script>~;}
56elsif(!($userinfo->{is_super_admin} || $userinfo->{_admin_is_super_admin})){print qq~<script>window.location="$url->{dashboard}";</script>~;}
57elsif(($ENV{REQUEST_METHOD}||'') eq 'POST'){&plans_action;}
58else{&plans_list;}
59
60
61############################################################
62# Subroutines
63############################################################
64#Convert "+ feature\n- excluded" textareas into the JSON shape billing_plans.features stores
65sub 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)
90sub 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
105sub 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
176sub 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}