Diff -- /var/www/vhosts/3dshawn.com/affiliate.3dshawn.com/promotions.cgi
Diff

/var/www/vhosts/3dshawn.com/affiliate.3dshawn.com/promotions.cgi

added on local at 2026-07-01 13:47:41

Added
+313
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to 6f1d8f66341a
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# WebSTLs - Seller Promotions
4#
5# List page + new/edit form for the seller's own coupons, sales, bundles
6# and flash sales. Posts to promotion_action.cgi for create/update/delete.
7#======================================================================
8use strict;
9use warnings;
10
11use lib '/var/www/vhosts/3dshawn.com/affiliate.3dshawn.com';
12use CGI;
13use MODS::Template;
14use MODS::DBConnect;
15use MODS::Login;
16use MODS::AffSoft::Config;
17use MODS::AffSoft::Wrapper;
18use MODS::AffSoft::Promotions;
19
20my $q = CGI->new;
21my $form = $q->Vars;
22my $auth = MODS::Login->new;
23my $wrap = MODS::AffSoft::Wrapper->new;
24my $tfile = MODS::Template->new;
25my $db = MODS::DBConnect->new;
26my $cfg = MODS::AffSoft::Config->new;
27my $promo = MODS::AffSoft::Promotions->new;
28my $DB = $cfg->settings('database_name');
29
30$|=1;
31my $userinfo = $auth->login_verify();
32if (!$userinfo) {
33 print "Status: 302 Found\nLocation: /login.cgi\n\n";
34 exit;
35}
36require MODS::AffSoft::Permissions;
37MODS::AffSoft::Permissions->new->require_feature($userinfo, 'view_promotions');
38my $uid = $userinfo->{user_id};
39$uid =~ s/[^0-9]//g;
40
41my $tutorial_mode = ($form->{tutorial} && $form->{tutorial} eq '1') ? 1 : 0;
42
43# View mode: list (default) | new | edit
44my $view = ($form->{new} && $form->{new} eq '1') ? 'new'
45 : ($form->{edit_id} && $form->{edit_id} =~ /^\d+$/) ? 'edit'
46 : 'list';
47my $edit_id = ($form->{edit_id} || '');
48$edit_id =~ s/[^0-9]//g;
49
50my $dbh = $db->db_connect();
51my $schema_ready = $promo->schema_ready($db, $dbh, $DB);
52
53# Find the seller's primary storefront (one per user today).
54my $sf = $db->db_readwrite($dbh,
55 qq~SELECT id, name FROM `${DB}`.storefronts WHERE user_id='$uid' ORDER BY id LIMIT 1~,
56 $ENV{SCRIPT_NAME}, __LINE__);
57my $store_id = ($sf && $sf->{id}) ? $sf->{id} : 0;
58my $store_name = ($sf && $sf->{name}) ? $sf->{name} : 'Your storefront';
59
60# Load list rows (always, used to render the counts + table when view=list).
61my @rows_raw = $schema_ready ? $promo->list_for_seller($db, $dbh, $DB, $uid, 100) : ();
62
63my @rows;
64my $cnt_active = 0; my $cnt_scheduled = 0; my $cnt_paused = 0; my $cnt_expired = 0;
65foreach my $r (@rows_raw) {
66 my $effective = $promo->effective_status($r);
67 $cnt_active++ if $effective eq 'active';
68 $cnt_scheduled++ if $effective eq 'scheduled';
69 $cnt_paused++ if $effective eq 'paused';
70 $cnt_expired++ if $effective eq 'expired' || $effective eq 'exhausted';
71
72 push @rows, _row_for_template($r, $promo, $db, $dbh, $DB);
73}
74
75# Edit-mode load.
76my %edit_row;
77my @edit_models;
78if ($view eq 'edit' && $edit_id) {
79 my $e = $promo->get_for_seller($db, $dbh, $DB, $edit_id, $uid);
80 if ($e && $e->{id}) {
81 %edit_row = _row_for_edit_form($e);
82 @edit_models = $promo->models_for_promo($db, $dbh, $DB, $edit_id);
83 } else {
84 $view = 'list';
85 }
86}
87
88# Available models for the picker (only on new/edit views).
89my @user_models;
90if ($view eq 'new' || $view eq 'edit') {
91 @user_models = $db->db_readwrite_multiple($dbh, qq~
92 SELECT id, title, status
93 FROM `${DB}`.models
94 WHERE user_id='$uid' AND purged_at IS NULL
95 ORDER BY title ASC
96 LIMIT 500
97 ~, $ENV{SCRIPT_NAME}, __LINE__);
98 # Mark which model_ids are linked when editing.
99 my %sel = map { $_->{model_id} => 1 } @edit_models;
100 foreach my $m (@user_models) {
101 $m->{is_selected} = $sel{ $m->{id} } ? 1 : 0;
102 }
103}
104
105# Flash banner from action handler redirects.
106my %flash;
107if ($form->{ok} eq 'created') { %flash = (kind=>'ok', msg=>'Promotion created.'); }
108elsif ($form->{ok} eq 'updated') { %flash = (kind=>'ok', msg=>'Promotion updated.'); }
109elsif ($form->{ok} eq 'deleted') { %flash = (kind=>'ok', msg=>'Promotion deleted.'); }
110elsif ($form->{ok} eq 'activated') { %flash = (kind=>'ok', msg=>'Promotion is now active.'); }
111elsif ($form->{ok} eq 'paused') { %flash = (kind=>'ok', msg=>'Promotion paused.'); }
112elsif ($form->{err} eq 'bad_input') { %flash = (kind=>'err', msg=>'Required fields missing.'); }
113elsif ($form->{err} eq 'denied') { %flash = (kind=>'err', msg=>'Action not permitted.'); }
114
115$db->db_disconnect($dbh);
116
117my $tvars = {
118 user_id => $uid,
119 store_id => $store_id,
120 store_name => $store_name,
121
122 is_list => ($view eq 'list') ? 1 : 0,
123 is_new => ($view eq 'new') ? 1 : 0,
124 is_edit => ($view eq 'edit') ? 1 : 0,
125
126 rows => \@rows,
127 has_rows => scalar(@rows) ? 1 : 0,
128 cnt_active => $cnt_active,
129 cnt_scheduled => $cnt_scheduled,
130 cnt_paused => $cnt_paused,
131 cnt_expired => $cnt_expired,
132 cnt_total => scalar(@rows),
133
134 edit => \%edit_row,
135 has_edit => %edit_row ? 1 : 0,
136 user_models => \@user_models,
137 has_user_models => scalar(@user_models) ? 1 : 0,
138
139 schema_ready => $schema_ready ? 1 : 0,
140 schema_missing => $schema_ready ? 0 : 1,
141
142 flash_kind => $flash{kind} || '',
143 flash_msg => $flash{msg} || '',
144 has_flash => %flash ? 1 : 0,
145
146 tutorial_mode => $tutorial_mode,
147 not_tutorial_mode => $tutorial_mode ? 0 : 1,
148};
149
150# Tutorial sample data: only meaningful on the list view.
151if ($tutorial_mode && $view eq 'list') {
152 $tvars->{rows} = [
153 { id=>1, name=>'Spring 20 percent off', kind_label=>'Coupon', is_coupon=>1, is_sale=>0, is_bundle=>0, is_flash=>0,
154 discount_label=>'20% off', code=>'SPRING20', code_present=>1, public_slug=>'spring20', slug_present=>1,
155 status=>'active', status_label=>'Active', is_active=>1, is_paused=>0, is_scheduled=>0, is_expired=>0, is_draft=>0,
156 starts_at=>'2026-04-01 00:00:00', ends_at=>'2026-06-01 00:00:00', has_window=>1,
157 current_redemptions=>87, max_redemptions=>500, has_cap=>1, cap_label=>'87 of 500 used',
158 discount_given_display=>'$1,160.00', edit_url=>'/promotions.cgi?edit_id=1', tutorial_demo=>1 },
159 { id=>2, name=>'Skirmisher bundle special', kind_label=>'Bundle', is_coupon=>0, is_sale=>0, is_bundle=>1, is_flash=>0,
160 discount_label=>'Bundle for $39.00', code=>'', code_present=>0, public_slug=>'skirmisher-bundle', slug_present=>1,
161 status=>'active', status_label=>'Active', is_active=>1, is_paused=>0, is_scheduled=>0, is_expired=>0, is_draft=>0,
162 starts_at=>'2026-05-01 00:00:00', ends_at=>'2026-07-01 00:00:00', has_window=>1,
163 current_redemptions=>34, max_redemptions=>0, has_cap=>0, cap_label=>'34 used',
164 discount_given_display=>'$612.00', edit_url=>'/promotions.cgi?edit_id=2', tutorial_demo=>1 },
165 { id=>3, name=>'Memorial Day flash sale', kind_label=>'Flash sale', is_coupon=>0, is_sale=>0, is_bundle=>0, is_flash=>1,
166 discount_label=>'30% off', code=>'', code_present=>0, public_slug=>'memorial-day', slug_present=>1,
167 status=>'scheduled', status_label=>'Scheduled', is_active=>0, is_paused=>0, is_scheduled=>1, is_expired=>0, is_draft=>0,
168 starts_at=>'2026-05-23 00:00:00', ends_at=>'2026-05-27 23:59:00', has_window=>1,
169 current_redemptions=>0, max_redemptions=>200, has_cap=>1, cap_label=>'0 of 200 used',
170 discount_given_display=>'$0.00', edit_url=>'/promotions.cgi?edit_id=3', tutorial_demo=>1 },
171 { id=>4, name=>'First-time buyer 10 off', kind_label=>'Coupon', is_coupon=>1, is_sale=>0, is_bundle=>0, is_flash=>0,
172 discount_label=>'$10.00 off', code=>'WELCOME10', code_present=>1, public_slug=>'welcome10', slug_present=>1,
173 status=>'active', status_label=>'Active', is_active=>1, is_paused=>0, is_scheduled=>0, is_expired=>0, is_draft=>0,
174 starts_at=>'2026-01-01 00:00:00', ends_at=>'', has_window=>0,
175 current_redemptions=>142, max_redemptions=>0, has_cap=>0, cap_label=>'142 used',
176 discount_given_display=>'$1,420.00', edit_url=>'/promotions.cgi?edit_id=4', tutorial_demo=>1 },
177 { id=>5, name=>'Old test promo', kind_label=>'Sale', is_coupon=>0, is_sale=>1, is_bundle=>0, is_flash=>0,
178 discount_label=>'15% off', code=>'', code_present=>0, public_slug=>'', slug_present=>0,
179 status=>'expired', status_label=>'Expired', is_active=>0, is_paused=>0, is_scheduled=>0, is_expired=>1, is_draft=>0,
180 starts_at=>'2026-03-01 00:00:00', ends_at=>'2026-03-31 23:59:00', has_window=>1,
181 current_redemptions=>62, max_redemptions=>0, has_cap=>0, cap_label=>'62 used',
182 discount_given_display=>'$540.00', edit_url=>'/promotions.cgi?edit_id=5', tutorial_demo=>1 },
183 ];
184 $tvars->{has_rows} = 1;
185 $tvars->{cnt_active} = 3;
186 $tvars->{cnt_scheduled} = 1;
187 $tvars->{cnt_paused} = 0;
188 $tvars->{cnt_expired} = 1;
189 $tvars->{cnt_total} = 5;
190 $tvars->{schema_ready} = 1;
191 $tvars->{schema_missing}= 0;
192}
193
194print "Content-Type: text/html; charset=utf-8\nCache-Control: no-store, no-cache, must-revalidate, max-age=0\nPragma: no-cache\nExpires: 0\n\n";
195
196my $body = join('', $tfile->template('webstls_promotions.html', $tvars, $userinfo));
197
198$wrap->render({
199 userinfo => $userinfo,
200 page_key => 'promotions',
201 title => 'Promotions',
202 body => $body,
203});
204
205# ----------------------------------------------------------------- helpers
206sub _row_for_template {
207 my ($r, $promo, $db, $dbh, $DB) = @_;
208 my $eff = $promo->effective_status($r);
209 return {
210 id => $r->{id},
211 name => _h($r->{name} || ''),
212 kind_label => _kind_label($r->{kind}),
213 is_coupon => ($r->{kind} eq 'coupon') ? 1 : 0,
214 is_sale => ($r->{kind} eq 'sale') ? 1 : 0,
215 is_bundle => ($r->{kind} eq 'bundle') ? 1 : 0,
216 is_flash => ($r->{kind} eq 'flash_sale') ? 1 : 0,
217 discount_label => $promo->discount_label($r),
218 code => _h($r->{code} || ''),
219 code_present => ($r->{code} && $r->{code} ne '') ? 1 : 0,
220 public_slug => _h($r->{public_slug} || ''),
221 slug_present => ($r->{public_slug} && $r->{public_slug} ne '') ? 1 : 0,
222 status => $eff,
223 status_label => $promo->status_label($eff),
224 is_active => $eff eq 'active' ? 1 : 0,
225 is_paused => $eff eq 'paused' ? 1 : 0,
226 is_scheduled => $eff eq 'scheduled' ? 1 : 0,
227 is_expired => ($eff eq 'expired' || $eff eq 'exhausted') ? 1 : 0,
228 is_draft => $eff eq 'draft' ? 1 : 0,
229 starts_at => $r->{starts_at} || '',
230 ends_at => $r->{ends_at} || '',
231 has_window => (($r->{starts_at} || $r->{ends_at}) ? 1 : 0),
232 current_redemptions => $r->{current_redemptions} || $promo->redemption_count($db, $dbh, $DB, $r->{id}),
233 max_redemptions => defined $r->{max_redemptions} ? $r->{max_redemptions} : 0,
234 has_cap => (defined $r->{max_redemptions} && $r->{max_redemptions} > 0) ? 1 : 0,
235 cap_label => _cap_label($r),
236 discount_given_display => '$' . sprintf('%.2f', ($promo->total_discount_given_cents($db, $dbh, $DB, $r->{id}) || 0) / 100),
237 edit_url => '/promotions.cgi?edit_id=' . $r->{id},
238 };
239}
240
241sub _row_for_edit_form {
242 my ($r) = @_;
243 my $pct = ($r->{discount_value_bp} || 0) / 100;
244 my $amt = ($r->{discount_value_cents} || 0) / 100;
245 my $min = ($r->{minimum_order_cents} || 0) / 100;
246 return (
247 id => $r->{id},
248 name => _h($r->{name} || ''),
249 description => _h($r->{description} || ''),
250 kind => $r->{kind},
251 is_coupon => ($r->{kind} eq 'coupon') ? 1 : 0,
252 is_sale => ($r->{kind} eq 'sale') ? 1 : 0,
253 is_bundle => ($r->{kind} eq 'bundle') ? 1 : 0,
254 is_flash => ($r->{kind} eq 'flash_sale') ? 1 : 0,
255 discount_type => $r->{discount_type},
256 is_dtype_pct => ($r->{discount_type} eq 'percent') ? 1 : 0,
257 is_dtype_amt => ($r->{discount_type} eq 'fixed_amount') ? 1 : 0,
258 is_dtype_bun => ($r->{discount_type} eq 'bundle_price') ? 1 : 0,
259 is_dtype_free => ($r->{discount_type} eq 'free') ? 1 : 0,
260 discount_value_pct => sprintf('%.0f', $pct),
261 discount_value_amt => sprintf('%.2f', $amt),
262 minimum_order => sprintf('%.2f', $min),
263 code => _h($r->{code} || ''),
264 public_slug => _h($r->{public_slug} || ''),
265 starts_at_local => _to_local_input($r->{starts_at}),
266 ends_at_local => _to_local_input($r->{ends_at}),
267 max_redemptions => defined $r->{max_redemptions} ? $r->{max_redemptions} : '',
268 max_per_user => defined $r->{max_per_user} ? $r->{max_per_user} : '',
269 new_customers_only => $r->{new_customers_only} ? 1 : 0,
270 status => $r->{status},
271 status_label => MODS::AffSoft::Promotions->status_label($r->{status}),
272 is_status_active => ($r->{status} eq 'active') ? 1 : 0,
273 is_status_paused => ($r->{status} eq 'paused') ? 1 : 0,
274 is_status_draft => ($r->{status} eq 'draft') ? 1 : 0,
275 );
276}
277
278sub _to_local_input {
279 my $ts = shift;
280 return '' unless defined $ts && $ts ne '';
281 if ($ts =~ /^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2})/) {
282 return "$1T$2"; # HTML datetime-local format
283 }
284 return '';
285}
286
287sub _kind_label {
288 my $k = shift || '';
289 return 'Coupon' if $k eq 'coupon';
290 return 'Sale' if $k eq 'sale';
291 return 'Bundle' if $k eq 'bundle';
292 return 'Flash sale' if $k eq 'flash_sale';
293 return ucfirst $k;
294}
295
296sub _cap_label {
297 my ($r) = @_;
298 my $cur = $r->{current_redemptions} || 0;
299 if (defined $r->{max_redemptions} && $r->{max_redemptions} > 0) {
300 return "$cur of $r->{max_redemptions} used";
301 }
302 return "$cur used";
303}
304
305sub _h {
306 my $s = shift;
307 $s //= '';
308 $s =~ s/&/&/g;
309 $s =~ s/</&lt;/g;
310 $s =~ s/>/&gt;/g;
311 $s =~ s/"/&quot;/g;
312 return $s;
313}
Keyboard: j next diff k previous diff g top G bottom r restore c compile-check ? help