Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 23 additions & 5 deletions POS/src/components/sale/CouponDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,14 @@ const props = defineProps({
required: true,
note: __("Cart subtotal BEFORE tax - used for discount calculations"),
},
taxAmount: {
type: Number,
default: 0,
},
grandTotal: {
type: Number,
default: 0,
},
items: Array,
posProfile: String,
customer: String,
Expand Down Expand Up @@ -257,6 +265,14 @@ function applyGiftCard(card) {
applyCoupon()
}

function getCouponBaseAmount(coupon) {
const grandTotal = Number.parseFloat(props.grandTotal || 0)
const taxAmount = Number.parseFloat(props.taxAmount || 0)
const netTotal = Math.max(grandTotal - taxAmount, 0)

return coupon.apply_on === "Grand Total" ? grandTotal : netTotal
}

async function applyCoupon() {
if (!couponCode.value.trim()) {
errorMessage.value = __("Please enter a coupon code")
Expand All @@ -282,9 +298,10 @@ async function applyCoupon() {
}

const coupon = validationData.coupon
const baseAmount = getCouponBaseAmount(coupon)

// Check minimum amount (on subtotal before tax)
if (coupon.min_amount && props.subtotal < coupon.min_amount) {
// Check minimum amount on the configured coupon base
if (coupon.min_amount && baseAmount < coupon.min_amount) {
errorMessage.value = __('This coupon requires a minimum purchase of ', [formatCurrency(coupon.min_amount)])
showWarning(errorMessage.value)
return
Expand All @@ -297,15 +314,15 @@ async function applyCoupon() {
amount: coupon.discount_type === "Amount" ? coupon.discount_amount : 0,
}

let discountAmount = calculateDiscountAmount(discountObj, props.subtotal)
let discountAmount = calculateDiscountAmount(discountObj, baseAmount)

// Apply maximum discount limit if specified
if (coupon.max_amount && discountAmount > coupon.max_amount) {
discountAmount = coupon.max_amount
}

// Clamp discount to subtotal to prevent negative totals
discountAmount = Math.min(discountAmount, props.subtotal)
// Clamp discount to the selected coupon base to prevent negative totals
discountAmount = Math.min(discountAmount, baseAmount)

appliedDiscount.value = {
name: coupon.coupon_name || coupon.coupon_code,
Expand All @@ -315,6 +332,7 @@ async function applyCoupon() {
type: coupon.discount_type,
coupon: coupon,
apply_on: coupon.apply_on,
base_amount: baseAmount,
}

emit("discount-applied", appliedDiscount.value)
Expand Down
13 changes: 9 additions & 4 deletions POS/src/composables/useInvoice.js
Original file line number Diff line number Diff line change
Expand Up @@ -534,12 +534,17 @@ export function useInvoice() {
// Store coupon code for tracking
couponCode.value = discount.code || discount.name

const baseAmount =
typeof discount.base_amount === "number"
? discount.base_amount
: subtotal.value

// Use centralized calculation to handle percentage/amount and clamping
let discountAmount = calculateDiscountAmount(discount, subtotal.value)
let discountAmount = calculateDiscountAmount(discount, baseAmount)

// Clamp discount to subtotal (cannot exceed total)
if (discountAmount > subtotal.value) {
discountAmount = subtotal.value
// Clamp discount to the same base the coupon was calculated against
if (discountAmount > baseAmount) {
discountAmount = baseAmount
}

// Ensure non-negative
Expand Down
2 changes: 2 additions & 0 deletions POS/src/pages/POSSale.vue
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,8 @@
<CouponDialog
v-model="uiStore.showCouponDialog"
:subtotal="cartStore.subtotal"
:tax-amount="cartStore.totalTax"
:grand-total="cartStore.grandTotal"
:items="cartStore.invoiceItems"
:pos-profile="shiftStore.profileName"
:customer="cartStore.customer?.name || cartStore.customer"
Expand Down
Loading