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
| Field | Type | Description |
|---|---|---|
name | VARCHAR | Display name (e.g., "Happy Birthday Card") |
price | DECIMAL | Purchase price for the card |
value | DECIMAL | Monetary value loaded on card |
occasions_id | SELECT | Occasion type (birthday, anniversary, etc.) |
service_expiration_time | STRING | Expiration period (e.g., "12 months") |
status | TOGGLE | Active (1) / Inactive (0) |
Optional Fields
| Field | Type | Description |
|---|---|---|
image | FILE | Custom card image |
occasion_images | VARCHAR | Theme image URLs |
charge_service_tax | TOGGLE | Apply service tax to purchase (0/1) |
available_online | TOGGLE | Show in user app (0/1) |
terms_and_condition | HTML | Card-specific terms |
services[] | MULTI-SELECT | Restrict 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, stringprice: Required, numericvalue: Required, numericoccasions_id: Required, exists in occasions tableservices: 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:
- Update gift card fields
- Delete existing service associations
- Create new service associations
- Clear all gift card caches
4. Delete Gift Card
POST /admin/giftcard/delete
Request: { "giftcard_id": 5 }
Cascade Deletes:
- All
giftcard_servicesentries (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/MembershipBuyCard- Display selected card previewGiftCardList- Browse available cardsServiceModal- Show redeemable servicesDetailModal- 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:
-
Delivery Details
- Formatted delivery date
- Delivery time
-
Recipient Information (if "For Someone")
- First Name + Last Name
- Email address
-
Sender Information (if "For Someone")
- Sender name
- Personal message
-
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:
- Save to
purchased_gift_cardstable - Generate unique
giftcard_number - Create
purchased_giftcard_servicesassociations - Send email to recipient
- 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:
- Create gift card: $50 value, $50 price
- Set
available_online = 1
Test:
- Navigate to Gift Cards page
- Select card
- Click "For Myself"
- Select delivery date
- Review & Confirm
- 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:
- Select card
- Click "For Someone"
- Fill recipient details
- Add personal message
- 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:
- Purchase card
- Try to book appointment with service #20
- 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:
- Select gift card as payment method
- Complete appointment booking
Expected Result:
purchased_giftcard_historiesentry created- Amount: $30
- Remaining balance: $20
Troubleshooting
Issue: Card not showing in user app
Causes:
available_online = 0status = 0(inactive)- 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
- Set Reasonable Expiration: 12-24 months is standard
- Promotional Cards: Offer bonus value (pay $80, get $100)
- Service Restrictions: Only restrict if necessary
- Clear Terms: Write detailed T&C for each card type
- Track Balances: Review unpaid balances monthly
For Developers
- Validate Balances: Always check before redemption
- Check Expiration: Enforce expiration dates
- Service Restrictions: Validate against
purchased_giftcard_services - Unique Card Numbers: Ensure
giftcard_numberis unique - Cascade Deletes: Properly configured in migrations
API Reference Summary
| Endpoint | Method | Purpose |
|---|---|---|
/admin/giftcard/index | GET | List all gift cards |
/admin/giftcard/create | POST | Create gift card |
/admin/giftcard/update | POST | Update gift card |
/admin/giftcard/delete | POST | Delete gift card |
/admin/giftcard/purchased-giftcard | GET | Purchase history |
/online/create-gift-card-sell | POST | Purchase gift card |
/online/get-customer-gift-cards | GET | Customer's cards |
Related Documentation
- Packages - Package system documentation
- Memberships - Membership system
- Payment Integration - Stripe setup
- Database Schema - Complete database reference