Discount Codes
Apply and manage discount codes using the Storefront SDK
Overview
The discountCode resource provides methods for applying, retrieving, and removing discount codes from the cart. Discount codes provide percentage or fixed amount discounts at checkout.
Key Features:
- Two discount types: percentage (e.g., 20% off) or fixed amount (e.g., 5€ off)
- Automatic campaign conflict detection (discount codes cannot be used with BuyXPayY campaigns)
- Stored in cart (Redis) and automatically applied at checkout
- Validation for expiry, usage limits, and minimum order requirements
Important: Discount codes and BuyXPayY campaigns are mutually exclusive. When a campaign applies to the cart, discount codes cannot be used. The SDK checks for this conflict before calling the API.
Campaign Conflict Rule
Discount codes cannot be combined with BuyXPayY campaigns. When cart items qualify for a campaign discount:
- Applying a code → Returns error
CAMPAIGN_ACTIVE - Code already applied + cart changes to qualify for campaign → Code is automatically removed during cart validation
The SDK performs client-side campaign conflict checking for instant feedback, but the backend also validates at checkout.
Methods
discountCode.apply(params)
Apply a discount code to the cart.
try {
const result = await storefront.discountCode.apply({
code: "SUMMER20",
cartId,
cartItems: cart.items,
campaigns: storeConfig.campaigns,
});
console.log(result.discount);
// { code: "SUMMER20", discountType: "PERCENTAGE", discountValue: 20, minOrderAmount: 5000 }
} catch (error) {
if (error instanceof StorefrontError && error.code === "CAMPAIGN_ACTIVE") {
toast.error("Alennuskoodia ei voi käyttää kun kampanja on voimassa");
}
}Parameters:
| Param | Type | Required | Description |
|---|---|---|---|
code | string | Yes | Discount code to apply |
cartId | string | No | Cart ID for guest users |
sessionId | string | No | Session ID for logged-in users |
cartItems | CartItem[] | No | Cart items for campaign conflict check |
campaigns | Campaign[] | No | Active campaigns for conflict check |
Returns: Promise<ApplyDiscountResponse>
interface ApplyDiscountResponse {
success: boolean;
discount: {
code: string;
discountType: "PERCENTAGE" | "FIXED_AMOUNT";
discountValue: number; // Percentage (1-100) or cents
minOrderAmount: number; // Minimum order in cents
};
}Throws:
StorefrontErrorwith various error codes (see Error Codes)
discountCode.get(params?)
Get the currently applied discount code from the cart.
const { discount } = await storefront.discountCode.get({ cartId });
if (discount) {
console.log(`Code: ${discount.code}`);
console.log(`Type: ${discount.discountType}`);
console.log(`Value: ${discount.discountValue}`);
}Parameters:
| Param | Type | Required | Description |
|---|---|---|---|
cartId | string | No | Cart ID for guest users |
sessionId | string | No | Session ID for logged-in users |
Returns: Promise<GetDiscountResponse>
interface GetDiscountResponse {
discount: AppliedDiscount | null;
}
interface AppliedDiscount {
code: string;
discountType: "PERCENTAGE" | "FIXED_AMOUNT";
discountValue: number;
}discountCode.remove(params?)
Remove the currently applied discount code from the cart.
await storefront.discountCode.remove({ cartId });Parameters:
| Param | Type | Required | Description |
|---|---|---|---|
cartId | string | No | Cart ID for guest users |
sessionId | string | No | Session ID for logged-in users |
Returns: Promise<RemoveDiscountResponse>
interface RemoveDiscountResponse {
success: boolean;
}Note: Safe to call even if no discount is applied (returns success: true).
Error Codes
When applying a discount code fails, the error includes a code field:
| Code | Description |
|---|---|
NOT_FOUND | Discount code doesn't exist |
INACTIVE | Code is disabled by store owner |
NOT_STARTED | Code's valid date range hasn't started |
EXPIRED | Code has passed its end date |
MAX_USES_REACHED | Code's usage limit exceeded |
MIN_ORDER_NOT_MET | Cart total below minimum order |
CAMPAIGN_ACTIVE | Cannot use with active campaign |
CART_MISSING | No cart ID or session provided |
Utility Functions
calculateDiscountAmount(subtotal, discount)
Calculate the discount amount in cents.
import { calculateDiscountAmount } from '@putiikkipalvelu/storefront-sdk';
// Percentage discount: 20% off 100€
const amount1 = calculateDiscountAmount(10000, {
discountType: "PERCENTAGE",
discountValue: 20
});
// Returns: 2000 (20€ in cents)
// Fixed amount discount: 5€ off
const amount2 = calculateDiscountAmount(10000, {
discountType: "FIXED_AMOUNT",
discountValue: 500
});
// Returns: 500 (5€ in cents)
// Fixed discount capped at subtotal
const amount3 = calculateDiscountAmount(300, {
discountType: "FIXED_AMOUNT",
discountValue: 500
});
// Returns: 300 (can't discount more than total)formatDiscountValue(discount, options?)
Format a discount value for display.
import { formatDiscountValue } from '@putiikkipalvelu/storefront-sdk';
// Percentage discount
formatDiscountValue({ discountType: "PERCENTAGE", discountValue: 20 });
// Returns: "-20%"
// Fixed amount discount (Finnish locale)
formatDiscountValue({ discountType: "FIXED_AMOUNT", discountValue: 500 });
// Returns: "-5,00 €"
// Custom currency and position
formatDiscountValue(
{ discountType: "FIXED_AMOUNT", discountValue: 1000 },
{ currencySymbol: "$", currencyPosition: "before" }
);
// Returns: "-$10.00"Options:
| Option | Type | Default | Description |
|---|---|---|---|
currencySymbol | string | "€" | Currency symbol |
currencyPosition | "before" | "after" | "after" | Symbol position |
decimals | number | 2 | Decimal places |
showMinus | boolean | true | Show minus sign prefix |
getDiscountApplyErrorMessage(errorCode, locale?)
Get a user-friendly error message for discount apply errors.
import { getDiscountApplyErrorMessage } from '@putiikkipalvelu/storefront-sdk';
getDiscountApplyErrorMessage("EXPIRED", "fi");
// Returns: "Alennuskoodi on vanhentunut"
getDiscountApplyErrorMessage("EXPIRED", "en");
// Returns: "Discount code has expired"getDiscountRemovalMessage(reason, locale?)
Get a user-friendly message when discount is removed during validation.
import { getDiscountRemovalMessage } from '@putiikkipalvelu/storefront-sdk';
getDiscountRemovalMessage("CAMPAIGN_ACTIVE", "fi");
// Returns: "Alennuskoodi poistettu - kampanja-alennus aktivoitui"
getDiscountRemovalMessage("MIN_ORDER_NOT_MET", "en");
// Returns: "Discount code removed - cart total below minimum order"Cart Validation Integration
The cart validation endpoint checks discount codes and may remove them. Use the changes object to notify users:
const { hasChanges, changes } = await storefront.cart.validate(
{ cartId },
cartItems,
campaigns // Pass campaigns for conflict detection
);
if (changes.discountCouponRemoved) {
const message = getDiscountRemovalMessage(
changes.discountRemovalReason,
'fi'
);
toast.warning(message);
}Removal Reasons:
| Reason | Description |
|---|---|
CAMPAIGN_ACTIVE | Cart now qualifies for a campaign discount |
MIN_ORDER_NOT_MET | Cart total dropped below minimum order |
CODE_INVALID | Code was deactivated or expired |
Examples
Discount Code Input Component
'use client';
import { useState } from 'react';
import { storefront, StorefrontError, getDiscountApplyErrorMessage } from '@/lib/storefront';
interface DiscountCodeInputProps {
cartId: string;
cartItems: CartItem[];
campaigns: Campaign[];
onApplied: (discount: AppliedDiscount) => void;
}
export function DiscountCodeInput({
cartId,
cartItems,
campaigns,
onApplied
}: DiscountCodeInputProps) {
const [code, setCode] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleApply = async (e: React.FormEvent) => {
e.preventDefault();
if (!code.trim()) return;
setIsLoading(true);
setError(null);
try {
const result = await storefront.discountCode.apply({
code: code.trim(),
cartId,
cartItems,
campaigns,
});
onApplied(result.discount);
setCode('');
} catch (err) {
if (err instanceof StorefrontError) {
setError(getDiscountApplyErrorMessage(err.code, 'fi'));
} else {
setError('Virhe alennuskoodin käytössä');
}
} finally {
setIsLoading(false);
}
};
return (
<form onSubmit={handleApply} className="space-y-2">
<div className="flex gap-2">
<input
type="text"
value={code}
onChange={(e) => setCode(e.target.value.toUpperCase())}
placeholder="Alennuskoodi"
className="flex-1 px-3 py-2 border rounded"
disabled={isLoading}
/>
<button
type="submit"
disabled={isLoading || !code.trim()}
className="px-4 py-2 bg-primary text-white rounded disabled:opacity-50"
>
{isLoading ? 'Ladataan...' : 'Käytä'}
</button>
</div>
{error && (
<p className="text-sm text-red-600">{error}</p>
)}
</form>
);
}Applied Discount Display
'use client';
import { formatDiscountValue, calculateDiscountAmount } from '@putiikkipalvelu/storefront-sdk';
interface AppliedDiscountDisplayProps {
discount: AppliedDiscount;
subtotal: number; // Cart subtotal in cents
onRemove: () => void;
}
export function AppliedDiscountDisplay({
discount,
subtotal,
onRemove
}: AppliedDiscountDisplayProps) {
const discountAmount = calculateDiscountAmount(subtotal, discount);
const formattedValue = formatDiscountValue(discount);
return (
<div className="flex items-center justify-between p-3 bg-green-50 border border-green-200 rounded">
<div>
<span className="font-medium text-green-800">{discount.code}</span>
<span className="ml-2 text-green-600">{formattedValue}</span>
</div>
<div className="flex items-center gap-3">
<span className="text-green-700">
-{(discountAmount / 100).toFixed(2)} €
</span>
<button
onClick={onRemove}
className="text-sm text-red-600 hover:underline"
>
Poista
</button>
</div>
</div>
);
}Complete Cart Summary with Discounts
'use client';
import { useMemo, useEffect, useState } from 'react';
import {
calculateCartWithCampaigns,
calculateDiscountAmount,
formatDiscountValue
} from '@putiikkipalvelu/storefront-sdk';
import { storefront } from '@/lib/storefront';
import { DiscountCodeInput } from './DiscountCodeInput';
import { AppliedDiscountDisplay } from './AppliedDiscountDisplay';
interface CartSummaryProps {
cartId: string;
cartItems: CartItem[];
campaigns: Campaign[];
}
export function CartSummary({ cartId, cartItems, campaigns }: CartSummaryProps) {
const [discount, setDiscount] = useState<AppliedDiscount | null>(null);
// Load existing discount on mount
useEffect(() => {
async function loadDiscount() {
const { discount } = await storefront.discountCode.get({ cartId });
setDiscount(discount);
}
loadDiscount();
}, [cartId]);
// Calculate cart totals with campaigns
const calculation = useMemo(() => {
return calculateCartWithCampaigns(cartItems, campaigns);
}, [cartItems, campaigns]);
// Calculate discount amount (only if no campaign applies)
const discountAmount = useMemo(() => {
if (!discount || calculation.totalSavings > 0) return 0;
return calculateDiscountAmount(calculation.cartTotal, discount);
}, [discount, calculation]);
const finalTotal = calculation.cartTotal - discountAmount;
const handleRemoveDiscount = async () => {
await storefront.discountCode.remove({ cartId });
setDiscount(null);
};
// Check if campaign is active (discount codes not allowed)
const campaignActive = calculation.totalSavings > 0;
return (
<div className="space-y-4 p-4 bg-gray-50 rounded">
{/* Subtotal */}
<div className="flex justify-between">
<span>Välisumma</span>
<span>{(calculation.originalTotal / 100).toFixed(2)} €</span>
</div>
{/* Campaign savings (if any) */}
{calculation.totalSavings > 0 && (
<div className="flex justify-between text-green-600">
<span>Kampanja-alennus</span>
<span>-{(calculation.totalSavings / 100).toFixed(2)} €</span>
</div>
)}
{/* Discount code section */}
{discount && !campaignActive ? (
<AppliedDiscountDisplay
discount={discount}
subtotal={calculation.cartTotal}
onRemove={handleRemoveDiscount}
/>
) : !campaignActive ? (
<DiscountCodeInput
cartId={cartId}
cartItems={cartItems}
campaigns={campaigns}
onApplied={setDiscount}
/>
) : (
<p className="text-sm text-gray-500">
Alennuskoodia ei voi käyttää kun kampanja on voimassa
</p>
)}
{/* Discount code savings */}
{discountAmount > 0 && (
<div className="flex justify-between text-green-600">
<span>Alennuskoodi ({discount?.code})</span>
<span>-{(discountAmount / 100).toFixed(2)} €</span>
</div>
)}
<hr />
{/* Final total */}
<div className="flex justify-between text-xl font-bold">
<span>Yhteensä</span>
<span>{(finalTotal / 100).toFixed(2)} €</span>
</div>
</div>
);
}Order Confirmation
After checkout, the order includes discount information:
interface Order {
// ... other fields
discountCodeValue: string | null; // "SUMMER20"
discountAmount: number | null; // Amount in cents
discountVatRate: number | null; // VAT rate used for calculation
}Display on confirmation page:
{order.discountCodeValue && (
<div className="flex justify-between text-green-600">
<span>Alennuskoodi ({order.discountCodeValue})</span>
<span>-{(order.discountAmount! / 100).toFixed(2)} €</span>
</div>
)}Error Handling
import {
StorefrontError,
getDiscountApplyErrorMessage
} from '@putiikkipalvelu/storefront-sdk';
try {
await storefront.discountCode.apply({
code: "INVALIDCODE",
cartId,
cartItems,
campaigns
});
} catch (error) {
if (error instanceof StorefrontError) {
// Get localized error message
const message = getDiscountApplyErrorMessage(error.code, 'fi');
toast.error(message);
// Handle specific error codes
switch (error.code) {
case 'NOT_FOUND':
// Code doesn't exist
break;
case 'EXPIRED':
// Code has expired
break;
case 'MIN_ORDER_NOT_MET':
// Cart total too low
break;
case 'CAMPAIGN_ACTIVE':
// Campaign discount takes priority
break;
}
}
}