Skip to main content

Gift Card System

Complete implementation guide for the Gift Card system in SalonN Z, covering purchase, redemption, and balance tracking.

Overview

The Gift Card system allows salons to sell prepaid cards that customers can purchase for themselves or as gifts. Cards have monetary value that can be redeemed against services.

Key Features

  • Flexible Pricing: Purchase price can differ from card value
  • Service Restrictions: Limit cards to specific services
  • Occasion Themes: Customize cards for events (birthdays, holidays, etc.)
  • Recipient Options: Send to someone else or keep for yourself
  • Balance Tracking: Real-time tracking of redemptions and remaining balance
  • Expiration Management: Configure expiration periods
  • Tax Configuration: Optional service tax on purchases

Admin Panel Configuration

Gift Card Management

Location: Admin Panel → Settings → Gift Cards → Manage Gift Cards

File: Frontend-Admin-Panel/pages/setting/gift-card/manage-giftcard/index.vue

Creating a Gift Card

Required Fields

FieldTypeDescription
nameVARCHARDisplay name (e.g., "Happy Birthday Card")
priceDECIMALPurchase price for the card
valueDECIMALMonetary value loaded on card
occasions_idSELECTOccasion type (birthday, anniversary, etc.)
service_expiration_timeSTRINGExpiration period (e.g., "12 months")
statusTOGGLEActive (1) / Inactive (0)

Optional Fields

FieldTypeDescription
imageFILECustom card image
occasion_imagesVARCHARTheme image URLs
charge_service_taxTOGGLEApply service tax to purchase (0/1)
available_onlineTOGGLEShow in user app (0/1)
terms_and_conditionHTMLCard-specific terms
services[]MULTI-SELECTRestrict to specific services

Configuration Examples

Example 1: Simple Gift Card

Scenario: Basic $50 gift card for any service

{
"name": "Gift Card $50",
"price": 50,
"value": 50,
"occasions_id": 1,
"service_expiration_time": "12 months",
"charge_service_tax": 0,
"available_online": 1,
"status": 1,
"services": [] // Empty = all services
}

Example 2: Promotional Gift Card

Scenario: Pay $80, get $100 value

{
"name": "Holiday Special - $100 Value",
"price": 80,
"value": 100,
"occasions_id": 5, // Holidays
"service_expiration_time": "6 months",
"charge_service_tax": 1,
"services": []
}

Result: Customer pays $80, receives card worth $100


Example 3: Service-Specific Card

Scenario: Spa services only

{
"name": "Spa Day Gift Card",
"price": 75,
"value": 75,
"occasions_id": 2,
"services": [12, 15, 18] // Only massage, facial, steam room
}

Admin API Endpoints

1. Get All Gift Cards

GET /admin/giftcard/index

Controller: GiftcardController.php::index()

Response:

{
"status": true,
"data": [
{
"id": 1,
"name": "Gift Card $50",
"price": "50",
"value": "50",
"image": "https://...",
"charge_service_tax": "0",
"occasions_id": 1,
"service_expiration_time": "12 months",
"available_online": "1",
"status": 1,
"services": [
{
"id": 5,
"name": "Haircut",
"price": "35"
}
]
}
]
}

Cache Key: giftcard_get_list


2. Create Gift Card

POST /admin/giftcard/create

Request Body:

{
"name": "Summer Special",
"price": 60,
"value": 75,
"image": "base64_encoded_image",
"charge_service_tax": 1,
"occasions_id": 3,
"service_expiration_time": "6 months",
"terms_and_condition": "<p>Valid for 6 months</p>",
"occasion_images": "https://...",
"available_online": 1,
"status": 1,
"services": [5, 10, 15]
}

Validation Rules:

  • name: Required, string
  • price: Required, numeric
  • value: Required, numeric
  • occasions_id: Required, exists in occasions table
  • services: Optional array

Backend Process (GiftcardController.php::create()):

// 1. Create gift card
$giftcard = Giftcard::create([
'name' => $request->name,
'price' => $request->price,
'value' => $request->value,
'image' => $uploaded_image_url,
'charge_service_tax' => $request->charge_service_tax,
'occasions_id' => $request->occasions_id,
'service_expiration_time' => $request->service_expiration_time,
'terms_and_condition' => $request->terms_and_condition,
'occasion_images' => $request->occasion_images,
'available_online' => $request->available_online,
'status' => $request->status
]);

// 2. Associate services
if ($request->services && count($request->services) > 0) {
foreach ($request->services as $service_id) {
GiftcardService::create([
'giftcard_id' => $giftcard->id,
'service_id' => $service_id
]);
}
}

// 3. Clear caches
clearCacheByPattern('giftcard_get_list');
clearCacheByPattern('online_giftcard_list');

3. Update Gift Card

POST /admin/giftcard/update

Additional Field: giftcard_id

Backend Process:

  1. Update gift card fields
  2. Delete existing service associations
  3. Create new service associations
  4. Clear all gift card caches

4. Delete Gift Card

POST /admin/giftcard/delete

Request: { "giftcard_id": 5 }

Cascade Deletes:

  • All giftcard_services entries (via ON DELETE CASCADE)

5. Get Purchase History

GET /admin/giftcard/purchased-giftcard

Response Structure:

{
"status": true,
"purchasedgiftcard": [
{
"id": 123,
"giftcard_number": "GC20241206001",
"name": "Gift Card $50",
"price": "50",
"value": "50",
"delivery_date": "2024-12-25",
"recipient_fname": "John",
"recipient_lname": "Doe",
"customer": {
"fname": "Jane",
"lname": "Smith"
},
"balance": 35.00, // Calculated field
"services": [...],
"history": [
{
"date": "2024-12-10",
"amount": "15.00",
"description": "Haircut service",
"service_id": "5"
}
]
}
]
}

Balance Calculation:

$balance = (float)$purchased_card->value - (float)$total_used_amount;

User App Purchase Flow

Step 1: Gift Card Listing

Page: /[slug]/giftCards/page.tsx

Features:

  • Tab switcher: "Gift Cards" / "Purchase History"
  • Displays active gift cards (available_online = 1)
  • Shows occasion images
  • Card price and value displayed

Data Source: Redux giftCardInfo

const { giftCardInfo } = useSelector((state: RootState) => state.home);

API Call: Loaded via getFrontendSettings() on app init


Step 2: Location Selection

Page: /[slug]/gift-card/page.tsx

Purpose: Select salon location for purchase

Logic:

useEffect(() => {
const init = async () => {
const locations = await getLocations();

if (locations.length === 1) {
// Auto-select and redirect
dispatch(setSelectedLocation(locations[0]));
router.push(`/${slug}/gift-card/buy-card`);
} else {
// Show location selection UI
setMultipleLocations(true);
}
};

init();
}, []);

Step 3: Card Selection & Details

Page: /[slug]/gift-card/buy-card/page.tsx

Components Used:

  • SelectGift - Toggle between Amount/Gift/Package/Membership
  • BuyCard - Display selected card preview
  • GiftCardList - Browse available cards
  • ServiceModal - Show redeemable services
  • DetailModal - Show card details and T&C

Purchase Modes:

Mode 1: "For Someone" (Gift)

Form Fields:

{
firstName: string,
lastName: string,
recipientEmail: string,
sender: string,
message: string,
date: string // Delivery date
}

UI:

<BuyingCardForm
firstName={firstName}
lastName={lastName}
email={recipientEmail}
sender={sender}
message={message}
onChange={(field, value) => {
dispatch(setFormField({ field, value }));
}}
/>

<DeliveryDate
selectedDate={date}
onDateSelect={(date) => dispatch(setDate(date))}
/>

Mode 2: "For Myself"

Form Fields:

{
date: string // Delivery date only
}

Simplified UI: Just delivery date picker


Redux State Management (buycardSlice):

interface BuyCardState {
selectedGift: "amount" | "gift" | "package" | "membership";
selectedGiftCard: GiftCard | null;
tabs: "For Someone" | "For Myself";
firstName: string;
lastName: string;
recipientEmail: string;
sender: string;
message: string;
date: string;
serviceModal: boolean;
detailModal: boolean;
}

Key Actions:

dispatch(setSelectedGift("gift"));
dispatch(setSelectedGiftCard(card));
dispatch(setTabs("For Someone"));
dispatch(setFirstName("John"));
// ... etc

Step 4: Review & Confirm

Page: /[slug]/gift-card/review-confirm/page.tsx

Displayed Information:

  1. Delivery Details

    • Formatted delivery date
    • Delivery time
  2. Recipient Information (if "For Someone")

    • First Name + Last Name
    • Email address
  3. Sender Information (if "For Someone")

    • Sender name
    • Personal message
  4. Price Summary

const calculateTotal = () => {
const subtotal = parseFloat(selectedGiftCard.price);
let tax = 0;

// Apply tax if enabled on the card AND location has tax
if (
selectedGiftCard.charge_service_tax === "1" &&
selectedLocation.service_tax
) {
tax = (subtotal * parseFloat(selectedLocation.service_tax)) / 100;
}

return {
subtotal: subtotal,
tax: tax,
total: subtotal + tax
};
};

Example Calculation:

Card Price: $50.00
Service Tax (10%): $5.00 (if charge_service_tax = 1)
----------------------------
Total: $55.00

Creating the Sale (createGiftCardSell API):

const handlePurchase = async () => {
const payload = {
location_id: selectedLocation.id,
customer_id: customer.id,
giftcard_id: selectedGiftCard.id,
name: selectedGiftCard.name,
price: selectedGiftCard.price,
value: selectedGiftCard.value,
occasion_name: selectedGiftCard.occasion?.name,
recipient_fname: tabs === "For Someone" ? firstName : customer.fname,
recipient_lname: tabs === "For Someone" ? lastName : customer.lname,
recipient_email: tabs === "For Someone" ? recipientEmail : customer.email,
service_expiration_time: selectedGiftCard.service_expiration_time,
terms_and_condition: selectedGiftCard.terms_and_condition,
occasion_images: selectedGiftCard.occasion_images,
delivery_date: date,
sender: tabs === "For Someone" ? sender : null,
message: tabs === "For Someone" ? message : null,
services: selectedGiftCard.services.map(s => s.id)
};

const response = await createGiftCardSell(payload);

if (response.status) {
// Store created sale
dispatch(setCreatedGiftCardSale(response.data));

// Proceed to payment if needed
if (totalWithTax > 0) {
handleStripePayment();
} else {
router.push(`/${slug}/gift-card/confirmed`);
}
}
};

Payment Flow (Stripe Integration):

// 1. Create payment intent
const { token } = await getToken({
user: customer.id,
location_id: selectedLocation.id,
amount: totalWithTax
});

dispatch(setPaymentToken(token));

// 2. Show payment UI (Stripe Elements)
<Card />

// 3. Confirm payment
const { paymentIntent } = await stripe.confirmPayment({
elements,
confirmParams: {
return_url: `${window.location.origin}/${slug}/gift-card/confirmed`
}
});

// 4. Update sale status
await updatePaymentStatus({
sale_id: createdSale.id,
payment_intent: paymentIntent.id
});

Step 5: Confirmation

Page: /[slug]/gift-card/confirmed/page.tsx

Displayed Information:

  • Purchase confirmation message
  • Gift card number (auto-generated)
  • Delivery date
  • Card value
  • "View in My Gift Cards" link

Backend Process:

  1. Save to purchased_gift_cards table
  2. Generate unique giftcard_number
  3. Create purchased_giftcard_services associations
  4. Send email to recipient
  5. Send confirmation email to purchaser

Backend Implementation

Gift Card Purchase API

Endpoint: POST /online/create-gift-card-sell

Controller: CustomerSalesController.php

Request Validation:

[
'location_id' => 'required|exists:locations,id',
'customer_id' => 'required|exists:customers,id',
'giftcard_id' => 'required|exists:giftcards,id',
'name' => 'required|string',
'price' => 'required|numeric',
'value' => 'required|numeric',
'delivery_date' => 'required|date',
'recipient_fname' => 'required|string',
'recipient_lname' => 'required|string',
'recipient_email' => 'nullable|email',
'services' => 'nullable|array'
]

Backend Process:

// 1. Generate unique gift card number
$card_number = 'GC' . date('Ymd') . str_pad($next_id, 3, '0', STR_PAD_LEFT);

// 2. Create purchased gift card
$purchased_card = PurchasedGiftCard::create([
'sales_id' => $sale_id, // From customer_sales table
'name' => $request->name,
'price' => $request->price,
'giftcard_number' => $card_number,
'value' => $request->value,
'occasion_name' => $request->occasion_name,
'recipient_fname' => $request->recipient_fname,
'recipient_lname' => $request->recipient_lname,
'recipient_email' => $request->recipient_email,
'service_expiration_time' => $request->service_expiration_time,
'terms_and_condition' => $request->terms_and_condition,
'occasion_images' => $request->occasion_images,
'delivery_date' => $request->delivery_date,
'customer_id' => $request->customer_id
]);

// 3. Associate services
if ($request->services) {
foreach ($request->services as $service_id) {
PurchasedGiftcardService::create([
'purchased_giftcard_id' => $purchased_card->id,
'service_id' => $service_id
]);
}
}

// 4. Send emails
send_gift_card_email($purchased_card);

// 5. Clear caches
clearCacheByPattern('purchased_giftcards');

Gift Card Redemption

Redemption Flow

When a customer books an appointment and selects a gift card as payment:

Backend Redemption Logic

File: BookingController.php or QuickSalesController.php

// 1. Validate gift card
$gift_card = PurchasedGiftCard::where('giftcard_number', $card_number)
->where('customer_id', $customer_id)
->first();

if (!$gift_card) {
return response()->json(['status' => false, 'message' => 'Invalid gift card']);
}

// 2. Calculate balance
$total_used = PurchasedGiftcardHistory::where('purchased_giftcard_id', $gift_card->id)
->sum('amount');

$balance = (float)$gift_card->value - (float)$total_used;

if ($balance < $appointment_cost) {
return response()->json(['status' => false, 'message' => 'Insufficient balance']);
}

// 3. Check service restrictions
if ($gift_card->services->count() > 0) {
$allowed_service_ids = $gift_card->services->pluck('service_id')->toArray();

foreach ($appointment_services as $service) {
if (!in_array($service->id, $allowed_service_ids)) {
return response()->json([
'status' => false,
'message' => 'Service not allowed for this gift card'
]);
}
}
}

// 4. Check expiration
if ($gift_card->service_expiration_time) {
$expiration_date = Carbon::parse($gift_card->created_at)
->addMonths($gift_card->service_expiration_time);

if (Carbon::now()->greaterThan($expiration_date)) {
return response()->json(['status' => false, 'message' => 'Gift card expired']);
}
}

// 5. Create redemption record
PurchasedGiftcardHistory::create([
'purchased_giftcard_id' => $gift_card->id,
'sales_id' => $appointment->sales_id,
'description' => 'Redeemed for ' . $service->name,
'service_id' => $service->id,
'amount' => $redemption_amount,
'date' => now()
]);

// 6. Update payment record
AppointmentPayment::create([
'appointment_id' => $appointment->id,
'payment_type_id' => 4, // Gift Card
'amount' => $redemption_amount,
'paid_amount' => $redemption_amount,
'pending_amount' => 0,
'txn_id' => $gift_card->giftcard_number,
'status' => 1
]);

Database Schema

Main Tables

1. giftcards (Master Database)

CREATE TABLE giftcards (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
price VARCHAR(255) NOT NULL,
image VARCHAR(255) NULL,
value VARCHAR(255) NOT NULL,
charge_service_tax STRING DEFAULT '0',
occasions_id BIGINT NOT NULL,
service_expiration_time STRING,
terms_and_condition LONGTEXT NULL,
occasion_images VARCHAR(255) NULL,
available_online STRING DEFAULT '1',
status BOOLEAN DEFAULT 1,
created_at TIMESTAMP,
updated_at TIMESTAMP,

FOREIGN KEY (occasions_id) REFERENCES occasions(id)
);

2. giftcard_services (Master Database)

CREATE TABLE giftcard_services (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
giftcard_id BIGINT NOT NULL,
service_id STRING NOT NULL,
created_at TIMESTAMP,
updated_at TIMESTAMP,

FOREIGN KEY (giftcard_id) REFERENCES giftcards(id) ON DELETE CASCADE
);

Purpose: Associate gift cards with specific services

Note: If no services are associated, card is valid for ALL services


3. purchased_gift_cards (Client Database)

CREATE TABLE purchased_gift_cards (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
sales_id BIGINT,
name VARCHAR(255),
price VARCHAR(255),
giftcard_number VARCHAR(255) UNIQUE,
value VARCHAR(255),
occasion_name VARCHAR(255),
recipient_fname VARCHAR(255),
recipient_lname VARCHAR(255),
recipient_email VARCHAR(255) NULL,
service_expiration_time STRING,
terms_and_condition LONGTEXT,
occasion_images VARCHAR(255),
delivery_date TIMESTAMP,
sender VARCHAR(255) NULL,
message TEXT NULL,
customer_id BIGINT,
created_at TIMESTAMP,
updated_at TIMESTAMP,

FOREIGN KEY (customer_id) REFERENCES customers(id)
);

Unique Field: giftcard_number (auto-generated, format: GC20241206001)


4. purchased_giftcard_services (Client Database)

CREATE TABLE purchased_giftcard_services (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
purchased_giftcard_id BIGINT NOT NULL,
service_id STRING NOT NULL,
created_at TIMESTAMP,
updated_at TIMESTAMP,

FOREIGN KEY (purchased_giftcard_id)
REFERENCES purchased_gift_cards(id)
ON DELETE CASCADE
);

Purpose: Copy service restrictions to purchased cards


5. purchased_giftcard_histories (Client Database)

CREATE TABLE purchased_giftcard_histories (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
purchased_giftcard_id BIGINT NOT NULL,
sales_id VARCHAR(255),
description VARCHAR(255),
service_id STRING,
amount STRING,
date STRING,
created_at TIMESTAMP,
updated_at TIMESTAMP,

FOREIGN KEY (purchased_giftcard_id)
REFERENCES purchased_gift_cards(id)
ON DELETE CASCADE
);

Purpose: Track all redemptions and balance changes

Balance Calculation:

SELECT 
pg.value AS original_value,
COALESCE(SUM(pgh.amount), 0) AS total_used,
(pg.value - COALESCE(SUM(pgh.amount), 0)) AS remaining_balance
FROM purchased_gift_cards pg
LEFT JOIN purchased_giftcard_histories pgh ON pg.id = pgh.purchased_giftcard_id
WHERE pg.id = ?
GROUP BY pg.id;

Testing Guide

Test Scenario 1: Basic Purchase

Setup:

  1. Create gift card: $50 value, $50 price
  2. Set available_online = 1

Test:

  1. Navigate to Gift Cards page
  2. Select card
  3. Click "For Myself"
  4. Select delivery date
  5. Review & Confirm
  6. Complete payment

Expected Result:

  • Sale created in purchased_gift_cards
  • Unique gift card number generated
  • Confirmation email sent
  • Card appears in "Purchase History"

Test Scenario 2: Gift Purchase

Setup: Same card as above

Test:

  1. Select card
  2. Click "For Someone"
  3. Fill recipient details
  4. Add personal message
  5. Complete purchase

Expected Result:

  • Recipient name and email saved
  • Sender and message saved
  • Recipient receives email on delivery date

Test Scenario 3: Service Restrictions

Setup:

  • Create card restricted to services [5, 10, 15]

Test:

  1. Purchase card
  2. Try to book appointment with service #20
  3. Select gift card for payment

Expected Result:

  • Error: "Service not allowed for this gift card"
  • Redemption blocked

Test Scenario 4: Redemption

Setup:

  • Purchased card with $50 balance
  • Book appointment for $30 service

Test:

  1. Select gift card as payment method
  2. Complete appointment booking

Expected Result:

  • purchased_giftcard_histories entry created
  • Amount: $30
  • Remaining balance: $20

Troubleshooting

Issue: Card not showing in user app

Causes:

  1. available_online = 0
  2. status = 0 (inactive)
  3. Cache not cleared after creation

Solution:

-- Check card status
SELECT id, name, available_online, status
FROM giftcards
WHERE id = ?;

-- Enable if needed
UPDATE giftcards
SET available_online = 1, status = 1
WHERE id = ?;

Clear cache:

clearCacheByPattern('online_giftcard_list');

Issue: Tax not calculating

Cause: charge_service_tax = 0 on card

Solution: Enable tax in admin panel


Issue: Balance calculation incorrect

Cause: Missing or duplicate history entries

Solution:

-- Verify history
SELECT * FROM purchased_giftcard_histories
WHERE purchased_giftcard_id = ?;

-- Recalculate balance
SELECT
pg.value,
SUM(pgh.amount) as used,
(pg.value - SUM(pgh.amount)) as balance
FROM purchased_gift_cards pg
LEFT JOIN purchased_giftcard_histories pgh
ON pg.id = pgh.purchased_giftcard_id
WHERE pg.id = ?;

Best Practices

For Salon Owners

  1. Set Reasonable Expiration: 12-24 months is standard
  2. Promotional Cards: Offer bonus value (pay $80, get $100)
  3. Service Restrictions: Only restrict if necessary
  4. Clear Terms: Write detailed T&C for each card type
  5. Track Balances: Review unpaid balances monthly

For Developers

  1. Validate Balances: Always check before redemption
  2. Check Expiration: Enforce expiration dates
  3. Service Restrictions: Validate against purchased_giftcard_services
  4. Unique Card Numbers: Ensure giftcard_number is unique
  5. Cascade Deletes: Properly configured in migrations

API Reference Summary

EndpointMethodPurpose
/admin/giftcard/indexGETList all gift cards
/admin/giftcard/createPOSTCreate gift card
/admin/giftcard/updatePOSTUpdate gift card
/admin/giftcard/deletePOSTDelete gift card
/admin/giftcard/purchased-giftcardGETPurchase history
/online/create-gift-card-sellPOSTPurchase gift card
/online/get-customer-gift-cardsGETCustomer's cards