Add Kleinunternehmerregelung (§19 UStG) support end-to-end
Checkout forces 0% VAT without de-grossing prices when the tenant is a Kleinunternehmer (a business decision, not just an engineering default — unlike the existing intra-community VAT exemption, which does de-gross). Snapshotted onto the order at checkout time so a later toggle of the company-settings checkbox never rewrites an already-issued invoice's tax treatment — same reasoning as the existing vatExempt field. Threaded through: checkout route, order creation/confirmation email, on-demand invoice/Storno/Gutschrift downloads, the Bestellbestätigung page, and the account order-detail page. The four storefront "inkl. X% MwSt." price hints (shop grid, cart upsell, ToDo-Karten landing page, homepage spotlight) drop that clause live when the setting is on. The company-settings Live Preview reflects the checkbox in real time too. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -22,6 +22,7 @@ export function CartContent({
|
||||
freeShippingThreshold,
|
||||
shippingSettings,
|
||||
defaultTaxRate,
|
||||
kleinunternehmer,
|
||||
showDiscountField,
|
||||
}: {
|
||||
trustBadges: TrustBadge[];
|
||||
@@ -42,6 +43,11 @@ export function CartContent({
|
||||
* override taxRatePercent themselves — see lib/cartTotals.ts's
|
||||
* effectiveTaxRate(). */
|
||||
defaultTaxRate: number;
|
||||
/** §19 UStG — this tenant's company-settings.kleinunternehmer (Payload's
|
||||
* lib/payload.ts's getKleinunternehmer(), same ISR freshness as
|
||||
* defaultTaxRate above). Drops the "inkl. X% MwSt." hints and the VAT
|
||||
* breakdown in favor of the §19 notice below. */
|
||||
kleinunternehmer: boolean;
|
||||
/** Whether Payload currently has at least one active discount code at
|
||||
* all (lib/discountServer.ts's hasActiveDiscountCode()) — no point
|
||||
* showing an open "enter a code" field when nothing could ever validate
|
||||
@@ -236,7 +242,7 @@ export function CartContent({
|
||||
<span className="text-label text-text-muted line-through">{formatPrice(product.compareAtPrice!)}</span>
|
||||
)}
|
||||
<span className="font-bold text-body-sm text-text-primary">{formatPrice(unitPrice)}</span>
|
||||
<span className="text-label text-text-muted">inkl. {taxRate}% MwSt.</span>
|
||||
{!kleinunternehmer && <span className="text-label text-text-muted">inkl. {taxRate}% MwSt.</span>}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -413,7 +419,11 @@ export function CartContent({
|
||||
<span className="flex-1" />
|
||||
<span className="font-bold text-h-small text-text-primary">{formatPrice(total)}</span>
|
||||
</div>
|
||||
<VatBreakdown groups={taxBreakdown} />
|
||||
{kleinunternehmer ? (
|
||||
<p className="text-label text-text-muted">Gemäß § 19 UStG wird keine Umsatzsteuer berechnet.</p>
|
||||
) : (
|
||||
<VatBreakdown groups={taxBreakdown} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Link
|
||||
|
||||
@@ -30,7 +30,7 @@ function pickAvailable(allIds: string[], excludeIds: string[], keep: string[], c
|
||||
return [...keep, ...pickRandom(allIds, [...excludeIds, ...keep], missing)];
|
||||
}
|
||||
|
||||
export function RelatedProducts({ defaultTaxRate }: { defaultTaxRate: number }) {
|
||||
export function RelatedProducts({ defaultTaxRate, kleinunternehmer }: { defaultTaxRate: number; kleinunternehmer: boolean }) {
|
||||
const cart = useCart();
|
||||
const products = useProducts();
|
||||
// Cart/checkout resolve any product regardless of `active` (see
|
||||
@@ -188,7 +188,7 @@ export function RelatedProducts({ defaultTaxRate }: { defaultTaxRate: number })
|
||||
<span className="text-label text-text-muted line-through">{formatPrice(product.compareAtPrice!)}</span>
|
||||
)}
|
||||
<span className="font-bold text-h4 text-text-primary">{formatPrice(product.price)}</span>
|
||||
<span className="text-label text-text-muted">inkl. {taxRate}% MwSt.</span>
|
||||
{!kleinunternehmer && <span className="text-label text-text-muted">inkl. {taxRate}% MwSt.</span>}
|
||||
</p>
|
||||
{/* Always rendered, text conditional — min-h reserves this
|
||||
line's height in both states so cards in the same row
|
||||
|
||||
+5
-3
@@ -4,7 +4,7 @@ import { CartContent } from "./components/CartContent";
|
||||
import { RelatedProducts } from "./components/RelatedProducts";
|
||||
import { TrustRow } from "../components/TrustRow";
|
||||
import { Footer } from "../components/Footer";
|
||||
import { getCartTrustBadges, getShippingMethods, getShippingSettings, getDefaultTaxRatePercent } from "../lib/payload";
|
||||
import { getCartTrustBadges, getShippingMethods, getShippingSettings, getDefaultTaxRatePercent, getKleinunternehmer } from "../lib/payload";
|
||||
import { hasActiveDiscountCode } from "../lib/discountServer";
|
||||
|
||||
// robots: noindex — transactional page (mirrors a specific shopper's cart
|
||||
@@ -20,11 +20,12 @@ export const metadata: Metadata = {
|
||||
};
|
||||
|
||||
export default async function CartPage() {
|
||||
const [trustBadges, shippingMethods, shipping, defaultTaxRate, showDiscountField] = await Promise.all([
|
||||
const [trustBadges, shippingMethods, shipping, defaultTaxRate, kleinunternehmer, showDiscountField] = await Promise.all([
|
||||
getCartTrustBadges(),
|
||||
getShippingMethods(),
|
||||
getShippingSettings(),
|
||||
getDefaultTaxRatePercent(),
|
||||
getKleinunternehmer(),
|
||||
hasActiveDiscountCode(),
|
||||
]);
|
||||
|
||||
@@ -55,10 +56,11 @@ export default async function CartPage() {
|
||||
freeShippingThreshold={freeShippingThreshold}
|
||||
shippingSettings={shipping}
|
||||
defaultTaxRate={defaultTaxRate}
|
||||
kleinunternehmer={kleinunternehmer}
|
||||
showDiscountField={showDiscountField}
|
||||
/>
|
||||
</Suspense>
|
||||
<RelatedProducts defaultTaxRate={defaultTaxRate} />
|
||||
<RelatedProducts defaultTaxRate={defaultTaxRate} kleinunternehmer={kleinunternehmer} />
|
||||
<TrustRow />
|
||||
</main>
|
||||
<Footer />
|
||||
|
||||
Reference in New Issue
Block a user