Add shipping cost management and checkout shipping fees.

Business store shipping groups/static rates UI, and customer checkout that quotes and applies shipping on delivery and payment.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-10-02 21:52:47 +03:30
co-authored by Cursor
parent 95390933a7
commit c59ca4444f
38 changed files with 5353 additions and 98 deletions
@@ -30,6 +30,54 @@ function totalQuantity(order: Order) {
return order.items.reduce((sum, item) => sum + item.quantity, 0)
}
function asRecord(value: unknown): Record<string, unknown> | null {
return value && typeof value === 'object' && !Array.isArray(value)
? (value as Record<string, unknown>)
: null
}
function parseOrderShipping(shippingAddress: Record<string, unknown>) {
const root = asRecord(shippingAddress) ?? {}
const deliveryMode =
root.deliveryMode === 'pickup' || root.deliveryMode === 'delivery'
? root.deliveryMode
: root.province === 'Pickup'
? 'pickup'
: 'delivery'
const shipping = asRecord(root.shipping)
const groupsRaw = Array.isArray(shipping?.groups) ? shipping.groups : []
const groups = groupsRaw
.map((entry) => {
const row = asRecord(entry)
if (!row) return null
const groupId = typeof row.groupId === 'string' ? row.groupId : ''
if (!groupId) return null
return {
groupId,
title: typeof row.title === 'string' ? row.title : '',
methodLabel: typeof row.methodLabel === 'string' ? row.methodLabel : null,
amount: typeof row.amount === 'number' && Number.isFinite(row.amount) ? row.amount : 0,
itemNames: Array.isArray(row.itemNames)
? row.itemNames.filter((name): name is string => typeof name === 'string')
: [],
}
})
.filter((group): group is NonNullable<typeof group> => Boolean(group))
const addressParts = [
typeof root.province === 'string' ? root.province : '',
typeof root.city === 'string' ? root.city : '',
typeof root.address === 'string' ? root.address : '',
typeof root.postalCode === 'string' ? root.postalCode : '',
].filter((part) => part && part !== 'Pickup' && part !== 'Store' && part !== 'In-store pickup')
return {
deliveryMode,
addressLine: addressParts.join(' · '),
groups,
}
}
export function OrderItemsModal({ open, order, onClose }: OrderItemsModalProps) {
const t = useT()
const [mounted, setMounted] = useState(open)
@@ -105,6 +153,47 @@ export function OrderItemsModal({ open, order, onClose }: OrderItemsModalProps)
</span>
</div>
{(() => {
const shippingInfo = parseOrderShipping(order.shippingAddress)
return (
<div className={styles.shippingBlock}>
<div className={styles.shippingSectionTitle}>{t('orderItems.delivery')}</div>
{shippingInfo.deliveryMode === 'pickup' ? (
<p className={styles.addressText}>{t('orderItems.pickup')}</p>
) : shippingInfo.addressLine ? (
<p className={styles.addressText}>{shippingInfo.addressLine}</p>
) : null}
{shippingInfo.groups.length > 0 ? (
<ul className={styles.shippingGroupList}>
{shippingInfo.groups.map((group) => (
<li key={group.groupId} className={styles.shippingGroupCard}>
<div className={styles.shippingGroupHeader}>
<span className={styles.shippingGroupTitle}>{group.title}</span>
<span className={styles.shippingGroupAmount}>
{group.amount > 0
? formatIrtPrice(group.amount)
: t('orderItems.freeShipping')}
</span>
</div>
{group.methodLabel ? (
<div className={styles.shippingMethod}>{group.methodLabel}</div>
) : null}
{group.itemNames.length > 0 ? (
<ul className={styles.shippingItemNames}>
{group.itemNames.map((name) => (
<li key={`${group.groupId}-${name}`}>{name}</li>
))}
</ul>
) : null}
</li>
))}
</ul>
) : null}
</div>
)
})()}
{order.items.length === 0 ? (
<p className={styles.empty}>{t('orderItems.empty')}</p>
) : (
@@ -144,15 +233,35 @@ export function OrderItemsModal({ open, order, onClose }: OrderItemsModalProps)
</div>
)}
<div className={styles.summary}>
<span className={styles.summaryLabel}>
{t('orderItems.summary', {
count: itemCount,
itemsLabel:
itemCount === 1 ? t('orderItems.item') : t('orderItems.itemsPlural'),
})}
</span>
<span className={styles.summaryValue}>{formatIrtPrice(order.total)}</span>
<div className={styles.totalsBlock}>
<div className={styles.totalsRow}>
<span>{t('orderItems.subtotal')}</span>
<span className={styles.totalsValue}>{formatIrtPrice(order.subtotal)}</span>
</div>
<div className={styles.totalsRow}>
<span>{t('orderItems.shipping')}</span>
<span className={styles.totalsValue}>
{order.shippingTotal > 0
? formatIrtPrice(order.shippingTotal)
: t('orderItems.freeShipping')}
</span>
</div>
{order.discountTotal > 0 ? (
<div className={styles.totalsRow}>
<span>{t('orderItems.discount')}</span>
<span className={styles.totalsValue}>{formatIrtPrice(order.discountTotal)}</span>
</div>
) : null}
<div className={`${styles.totalsRow} ${styles.totalsRowGrand}`}>
<span>
{t('orderItems.summary', {
count: itemCount,
itemsLabel:
itemCount === 1 ? t('orderItems.item') : t('orderItems.itemsPlural'),
})}
</span>
<span className={styles.summaryValue}>{formatIrtPrice(order.total)}</span>
</div>
</div>
<div className={modalStyles.actions}>