Common Gift Cards Bugs and How to Catch Them
Common Gift Cards Bugs and How to Catch Them
Common Gift Cards Bugs and How to Catch Them
Gift card functionality is a frequent source of post‑release incidents because it touches payments, state management, and user‑facing codes all at once. A single oversight — such as accepting a malformed code or mis‑calculating a remaining balance — can lead to revenue loss, customer‑support overload, or compliance violations. This guide walks through the most common bug patterns, shows how to reproduce each one, explains manual and automated detection techniques, and offers concrete fixes that prevent regressions. By the end you will have a test matrix, a set of ready‑to‑run code snippets, and a release‑gate checklist you can bookmark for future work.
Common Gift Cards Bugs and How to Catch Them
Why gift cards are prone to defects
Gift‑card systems often start as a thin wrapper around a payment gateway, then accrue features like partial redemption, reloadable balances, expiry dates, and multi‑currency support. Each new feature adds state that must be persisted atomically. When developers treat the card as a simple string lookup, they overlook concurrency, validation edge cases, and audit‑trail requirements. The result is a class of bugs that only surface under specific user behaviors — such as rapid successive redemptions, use of special characters in codes, or attempts to redeem an expired card after a system clock change.
Impact on users and business
From the user’s perspective, a bug can manifest as a rejected valid card, a balance that shows zero after a successful reload, or an error message that leaks internal identifiers. For the business, the same defect can enable fraud (e.g., code guessing), cause chargebacks when a card is redeemed twice, or trigger regulatory fines if expiry handling violates local consumer‑protection laws. Detecting these issues early saves both reputation and money.
Common Gift Cards Bugs and How to Catch Them
Manual test matrix for gift‑card flows
| # | Test scenario | Preconditions | Steps | Expected result | Common failure mode |
|---|---|---|---|---|---|
| 1 | Valid code redemption | Card with $50 balance, not expired | 1. Enter code on checkout page 2. Submit | Balance reduced by purchase amount, receipt shown | Code rejected despite being valid |
| 2 | Invalid code format | No card exists | 1. Enter code containing spaces or symbols 2. Submit | Clear “invalid code” message | System crashes or returns 500 |
| 3 | Duplicate redemption attempt | Card already redeemed for full amount | 1. Redeem card once (success) 2. Attempt same code again | Second attempt fails with “already used” | Balance goes negative or second redemption succeeds |
| 4 | Balance calculation after partial use | Card $100, purchase $30 | 1. Redeem $30 2. Check balance | Balance displayed as $70 | Balance shows $100 or $0 |
| 5 | Expiry handling | Card expires today at 23:59 UTC | 1. Attempt redemption at 23:58 UTC 2. Attempt again at 00:02 UTC | First succeeds, second fails with “expired” | Both attempts succeed or both fail |
| 6 | Reload after expiry | Expired card, reload $20 | 1. Attempt reload 2. Check balance | Reload rejected, balance unchanged | Reload succeeds, creating a zombie card |
| 7 | Concurrent redemptions | Two devices, same card $50 | 1. Device A initiates $30 redemption 2. Device B initiates $30 redemption within 200 ms | One succeeds, other fails with insufficient funds | Both succeed, resulting in negative balance |
| 8 | Locale‑specific formatting | User locale = ja_JP, currency = JPY | 1. View card details 2. Attempt redemption | Amount displayed without decimal places, correct symbol | Amount shows .00 or wrong symbol |
| 9 | Accessibility – screen reader | Card entry field | 1. Navigate with Tab 2. Activate screen reader | Field announces “gift‑card code, required entry” | Field unlabeled or announces generic “edit text” |
| 10 | API rate‑limit abuse | No authentication on /redeem endpoint | 1. Send 100 rapid POST requests with random codes | Requests throttled, return 429 after threshold | Server processes all requests, enabling brute force |
Save this table as a reference when drafting test cases; each row can be turned into a manual exploratory session or an automated assertion.
Step‑by‑step reproduction guides for each bug pattern
Pattern 1 – Invalid or duplicated codes
- Generate a UUID‑like string that does not exist in the database.
- Submit it via the UI or the
/api/v1/giftcard/redeemendpoint. - Observe whether the system returns a clear 400‑level error with a user‑friendly message or whether it throws an unhandled exception (500).
Pattern 2 – Balance calculation errors
- Load a card with a known amount (e.g., $123.45).
- Perform a purchase of a non‑round amount (e.g., $12.34).
- Query the balance endpoint and compare the returned value to the expected $111.11 using a decimal‑aware assertion (avoid floating‑point equality).
Pattern 3 – Expiration date handling
- Set a card’s
expires_atto the current UTC time minus one second. - Attempt redemption; expect failure.
- Change the system clock (or mock time) to a test) to a moment before expiry and retry; expect success.
- Verify that the transition is instantaneous — no window where an expired card is still accepted.
Pattern 4 – Redemption flow race conditions
- Use two parallel threads or two browser tabs pointing to the same card.
- Both threads send a redemption request for half the balance within a 100 ms window.
- Check final balance: it should be zero, not negative, and only one transaction should be recorded as successful.
Pattern 5 – Security issues (code guessing, brute force)
- Disable any CAPTCHA or rate‑limit on the redemption endpoint.
- Write a script that iterates over a predictable code space (e.g., all 6‑digit numeric codes).
- Measure the rate of successful guesses; if the system allows more than a few attempts per minute without throttling, it is vulnerable.
Pattern 6 – Accessibility barriers
- Navigate to the gift‑card entry page using only the keyboard.
- Activate a screen reader (NVDA, VoiceOver, TalkBack).
- Confirm that the input field has an accessible name, that error messages are announced, and that focus moves appropriately after submission.
Pattern 7 – Locale and currency formatting
- Change the user’s profile locale to
fr_FRand currency to EUR. - View a card with a balance of €1 234,56 (note the comma as decimal separator).
- Ensure the UI displays the amount exactly as
1 234,56 €(non‑breaking space as thousands separator).
Pattern 8 – Integration with payment gateways (partial refunds, chargebacks)
- Redeem a card for $50, then trigger a refund of $20 via the gateway’s API.
- Verify that the card’s balance increases by $20 and that a refund transaction is recorded.
- Simulate a chargeback by sending a webhook that marks the original transaction as disputed; ensure the card balance is adjusted accordingly and that the user receives a notification.
Common Gift Cards Bugs and How to Catch Them
Automated detection approaches (unit, integration, API, UI)
Unit tests – pure logic
@Test
void balanceAfterPartialRedemption() {
GiftCard card = new GiftCard("ABC123", new Money(100, Currency.USD));
card.redeem(new Money(30, Currency.USD));
assertEquals(new Money(70, Currency.USD), card.getBalance());
}
*Key points*: use a money library that avoids floating‑point drift; test edge cases like redeeming the exact balance and redeeming zero.
Integration tests – database + service layer
@Test
fun `duplicate redemption fails`() {
val card = giftCardRepository.save(GiftCardEntity(code = "XYZ789", balanceCents = 5000))
val redeemReq = RedeemRequest(code = "XYZ789", amountCents = 3000)
// first call
val firstResp = giftCardService.redeem(redeemReq)
assertTrue(firstResp.isSuccess)
// second call
val secondResp = giftCardService.redeem(redeemReq)
assertFalse(secondResp.isSuccess)
assertEquals("ALREADY_USED", secondResp.errorCode)
}
*Key points*: wrap the service call in a transaction and flush after each operation to mimic real commit behavior.
API contract tests
# Using curl and jq to validate response schema
curl -s -X POST https://api.example.com/v1/giftcard/redeem \
-H "Content-Type: application/json" \
-d '{"code":"VALID12","amount":1500}' | \
jq -e '.success == true and .newBalanceCents == 3500'
Add this to a CI pipeline; any deviation in HTTP status or JSON structure fails the build.
UI tests – Playwright (web) / Appium (Android)
@pytest.mark.asyncio
async def test_expiration_blocks_redemption(page):
await page.goto("/giftcard/redeem")
await page.fill("#code", "EXPIRED1")
await page.click("#submit")
# expect error toast
assert await page.is_visible(".toast.error:has-text('Expired')")
Run the same test with the system clock mocked via a library like fake‑timer to verify the boundary condition.
Property‑based testing – generate random codes
Using Hypothesis (Python) or fast-check (JS) you can assert that *any* string that does not match the regex ^[A-Z0-9]{8,12}$ results in a 400 response, dramatically increasing coverage of invalid inputs.
Example test scripts (code snippets)
Postman collection snippet for rate‑limit check
{
"info": {
"_postman_id": "abcd1234",
"name": "Gift‑Card Rate Limit",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
},
"item": [
{
"name": "Rapid redeem attempts",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test(\"Status should be 429 after 20 requests\", function () {",
" if (pm.iterationCount > 20) {",
" pm.response.to.have.status(429);",
" }",
"});"
]
}
}
],
"request": {
"method": "POST",
"url": {
"raw": "{{baseUrl}}/giftcard/redeem",
"host": ["{{baseUrl}}"],
"path": ["giftcard","redeem"]
},
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\"code\":\"BADCODE\",\"amount\":100}"
}
}
}
]
}
Run the collection with Newman and iterate 100 times; the test will fail if the endpoint does not throttle.
Taxonomy of Gift Card Bug Patterns
Pattern 1: Invalid or duplicated codes
*Why it happens*: The validation layer trusts the frontend to send only “well‑formed” strings, or the database unique constraint is deferred until commit, allowing a race window.
*User impact*: Confusion, support tickets, possible loss of trust.
*Detection*: Send deliberately malformed strings (spaces, emojis, Unicode control characters) and verify a clear 400 response. Use a uniqueness test that attempts to insert the same code twice within the same transaction.
*Fix*: Centralize validation in a service method annotated with @Transactional and enforce a unique index on the code column. Return a standardized error payload.
Pattern 2: Balance calculation errors
*Why it happens*: Using float or double for monetary values, forgetting to apply tax or discounts before updating the balance, or mishandling negative amounts during refunds.
*User impact*: Over‑ or under‑charging, leading to revenue discrepancy.
*Detection*: Unit test with BigDecimal (Java) or Decimal (Python) and assert exact cent‑level results. Property‑based test that randomizes purchase amounts and verifies initial - purchase = remaining.
*Fix*: Adopt a money library everywhere, make balance updates atomic, and store amounts as integer cents.
Pattern 3: Expiration date handling
*Why it happens*: Comparing dates with >= instead of >, storing expiry as a date‑only value and ignoring time‑zone, or relying on client‑side date without server validation.
*User impact*: Valid cards rejected near midnight, or expired cards accepted in certain locales.
*Detection*: Use a mockable clock (e.g., java.time.Clock) to set the system time to exactly the expiry boundary and both sides of it.
*Fix*: Store expiry as an offset‑date‑time in UTC, compare using Instant.now().isBefore(expiry), and enforce the check in a service layer that all redemption paths call.
Pattern 4: Redemption flow race conditions
*Why it happens*: The service reads the balance, calculates the new balance, then writes it back without locking the row. Two concurrent requests can read the same stale balance.
*User impact*: Negative balances, enabling fraudsters to extract more value than the card holds.
*Detection*: Use a tool like golang.org/x/sync/errgroup or JUnit’s ExecutorService to fire N parallel redemption requests and assert that the sum of successful amounts ≤ initial balance.
*Fix*: Perform the update in a single SQL statement (UPDATE giftcard SET balance = balance - ? WHERE code = ? AND balance >= ?) and check the affected row count. This makes the operation atomic at the DB level.
Pattern 5: Security issues (code guessing, brute force)
*Why it happens*: No rate limiting, no CAPTCHA, and codes are sequentially generated or too short.
*User impact*: Attackers can enumerate valid cards and drain them.
*Detection*: Run a script that sends 1 000 requests with random codes and measure responses per second; if the endpoint returns 200 for any guess, the space is too small.
*Fix*: Implement exponential back‑off or a fixed limit (e.g., 5 attempts per minute per IP), add a CAPTCHA after failures, and use a cryptographically random code space of at least 12 alphanumeric characters.
Pattern 6: Accessibility barriers
*Why it happens*: Input fields lack aria-label, error messages are inserted as plain text without role="alert", or custom widgets do not support keyboard navigation.
*User impact*: Users relying on assistive technology cannot complete the flow.
*Detection*: Run axe-core or Lighthouse in CI; manually navigate with Tab and verify focus order.
*Fix*: Add proper labels, ensure live regions for errors, and test with real screen‑reader users.
Pattern 7: Locale and currency formatting
*Why it happens*: Hard‑coded decimal point, ignoring grouping separators, or using NumberFormat.getInstance() without specifying locale.
*User impact*: Users misread amounts, leading to abandoned carts or support calls.
*Detection*: Parameterize tests with a list of locales (en_US, de_DE, ja_JP, ar_SA) and assert that the rendered string matches the expected pattern from NumberFormat.getCurrencyInstance(locale).
*Fix*: Always format money through a locale‑aware utility; store the raw integer cents separately from the display string.
Pattern 8: Integration with payment gateways (partial refunds, chargebacks)
*Why it happens*: Refund logic updates the gateway but neglects to adjust the internal gift‑card ledger, or chargeback webhooks are ignored.
*User impact*: Balance shows funds that have already been withdrawn, leading to negative account reconciliation.
*Detection*: End‑to‑end test that triggers a refund via a stubbed gateway and asserts the card balance increase. Use a webhook simulator to send a disputed transaction and verify the balance decrement and notification.
*Fix*: Make refund and chargeback handlers call the same ledger‑update service used for redemptions, and persist a ledger entry for each financial event.
Detecting Bugs with Persona‑Driven Autonomous Exploration
How SUSA simulates different user personas
SUSA builds a behavioral model for each persona — curious, impatient, novice, adversarial, elderly, accessibility, power user — and drives the app accordingly. For gift‑card flows, the curious persona will try edge‑case characters in the code field, the impatient persona will spam the submit button, the adversarial persona will brute‑force short codes, and the accessibility persona will navigate solely with a screen reader. Because the explorer does not rely on pre‑written scripts, it can discover combinations of actions that a scripted test would never consider, such as entering a valid code, then immediately changing the locale before confirming redemption.
Example of a bug found only by adversarial persona
In a recent run against a retail client’s Android app, SUSA’s adversarial profile generated 500 random six‑digit numeric strings and submitted them via the gift‑card redeem endpoint. The backend returned a 200 response for three of those strings, revealing that the code‑generation algorithm used a predictable linear congruential generator with a small modulus. The defect was missed by the team’s unit tests because they only validated the format, not the entropy of the generated space. After the finding, the team switched to a cryptographically secure random generator and added a rate‑limit, eliminating the vulnerability.
Configuring SUSA for gift‑card flow
- Upload the APK or provide the web URL of the checkout page that contains the gift‑card widget.
- Enable the “payment” and “voucher” tags in the persona config so the explorer knows to interact with the code input and submit button.
- Set a custom data source for valid codes (if you have a test‑only batch) to let the curious persona try both valid and invalid values.
- Run a session of at least 15 minutes; Susa will automatically retry failed actions with different timings and will log any crash, ANR, or error toast.
- Review the generated report; look for entries labeled “Invalid code accepted”, “Balance negative after redemption”, or “Accessibility focus lost”. Each entry includes a reproducible step‑by‑step trace that can be copied into a manual test or turned into an automated assertion.
Because Susa learns from each run, subsequent executions will avoid previously explored dead ends and focus on new combinations, increasing the likelihood of catching regressions that slip past static test suites.
Manual vs Automated Trade‑offs
| Aspect | Manual exploratory testing | Automated scripted testing |
|---|---|---|
| Speed to find new bugs | High – tester can improvise on the fly | Low – limited to pre‑defined scenarios |
| Regression safety | Low – relies on human memory | High – runs on every commit |
| Cost per test execution | Moderate – requires skilled tester | Low after initial script creation |
| Coverage of edge cases | Very high – especially with persona‑driven tools | Medium – depends on test design |
| Maintenance overhead | Low – no code to maintain | High – scripts break with UI changes |
| Best use case | Early‑stage feature validation, bug‑bash sessions | Release‑gate, nightly regression, CI pipelines |
A balanced strategy uses manual exploration to discover novel failure modes (especially those tied to user behavior) and then encodes the discovered patterns into automated checks for ongoing protection.
Fixing and Preventing Gift Card Defects
Code‑level fixes (idempotency, transactions)
- Wrap balance updates in a single
UPDATE … WHERE … RETURNINGstatement to guarantee atomicity. - Make redemption endpoints idempotent by accepting an
Idempotency-Keyheader and storing the outcome keyed by that value; repeat requests return the stored result instead of reprocessing. - Validate all inputs early (regex for code,
BigDecimalfor amount) and return a uniform error envelope ({error:{code,message}}) to avoid leaking stack traces.
Design‑level safeguards (state machines, limits)
- Model the gift‑card life‑cycle as a finite state machine (
ISSUED → ACTIVE → REDEEMED_PARTIAL → REDEEMED_FULL → EXPIRED). Transitions are only allowed via explicit events, preventing illegal state changes (e.g., reloading an expired card). - Enforce daily and per‑user limits on redemption attempts and total value moved; these limits live in a centralized policy service that both the API and any background jobs consult.
- Store an audit log entry for every balance mutation (including refunds and chargebacks) with immutable fields (timestamp, actor, previous balance, delta, new balance). This enables forensic analysis when a discrepancy appears.
Monitoring and alerting in production
- Emit a metric
giftcard.balance_deltafor every adjustment; alert if the sum of deltas over a 5‑minute window deviates from zero by more than a configurable threshold (e.g., $500). - Track the rate of
400 Bad Requestresponses on the redeem endpoint; a sudden rise indicates either a bug in client validation or an active probing attack. - Use distributed tracing to follow a redemption request from the API gateway through the loyalty service to the database; look for spans where the DB update reports zero rows affected (sign of a race condition).
- Set up a daily reconciliation job that compares the sum of all gift‑card balances stored in the ledger against the total amount recorded in the payment gateway’s settlement report; any mismatch triggers a PagerDuty alert.
Quick Checklist for Release Gate
Pre‑release verification
- [ ] All unit tests for money handling pass with exact‑cent assertions.
- [ ] Integration test suite includes duplicate‑redemption, expiry‑boundary, and concurrent‑redeem scenarios.
- [ ] API contract tests verify 400 responses for malformed codes and 429 after rate‑limit threshold.
- [ ] Playwright/Appium smoke script runs a full redeem‑refund cycle on a test card.
- [ ] SUSA autonomous exploration has been executed with all eight personas for at least 10 minutes, and no crash/ANR/high‑severity error is reported.
- [ ] Accessibility scan (axe‑core) returns zero violations on the gift‑card page.
- [ ] Locale‑specific rendering test passes for
en_US,fr_FR,ja_JP,ar_SA.
Post‑release smoke
- [ ] Verify that a newly issued card shows the correct balance in the user wallet.
- [ ] Attempt to redeem an expired card; ensure a clear “expired” message and no balance change.
- [ ] Perform a partial redemption, then check that the balance updates correctly and the transaction appears in the history.
- [ ] Trigger a refund via the gateway simulator and confirm the ledger reflects the increase.
- [ ] Monitor the
giftcard.balance_deltametric for the first 30 minutes after release; alert if absolute sum > $100.
Common Gift Cards Bugs and How to Catch Them
Real‑world case study (example from a retailer)
A mid‑size e‑commerce company launched a reloadable gift‑card feature for its holiday campaign. Two weeks after launch, the finance team noticed a $12 k discrepancy between the total value of cards sold and the sum of balances reported by the loyalty service. Investigation revealed three concurrent issues:
- Race condition on reload – The reload endpoint read the current balance, added the reload amount, then wrote it back. When two reload requests arrived within 30 ms, both read the same base balance, causing the final balance to reflect only one reload.
- Incorrect tax application – The system added sales tax to the reload amount before storing it, but the customer‑facing UI displayed the pre‑tax amount, leading users to believe they had less money than they actually did.
- Missing expiry check on reload – Reload requests were accepted for cards whose
expires_atwas in the past, effectively extending the life of expired cards without any validation.
The team applied the fixes described earlier: switched the reload to an atomic UPDATE … SET balance = balance + ? WHERE code = ? AND expires_at > NOW(), removed tax from the stored value (tax is now calculated at checkout), and added a server‑side expiry validator that rejects any operation on an expired card. After deploying the patch, the nightly reconciliation job showed zero variance for three consecutive cycles, and customer‑support tickets related to gift‑card balances dropped by 80 %.
Lessons learned and best practices
- Treat money as an integer from the moment it enters the system; never convert to float for storage or calculations.
- Make every balance‑mutating endpoint idempotent and atomic; rely on the database’s unique constraints and
UPDATE … WHEREpatterns rather than application‑level locks. - Validate state at the service boundary, not only at the UI layer. Expiry, status, and ownership checks must be performed by the same code path that writes to the ledger.
- Leverage persona‑driven exploration early in the sprint to surface usage patterns that scripted tests miss; convert each discovered bug into an automated regression guard.
- Instrument reconciliation as a continuous process, not a monthly batch; early detection limits financial exposure.
By internalizing these practices, you can turn gift‑card functionality from a source of anxiety into a reliable, revenue‑positive feature that delights users and auditors alike.
---
*Keep this guide bookmarked; the matrix, code snippets, and checklist will serve as a ready reference whenever you touch gift‑card code, and the persona‑driven exploration tactics will help you stay ahead of the bugs that only real users reveal.*
Test Your App Autonomously
Upload your APK or URL. SUSA explores like 10 real users — finds bugs, accessibility violations, and security issues. No scripts.
Try SUSA Free