Diff -- /var/www/vhosts/webstls.com/httpdocs/_tax_preview.cgi
Diff

/var/www/vhosts/webstls.com/httpdocs/_tax_preview.cgi

added on WebSTLs (webstls.com) at 2026-07-01 22:27:12

Added
+157
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to 018bbcc73c22
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 -- _tax_preview.cgi (JSON, GET)
4#
5# Async endpoint: the checkout page calls this when the buyer picks
6# a billing country/region so the cart total can show the right tax
7# amount BEFORE Stripe Elements collects payment.
8#
9# GET /_tax_preview.cgi?storefront_id=N&bill_country=DE&bill_region=
10# Returns:
11# { ok:1, subtotal_cents:..., tax_cents:..., total_cents:..., lines:[...] }
12# { ok:0, error:'message' }
13#
14# Reads the same cart cookie the checkout form will POST, so the
15# preview matches exactly what checkout_intent.cgi will compute.
16#======================================================================
17use strict;
18use warnings;
19
20use lib '/var/www/vhosts/webstls.com/httpdocs';
21use CGI;
22use JSON::PP;
23use MODS::DBConnect;
24use MODS::WebSTLs::Config;
25use MODS::WebSTLs::Cart;
26use MODS::WebSTLs::Promotions;
27use MODS::WebSTLs::Tax;
28
29my $q = CGI->new;
30my $form = $q->Vars;
31my $db = MODS::DBConnect->new;
32my $cfg = MODS::WebSTLs::Config->new;
33my $cart = MODS::WebSTLs::Cart->new;
34my $tax = MODS::WebSTLs::Tax->new;
35my $promo = MODS::WebSTLs::Promotions->new;
36my $DB = $cfg->settings('database_name');
37
38sub _json {
39 my ($h, $status) = @_;
40 $status ||= '200 OK';
41 print "Status: $status\nContent-Type: application/json; charset=utf-8\nCache-Control: no-store\n\n";
42 print JSON::PP::encode_json($h);
43 exit;
44}
45
46my $sid = $form->{storefront_id} || 0; $sid =~ s/[^0-9]//g;
47_json({ ok => 0, error => 'storefront required' }) unless $sid;
48
49my $bc = uc($form->{bill_country} || ''); $bc =~ s/[^A-Z]//g; $bc = substr($bc, 0, 2);
50my $br = uc($form->{bill_region} || ''); $br =~ s/[^A-Z0-9]//g; $br = substr($br, 0, 10);
51my $vat_id = uc($form->{vat_id} || ''); $vat_id =~ s/[^A-Z0-9]//g; $vat_id = substr($vat_id, 0, 20);
52
53my $dbh = $db->db_connect();
54_json({ ok => 0, error => 'db' }) unless $dbh;
55
56my $token = $cart->read_token($q);
57my @rows = $cart->items($db, $dbh, $DB, $token, $sid);
58my $subtotal = $cart->total_cents($db, $dbh, $DB, $token, $sid);
59
60unless (@rows && $subtotal > 0) {
61 $db->db_disconnect($dbh);
62 _json({ ok => 1, subtotal_cents => 0, tax_cents => 0, total_cents => 0, lines => [] });
63}
64
65# Re-apply current promo so the preview is in lockstep with what
66# checkout_intent.cgi will compute on submit.
67my $applied = $promo->cart_applied($db, $dbh, $DB, $token, $sid);
68my $discount_cents = 0;
69if ($applied) {
70 my %v = $promo->validate($db, $dbh, $DB, $applied,
71 kind => 'model',
72 subtotal_cents => $subtotal,
73 buyer_email => '',
74 new_customer => 1,
75 );
76 $discount_cents = $promo->apply_to_total_cents($applied, $subtotal) if $v{ok};
77}
78my $after_discount = $subtotal - $discount_cents;
79$after_discount = 0 if $after_discount < 0;
80
81# Dominant product kind across the cart -- same logic as checkout_intent.
82my $kind = 'digital';
83{
84 my $has_physical = 0;
85 foreach my $r (@rows) {
86 my $mid = $r->{model_id} || 0; $mid =~ s/[^0-9]//g;
87 next unless $mid;
88 my $m = $db->db_readwrite($dbh, qq~
89 SELECT product_kind FROM `${DB}`.models WHERE id='$mid' LIMIT 1
90 ~, $ENV{SCRIPT_NAME}, __LINE__);
91 if ($m && $m->{product_kind} && $m->{product_kind} ne 'digital') {
92 $has_physical = 1; last;
93 }
94 }
95 $kind = $has_physical ? 'physical' : 'digital';
96}
97
98# B2B reverse-charge check: if a previously-VIES-validated VAT ID
99# matches what the buyer typed, set reverse_charge so the tax line
100# is zero-amount but still records the audit trail.
101my $reverse_charge = 0;
102my ($vat_country, $vat_id_clean) = ('', '');
103if (length $vat_id >= 4) {
104 my $cached = $db->db_readwrite($dbh, qq~
105 SELECT vat_country, is_valid,
106 DATEDIFF(NOW(), validated_at) AS age
107 FROM `${DB}`.buyer_tax_info
108 WHERE vat_id='~ . (do { my $v = $vat_id; $v =~ s/'/''/g; $v }) . qq~'
109 ORDER BY id DESC LIMIT 1
110 ~, $ENV{SCRIPT_NAME}, __LINE__);
111 if ($cached && $cached->{is_valid} && defined $cached->{age} && $cached->{age} < 180) {
112 $reverse_charge = 1;
113 $vat_country = $cached->{vat_country};
114 $vat_id_clean = $vat_id;
115 }
116}
117
118my @lines;
119my $tax_cents = 0;
120if (length $bc == 2 && $after_discount > 0) {
121 @lines = $tax->calc_tax_lines(
122 db => $db,
123 dbh => $dbh,
124 DB => $DB,
125 storefront_id => $sid,
126 country_iso2 => $bc,
127 region_code => $br,
128 product_kind => $kind,
129 taxable_amount_cents => $after_discount,
130 reverse_charge => $reverse_charge,
131 buyer_vat_id => $vat_id_clean,
132 buyer_vat_country => $vat_country,
133 evidence => { buyer_country_billing => $bc },
134 );
135 foreach my $l (@lines) { $tax_cents += ($l->{tax_amount_cents} || 0); }
136}
137
138$db->db_disconnect($dbh);
139
140my @out_lines;
141foreach my $l (@lines) {
142 push @out_lines, {
143 label => $l->{jurisdiction_label},
144 rate => sprintf('%.2f', $l->{rate_pct}) + 0,
145 cents => $l->{tax_amount_cents} + 0,
146 };
147}
148
149_json({
150 ok => 1,
151 subtotal_cents => $subtotal + 0,
152 discount_cents => $discount_cents + 0,
153 tax_cents => $tax_cents + 0,
154 total_cents => ($after_discount + $tax_cents) + 0,
155 lines => \@out_lines,
156 reverse_charge => $reverse_charge ? 1 : 0,
157});
Keyboard: j next diff k previous diff g top G bottom r restore c compile-check ? help