Skip to main content

Bug Fix: Vouchers of the Same Type Erroneously Grouped — Must Display Individually by Code

Last Updated: 14/07/2026
Type: Bug — Voucher Deduplication Logic Error
Screen: My Vouchers (VoucherScreen) — "Unused" Tab
Severity: 🔴 High — User cannot see or use all of their vouchers

Feedback Screenshot:


1. Bug Description

The user has 2 vouchers of "Voucher 50K Healing World":

  • Voucher 1: Existing voucher (code: HW50-260702-3MQYC5, Expiry: 1/8/2026)
  • Voucher 2: Just received from Lucky Spin — has a different code and/or expiry date.

The "My Vouchers" screen → "Unused" tab displays only 1 card, completely hiding the second voucher from the user interface.

Impact: The user is unaware they have 2 vouchers and cannot use the second one.


2. Root Cause

The frontend code performs a grouping/deduplication based on the name/type (voucherType or templateId) and only renders one card representing that group. This logic is business-incorrect because each voucher is a unique entity with:

  • A unique code (e.g., HW50-260702-3MQYC5 vs another code)
  • Different validity/expiration dates (marketing spins usually have shorter expiration periods)
  • Different origins (point redemptions vs lucky spin prizes)

3. Correct Business Logic

✅ Display SEPARATELY when:

  • Same name/type but different unique codes2 separate cards
  • Same name/type but different validity start/end dates2 separate cards
  • Same name/type but from different sources2 separate cards

✅ GROUP and show quantity badge ONLY WHEN:

All of the following conditions are met simultaneously:

  1. ✅ Same name/type (templateId or voucherTypeCode)
  2. ✅ Same validity start date (validFrom)
  3. ✅ Same expiration date (expiredAt / validTo)
  4. ✅ Same terms of use (discount value, applicable services...)

When grouped: Render 1 card with a quantity badge (e.g., x2) in the top corner of the card.

❌ NEVER group when:

  • Codes are different (even if they are of the same template type)
  • Expiry dates are different (even if they share the same template ID)

4. Code Adjustments

4.1 Fix the voucher list rendering logic

// ❌ WRONG — Current logic (grouping solely by name/type)
const groupedVouchers = vouchers.reduce((acc, v) => {
const key = v.voucherTypeName; // Group by name -> WRONG
if (!acc[key]) acc[key] = v;
return acc;
}, {});

// ✅ CORRECT — Each unique code is its own entity.
// Only group when templateId + validFrom + expiredAt are identical.
const groupVouchers = (vouchers: Voucher[]): VoucherDisplayItem[] => {
const groups = new Map<string, VoucherDisplayItem>();

vouchers.forEach((v) => {
// Group key = templateId + start date + end date
const groupKey = `${v.templateId}__${v.validFrom}__${v.expiredAt}`;

if (groups.has(groupKey)) {
// Same group -> increment quantity
groups.get(groupKey)!.quantity += 1;
// Store the code (to allow the user to select which code to use upon tap)
groups.get(groupKey)!.codes.push(v.code);
} else {
// New group -> create separate card
groups.set(groupKey, {
...v,
quantity: 1,
codes: [v.code],
});
}
});

return Array.from(groups.values());
};

4.2 Display quantity badge when quantity > 1

const VoucherCard = ({ voucher }: { voucher: VoucherDisplayItem }) => (
<TouchableOpacity style={styles.card}>
{/* Quantity Badge — only render when quantity > 1 */}
{voucher.quantity > 1 && (
<View style={styles.quantityBadge}>
<Text style={styles.quantityText}>x{voucher.quantity}</Text>
</View>
)}

{/* Main Card Content */}
<VoucherCardContent voucher={voucher} />

{/* Expiration date */}
<Text style={styles.expiry}>Expiry: {formatDate(voucher.expiredAt)}</Text>
</TouchableOpacity>
);

4.3 Detail screen for grouped vouchers

When a user taps a card with quantity > 1, show the list of individual codes to choose from:

// Within VoucherDetailScreen or bottom sheet
{voucher.codes.length > 1 && (
<View>
<Text style={styles.selectCodeLabel}>Select code to use:</Text>
{voucher.codes.map((code, index) => (
<TouchableOpacity key={code} style={styles.codeItem}
onPress={() => selectCode(code)}>
<Text style={styles.codeText}>Code {index + 1}: {code}</Text>
</TouchableOpacity>
))}
</View>
)}

5. Examples

Current User Bug Case (Needs Correction):

Voucher A: "50K Healing World" | Code: HW50-260702-3MQYC5 | Expiry: 1/8/2026
Voucher B: "50K Healing World" | Code: HW50-260714-XXXXX | Expiry: 14/8/2026 (from Lucky Spin)

→ DIFFERENT EXPIRY DATES → Display 2 separate cards ✅

Groupable Case:

Voucher A: "50K Healing World" | Code: HW50-AA | Expiry: 1/8/2026 | validFrom: 1/7/2026
Voucher B: "50K Healing World" | Code: HW50-BB | Expiry: 1/8/2026 | validFrom: 1/7/2026

→ Same type + same expiry + same start date → Group into 1 card with "x2" badge ✅

Non-groupable Case with Identical Expiry:

Voucher A: "50K Healing World" | Expiry: 1/8/2026 | Origin: Point Redemption
Voucher B: "50K Healing World" | Expiry: 1/8/2026 | Origin: Lucky Spin (Different template ID)

→ Different template ID → Display 2 separate cards ✅

Technical Checklist

  • Locate and edit the grouping logic in VoucherScreen.tsx or its selector/hook.
  • Apply groupVouchers() with the composite key templateId + validFrom + expiredAt.
  • Display x{quantity} badge on cards where quantity > 1.
  • Implement the code selector in the detail screen for grouped cards.
  • Verify the API response: ensure the backend returns distinct voucher entities (no grouping on the server side).
  • Test case: Create 2 vouchers of the same type with different expiry dates → verify 2 cards are shown.
  • Test case: Create 2 vouchers of the same type, same expiry, and same validFrom → verify 1 card with a "x2" badge.
  • Test case: Use 1 of the grouped vouchers → verify quantity updates to 1 (or moves to the used tab).