Skip to main content

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

FieldTypeDescription
nameVARCHARMembership name (e.g., "Gold Membership")
priceDECIMALMonthly or yearly price
billing_periodSELECT1 = Monthly, 2 = Yearly
charce_service_taxTOGGLEApply service tax (0/1)
service_expiration_timeSTRINGService validity period
price_for_payrollSELECT1 = Regular, 2 = Discount
terms_and_conditionHTMLMembership terms
statusTOGGLEActive (1) / Inactive (0)

Discount Configuration

Service Discounts:

FieldTypeDescription
discount_serviceTOGGLEEnable service discount (0/1)
discount_percentage_serviceINTEGERDiscount % for services

Product Discounts:

FieldTypeDescription
discount_productTOGGLEEnable product discount (0/1)
discount_percentage_for_productINTEGERDiscount % for products

Service Group Fields

Similar to packages, memberships can include service groups:

FieldTypeDescription
qtyINTEGERQuantity of services in group
priceDECIMALPrice for this group
services[]MULTI-SELECTServices 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:

  1. Use 3 included services (free)
  2. Book 2 additional services: $100 → $75 (25% off)
  3. 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, string
  • price: Required, numeric
  • billing_period: Required, in:1,2
  • discount_percentage_service: Required if discount_service = 1
  • discount_percentage_for_product: Required if discount_product = 1
  • service_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:

  1. Update membership fields
  2. Delete existing groups: MembershipServiceGroup::where('membership_id', $id)->delete()
  3. Delete existing services (cascade via foreign key)
  4. Recreate groups and services
  5. Clear all membership caches

4. Delete Membership

POST /admin/membership/delete

Request: { "membership_id": 5 }

Cascade Deletes:

  • All membership_service_groups entries
  • All membership_services entries (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:

  1. Book $50 haircut
  2. Verify price shown as $40 (20% off)
  3. Complete booking
  4. 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:

  1. Book haircut
  2. Select "Use Membership Service"
  3. Verify price = $0
  4. Check remaining = 1

Expected Result: Service deducted from membership balance


Test Scenario 3: Billing Period

Setup:

  • Monthly membership purchased on Jan 15

Test:

  1. Verify first charge: Jan 15
  2. Check next_billing_date = Feb 15
  3. Simulate renewal on Feb 15
  4. Verify second charge processed

Expected Result: Automatic monthly renewal


Best Practices

For Salon Owners

  1. Discount Strategy: 15-30% service discount is standard
  2. Billing: Monthly for most customers, yearly for 10-15% extra savings
  3. Included Services: 1-3 services/month for premium tiers
  4. Product Discounts: 10-20% to encourage retail sales
  5. Clear Terms: Specify cancellation policy and renewal terms

For Developers

  1. Active Check: Always verify membership is active before applying discounts
  2. Expiration: Check service_expiration_time for included services
  3. Discount Priority: Membership discounts override other promotions
  4. Recurring Billing: Implement with Stripe Subscriptions API
  5. History Tracking: Record every discount and service usage

API Reference Summary

EndpointMethodPurpose
/admin/membership/indexGETList all memberships
/admin/membership/storePOSTCreate membership
/admin/membership/updatePOSTUpdate membership
/admin/membership/deletePOSTDelete membership
/admin/membership/get-purchased-membershipGETPurchase history
/online/create-membership-sellPOSTPurchase membership
/online/get-customer-membershipsGETCustomer's memberships