Membership System
Complete implementation guide for the Membership system in Salonnz, covering subscription-based service access, discount structures, and billing management.
Overview
The Membership system provides subscription-based access to salon services with built-in discounts. Unlike packages, memberships focus on recurring billing and percentage-based discounts rather than fixed quantities.
Key Features
- Subscription Billing: Monthly or yearly recurring payments
- Dual Discount System: Separate discounts for services and products
- Service Groups: Organize included services into groups with quantities
- Percentage-Based Discounts: Apply % discounts to services/products
- Tax Configuration: Optional tax application
- Expiration Management: Control service validity periods
- Usage Tracking: Monitor service utilization
Admin Panel Configuration
Membership Management
Location: Admin Panel → Settings → Memberships → Manage Memberships
File: Frontend-Admin-Panel/pages/setting/membership/index.vue
Creating a Membership
Membership-Level Fields
| Field | Type | Description |
|---|---|---|
name | VARCHAR | Membership name (e.g., "Gold Membership") |
price | DECIMAL | Monthly or yearly price |
billing_period | SELECT | 1 = Monthly, 2 = Yearly |
charce_service_tax | TOGGLE | Apply service tax (0/1) |
service_expiration_time | STRING | Service validity period |
price_for_payroll | SELECT | 1 = Regular, 2 = Discount |
terms_and_condition | HTML | Membership terms |
status | TOGGLE | Active (1) / Inactive (0) |
Discount Configuration
Service Discounts:
| Field | Type | Description |
|---|---|---|
discount_service | TOGGLE | Enable service discount (0/1) |
discount_percentage_service | INTEGER | Discount % for services |
Product Discounts:
| Field | Type | Description |
|---|---|---|
discount_product | TOGGLE | Enable product discount (0/1) |
discount_percentage_for_product | INTEGER | Discount % for products |
Service Group Fields
Similar to packages, memberships can include service groups:
| Field | Type | Description |
|---|---|---|
qty | INTEGER | Quantity of services in group |
price | DECIMAL | Price for this group |
services[] | MULTI-SELECT | Services included |
Note: Service groups provide included services, while discounts apply to all bookings.
Configuration Examples
Example 1: Simple Discount Membership
Scenario: Gold membership - 20% off all services, monthly billing
{
"name": "Gold Membership",
"price": 49,
"billing_period": 1, // Monthly
"charce_service_tax": 1,
"service_expiration_time": "1 month",
"discount_service": 1,
"discount_percentage_service": 20,
"discount_product": 0,
"discount_percentage_for_product": 0,
"status": 1,
"service_group": [] // No included services, only discounts
}
How It Works:
- Customer pays $49/month
- Gets 20% off every service booking
- Example: $50 haircut becomes $40 with membership
- Renews automatically monthly
Example 2: Premium Membership with Included Services
Scenario: Platinum membership - included services + discounts
{
"name": "Platinum Membership",
"price": 149,
"billing_period": 1,
"discount_service": 1,
"discount_percentage_service": 25,
"discount_product": 1,
"discount_percentage_for_product": 15,
"service_group": [
{
"qty": 3,
"price": 100,
"services": [5, 10] // 3 free haircuts or massages/month
}
]
}
Benefits:
- $149/month
- 3 free services from selected list
- 25% off additional services
- 15% off all products
- Total value: ~$300+ for $149
Example 3: Yearly Membership
Scenario: Annual plan with bigger discount
{
"name": "VIP Annual Membership",
"price": 499,
"billing_period": 2, // Yearly
"service_expiration_time": "12 months",
"discount_service": 1,
"discount_percentage_service": 30,
"discount_product": 1,
"discount_percentage_for_product": 20,
"service_group": [
{
"qty": 12,
"price": 300,
"services": [5, 10, 15, 20] // 12 services/year
}
]
}
Benefits:
- $499/year (~$42/month)
- 12 included services
- 30% off all services
- 20% off all products
- Best value for loyal customers
Billing Periods Explained
Monthly Billing (billing_period = 1)
Behavior: Membership renews every month
Example:
- Purchase date: 2024-01-15
- First billing: $49 (immediately)
- Next billing: 2024-02-15 ($49)
- Subsequent: Every 15th of the month
Use Case: Regular clients who visit monthly
Yearly Billing (billing_period = 2)
Behavior: Membership renews every year
Example:
- Purchase date: 2024-01-15
- First billing: $499 (immediately)
- Next billing: 2025-01-15 ($499)
- Subsequent: Annually on Jan 15
Use Case: Committed customers wanting better rates
Discount System Explained
Service Discounts
How It Works: Percentage discount applied to service bookings
Calculation:
const regularPrice = 50; // Haircut
const membershipDiscount = 20; // 20% off
const discountedPrice = regularPrice * (1 - membershipDiscount / 100);
// $50 * (1 - 0.20) = $40
Applied At: Booking time (when scheduling appointments)
Product Discounts
How It Works: Percentage discount on retail product purchases
Calculation:
const productPrice = 30; // Shampoo
const membershipDiscount = 15; // 15% off
const discountedPrice = productPrice * (1 - membershipDiscount / 100);
// $30 * (1 - 0.15) = $25.50
Applied At: Point of sale (when purchasing products)
Combined Benefits
Scenario: Platinum membership ($149/month)
- Service discount: 25%
- Product discount: 15%
- Included services: 3/month
Monthly Usage:
- Use 3 included services (free)
- Book 2 additional services: $100 → $75 (25% off)
- Buy products: $50 → $42.50 (15% off)
Total Value: $249.50 value for $149
Admin API Endpoints
1. Get All Memberships
GET /admin/membership/index
Controller: MembershipController.php::index()
Response:
{
"status": true,
"data": [
{
"id": 1,
"name": "Gold Membership",
"price": "49",
"billing_period": "1",
"charce_service_tax": "1",
"discount_service": "1",
"discount_percentage_service": "20",
"discount_product": "0",
"discount_percentage_for_product": "0",
"service_expiration_time": "1 month",
"status": 1,
"service_group": [
{
"id": 15,
"membership_id": 1,
"qty": 3,
"price": "100",
"services": [...]
}
]
}
]
}
Cache Key: membership_get_list
2. Create Membership
POST /admin/membership/store
Request Body:
{
"name": "Platinum Membership",
"price": 149,
"image": "base64_encoded_image",
"billing_period": 1,
"charce_service_tax": 1,
"service_expiration_time": "1 month",
"discount_service": 1,
"discount_percentage_service": 25,
"discount_product": 1,
"discount_percentage_for_product": 15,
"price_for_payroll": 2,
"terms_and_condition": "<p>Terms here</p>",
"status": 1,
"service_group": [
{
"qty": 3,
"price": 100,
"service": [5, 10, 15]
}
]
}
Validation Rules:
name: Required, stringprice: Required, numericbilling_period: Required, in:1,2discount_percentage_service: Required ifdiscount_service = 1discount_percentage_for_product: Required ifdiscount_product = 1service_group: Optional array
Backend Process (MembershipController.php::store()):
// 1. Create membership
$membership = Membership::create([
'name' => $request->name,
'price' => $request->price,
'image' => $uploaded_image_url,
'billing_period' => $request->billing_period,
'charce_service_tax' => $request->charce_service_tax,
'discount_service' => $request->discount_service,
'discount_percentage_service' => $request->discount_percentage_service,
'discount_product' => $request->discount_product,
'discount_percentage_for_product' => $request->discount_percentage_for_product,
'price_for_payroll' => $request->price_for_payroll,
'service_expiration_time' => $request->service_expiration_time,
'terms_and_condition' => $request->terms_and_condition,
'status' => $request->status
]);
// 2. Create service groups (if any)
if ($request->service_group && count($request->service_group) > 0) {
foreach ($request->service_group as $sg) {
$membership_group = MembershipServiceGroup::create([
'membership_id' => $membership->id,
'qty' => $sg['qty'],
'price' => $sg['price']
]);
// 3. Link services to group
foreach ($sg['service'] as $service_id) {
MembershipService::create([
'group_id' => $membership_group->id,
'membership_id' => $membership->id,
'service_id' => $service_id
]);
}
}
}
// 4. Clear caches
clearCacheByPattern('membership_get_list');
clearCacheByPattern('online_membership_list');
3. Update Membership
POST /admin/membership/update
Additional Field: membership_id
Backend Process:
- Update membership fields
- Delete existing groups:
MembershipServiceGroup::where('membership_id', $id)->delete() - Delete existing services (cascade via foreign key)
- Recreate groups and services
- Clear all membership caches
4. Delete Membership
POST /admin/membership/delete
Request: { "membership_id": 5 }
Cascade Deletes:
- All
membership_service_groupsentries - All
membership_servicesentries (via ON DELETE CASCADE)
5. Get Purchase History
GET /admin/membership/get-purchased-membership
Response Structure:
{
"status": true,
"purchasedmemberships": [
{
"id": 789,
"name": "Gold Membership",
"price": "49",
"billing_period": "1",
"discount_service": "1",
"discount_percentage_service": "20",
"discount_product": "0",
"customer": {
"fname": "John",
"lname": "Doe"
},
"total_qty": 3,
"total_used": 1,
"remaining_balance": 2,
"service_group": [...],
"history": [
{
"date": "2024-12-10",
"description": "Used for Haircut",
"service_id": "5"
}
]
}
]
}
Balance Calculation (for included services):
$total_qty = PurchasedMembershipServiceGroup::where('purchased_membership_id', $id)
->sum('qty');
$total_used = PurchasedMembershipHistory::where('purchased_membership_id', $id)
->where('service_id', '!=', 0)
->count();
$remaining_balance = $total_qty - $total_used;
User App Purchase Flow
Step 1: Membership Listing
Page: /[slug]/memberships/page.tsx
Features:
- Tab switcher: "Memberships" / "Purchase History"
- Displays active memberships
- Shows billing period (Monthly/Yearly)
- Displays discount percentages
- Shows included services (if any)
Data Source: Redux membershipData
const { membershipData } = useSelector((state: RootState) => state.home);
Component: MembershipCardList with type="mem"
Step 2: Membership Details
Page: /[slug]/memberships/membershipdetail/page.tsx
Displayed Information:
- Membership name and price
- Billing period indicator
- Discount Information:
- Service discount: "Get 20% off all services"
- Product discount: "Get 15% off all products"
- Included Services (if any):
- Service groups with quantities
- List of services in each group
- Terms and conditions
- "Buy Now" button
Discount Display:
{membership.discount_service === "1" && (
<div className="discount-badge">
<Icon icon="mdi:tag-percent" />
<span>{membership.discount_percentage_service}% off services</span>
</div>
)}
{membership.discount_product === "1" && (
<div className="discount-badge">
<Icon icon="mdi:shopping" />
<span>{membership.discount_percentage_for_product}% off products</span>
</div>
)}
Step 3: Purchase Flow (Unified)
Page: /[slug]/gift-card/buy-card/page.tsx
Selection: selectedGift = "membership"
Redux State:
dispatch(setSelectedGift("membership"));
dispatch(setSelectedGiftCard(membershipData));
Same Form Modes:
- "For Someone" - Gift membership to another person
- "For Myself" - Purchase for self
Membership-Specific Data:
{
id: number,
name: string,
price: string,
billing_period: string, // "1" or "2"
discount_service: string,
discount_percentage_service: string,
discount_product: string,
discount_percentage_for_product: string,
service_expiration_time: string,
service_group: ServiceGroup[]
}
Step 4: Review & Confirm
Page: /[slug]/gift-card/review-confirm/page.tsx
Price Calculation:
const calculateTotal = () => {
const subtotal = parseFloat(selectedMembership.price);
let tax = 0;
// Apply tax if enabled
if (
selectedMembership.charce_service_tax === "1" &&
selectedLocation.service_tax
) {
tax = (subtotal * parseFloat(selectedLocation.service_tax)) / 100;
}
return {
subtotal: subtotal,
tax: tax,
total: subtotal + tax
};
};
Summary Display:
- Billing period highlighted
- Discount benefits listed
- Included services (if any)
- Price with tax
- Renewal information
Creating the Sale (createMembershipSell API):
const handlePurchase = async () => {
const payload = {
location_id: selectedLocation.id,
customer_id: customer.id,
membership_id: selectedMembership.id,
name: selectedMembership.name,
price: selectedMembership.price,
billing_period: selectedMembership.billing_period,
charce_service_tax: selectedMembership.charce_service_tax,
discount_service: selectedMembership.discount_service,
discount_percentage_service: selectedMembership.discount_percentage_service,
discount_product: selectedMembership.discount_product,
discount_percentage_for_product: selectedMembership.discount_percentage_for_product,
service_expiration_time: selectedMembership.service_expiration_time,
terms_and_condition: selectedMembership.terms_and_condition,
buy_for: tabs === "For Someone" ? 1 : 0,
recipient_fname: tabs === "For Someone" ? firstName : null,
recipient_lname: tabs === "For Someone" ? lastName : null,
recipient_email: tabs === "For Someone" ? recipientEmail : null,
sender_name: tabs === "For Someone" ? sender : null,
sender_msg: tabs === "For Someone" ? message : null,
service_groups: selectedMembership.service_group.map(g => ({
qty: g.qty,
price: g.price,
services: g.services.map(s => s.service_id)
}))
};
const response = await createMembershipSell(payload);
if (response.status) {
// Handle Stripe payment
if (totalWithTax > 0) {
handleStripePayment();
} else {
router.push(`/${slug}/memberships/confirmed`);
}
}
};
Step 5: Confirmation
Page: /[slug]/memberships/confirmed/page.tsx
Displayed Information:
- Purchase confirmation
- Membership details
- Discount percentages
- Included services summary
- Billing cycle start date
- Next renewal date
- "View My Memberships" link
Backend Implementation
Membership Purchase API
Endpoint: POST /online/create-membership-sell
Controller: CustomerSalesController.php
Backend Process:
// 1. Create purchased membership
$purchased_membership = PurchasedMembership::create([
'sales_id' => $sale_id,
'name' => $request->name,
'price' => $request->price,
'billing_period' => $request->billing_period,
'charce_service_tax' => $request->charce_service_tax,
'discount_service' => $request->discount_service,
'discount_percentage_service' => $request->discount_percentage_service,
'discount_product' => $request->discount_product,
'discount_percentage_for_product' => $request->discount_percentage_for_product,
'service_expiration_time' => $request->service_expiration_time,
'terms_and_condition' => $request->terms_and_condition,
'buy_for' => $request->buy_for,
'recipient_fname' => $request->recipient_fname,
'recipient_lname' => $request->recipient_lname,
'recipient_email' => $request->recipient_email,
'sender_name' => $request->sender_name,
'sender_msg' => $request->sender_msg,
'customer_id' => $request->customer_id
]);
// 2. Copy service groups (if any)
if ($request->service_groups && count($request->service_groups) > 0) {
foreach ($request->service_groups as $group) {
$purchased_group = PurchasedMembershipServiceGroup::create([
'purchased_membership_id' => $purchased_membership->id,
'qty' => $group['qty'],
'price' => $group['price']
]);
// 3. Copy services to purchased membership
foreach ($group['services'] as $service_id) {
PurchasedMembershipService::create([
'purchased_membership_id' => $purchased_membership->id,
'service_id' => $service_id
]);
}
}
}
// 4. Set up recurring billing (if Stripe recurring enabled)
// ... Stripe subscription logic
// 5. Send notifications
send_membership_email($purchased_membership);
// 6. Clear caches
clearCacheByPattern('purchased_memberships');
Membership Usage & Discounts
Applying Service Discounts at Booking
When a customer with an active membership books a service:
Discount Application Logic
Backend: BookingController.php
// 1. Check for active membership
$membership = PurchasedMembership::where('customer_id', $customer_id)
->where('status', 1)
->first();
if (!$membership) {
// No membership, use regular price
$final_price = $service->price;
} else {
// Apply membership discount
if ($membership->discount_service == '1') {
$discount_percent = (float)$membership->discount_percentage_service;
$discount_amount = ($service->price * $discount_percent) / 100;
$final_price = $service->price - $discount_amount;
// Record discount applied
$appointment->membership_discount = $discount_amount;
$appointment->membership_id = $membership->id;
} else {
$final_price = $service->price;
}
}
// Save appointment with discounted price
$appointment->final_price = $final_price;
$appointment->save();
Using Included Services
For memberships with service groups:
// 1. Check membership has included services
$membership = PurchasedMembership::with(['serviceGroups', 'services'])
->where('id', $membership_id)
->first();
if ($membership->serviceGroups->isEmpty()) {
// No included services, only discounts
return applyDiscount($service);
}
// 2. Check remaining quantity
$total_qty = PurchasedMembershipServiceGroup::where('purchased_membership_id', $membership->id)
->sum('qty');
$total_used = PurchasedMembershipHistory::where('purchased_membership_id', $membership->id)
->where('service_id', '!=', 0)
->count();
$remaining = $total_qty - $total_used;
if ($remaining <= 0) {
return response()->json(['status' => false, 'message' => 'No included services remaining']);
}
// 3. Check service eligibility
$allowed_services = PurchasedMembershipService::where('purchased_membership_id', $membership->id)
->pluck('service_id')
->toArray();
if (!in_array($selected_service_id, $allowed_services)) {
// Service not included, apply discount instead
return applyDiscount($service);
}
// 4. Use included service (no charge)
PurchasedMembershipHistory::create([
'purchased_membership_id' => $membership->id,
'sales_id' => $appointment->sales_id,
'description' => 'Used included service: ' . $service->name,
'service_id' => $service->id,
'date' => now()
]);
$appointment->final_price = 0; // Free with membership
$appointment->payment_type = 'membership_included';
$appointment->save();
Database Schema
Main Tables
1. memberships (Master Database)
CREATE TABLE memberships (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
charce_service_tax STRING DEFAULT '0',
image VARCHAR(255) NULL,
price VARCHAR(255) NOT NULL,
billing_period STRING NOT NULL,
discount_service STRING DEFAULT '0',
discount_percentage_service VARCHAR(255) NULL,
discount_product STRING DEFAULT '0',
discount_percentage_for_product VARCHAR(255) NULL,
price_for_payroll STRING DEFAULT '1',
service_expiration_time STRING NULL,
terms_and_condition LONGTEXT NULL,
status BOOLEAN DEFAULT 1,
created_at TIMESTAMP,
updated_at TIMESTAMP
);
2. membership_service_groups (Master Database)
CREATE TABLE membership_service_groups (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
membership_id BIGINT NOT NULL,
qty INTEGER NOT NULL,
price INTEGER NOT NULL,
created_at TIMESTAMP,
updated_at TIMESTAMP,
FOREIGN KEY (membership_id) REFERENCES memberships(id) ON DELETE CASCADE
);
3. membership_services (Master Database)
CREATE TABLE membership_services (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
group_id BIGINT NOT NULL,
membership_id BIGINT NOT NULL,
service_id STRING NOT NULL,
created_at TIMESTAMP,
updated_at TIMESTAMP,
FOREIGN KEY (group_id) REFERENCES membership_service_groups(id) ON DELETE CASCADE,
FOREIGN KEY (membership_id) REFERENCES memberships(id) ON DELETE CASCADE
);
4. purchased_memberships (Client Database)
CREATE TABLE purchased_memberships (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
sales_id BIGINT,
name VARCHAR(255),
charce_service_tax STRING,
price VARCHAR(255),
billing_period STRING,
discount_service STRING,
discount_percentage_service VARCHAR(255),
discount_product STRING,
discount_percentage_for_product VARCHAR(255),
price_for_payroll STRING,
service_expiration_time STRING,
terms_and_condition LONGTEXT,
buy_for INTEGER DEFAULT 0,
recipient_fname VARCHAR(255) NULL,
recipient_lname VARCHAR(255) NULL,
recipient_email VARCHAR(255) NULL,
customer_id BIGINT,
sender_name VARCHAR(255) NULL,
sender_msg VARCHAR(255) NULL,
purchased_from VARCHAR(255) NULL,
created_at TIMESTAMP,
updated_at TIMESTAMP,
FOREIGN KEY (customer_id) REFERENCES customers(id)
);
5-7. Additional Tables
Similar structure to packages for service groups, services, and histories.
Testing Guide
Test Scenario 1: Discount Application
Setup:
- Create membership: $49/month, 20% service discount
- Customer purchases membership
Test:
- Book $50 haircut
- Verify price shown as $40 (20% off)
- Complete booking
- Check appointment record has
membership_discount = $10
Expected Result: Discount automatically applied
Test Scenario 2: Included Services
Setup:
- Membership with 3 included haircuts/month
- Customer has 2 remaining
Test:
- Book haircut
- Select "Use Membership Service"
- Verify price = $0
- Check remaining = 1
Expected Result: Service deducted from membership balance
Test Scenario 3: Billing Period
Setup:
- Monthly membership purchased on Jan 15
Test:
- Verify first charge: Jan 15
- Check next_billing_date = Feb 15
- Simulate renewal on Feb 15
- Verify second charge processed
Expected Result: Automatic monthly renewal
Best Practices
For Salon Owners
- Discount Strategy: 15-30% service discount is standard
- Billing: Monthly for most customers, yearly for 10-15% extra savings
- Included Services: 1-3 services/month for premium tiers
- Product Discounts: 10-20% to encourage retail sales
- Clear Terms: Specify cancellation policy and renewal terms
For Developers
- Active Check: Always verify membership is active before applying discounts
- Expiration: Check service_expiration_time for included services
- Discount Priority: Membership discounts override other promotions
- Recurring Billing: Implement with Stripe Subscriptions API
- History Tracking: Record every discount and service usage
API Reference Summary
| Endpoint | Method | Purpose |
|---|---|---|
/admin/membership/index | GET | List all memberships |
/admin/membership/store | POST | Create membership |
/admin/membership/update | POST | Update membership |
/admin/membership/delete | POST | Delete membership |
/admin/membership/get-purchased-membership | GET | Purchase history |
/online/create-membership-sell | POST | Purchase membership |
/online/get-customer-memberships | GET | Customer's memberships |
Related Documentation
- Gift Cards - Gift card system
- Packages - Package system
- Payment Integration - Stripe setup
- Database Schema - Complete database reference