Deposit System
Complete technical implementation guide for the deposit payment system and cancellation policy features in Salonnz.
Overview
The deposit system allows salons to require partial upfront payments for appointments, reducing no-shows and securing bookings. The cancellation policy system ensures customers agree to salon terms before finalizing bookings.
Deposit System (deposit_enable)
What It Controls
The deposit_enable setting in frontend_settings table acts as a global master switch for deposit functionality:
- Enabled (1): Deposits can be collected when services have
payment_type = 1 - Disabled (0): All deposits are disabled, regardless of service settings
How It Works
Three-Level Implementation
Frontend Implementation
Step 1: Settings Fetch
File: Frontend-Userapp-Web/app/[slug]/booking/page.tsx
// Fetch booking settings on page load
const { data } = await getBookingSettings();
// Store in Redux
dispatch(setBookingSettings(data));
Response Structure:
{
"deposit_enable": 1,
"save_card_enable": 1,
"pay_later_enable": 0,
"STRIPE_KEY": "pk_test_..."
}
Step 2: Deposit Calculation
File: Frontend-Userapp-Web/app/[slug]/booking/review-confirm/page.tsx:131-163
The checkDeposit() function runs automatically when the review page loads:
const checkDeposit = async () => {
let deposit_amount = 0;
// Loop through all selected services
services.forEach((service) => {
// Payment Type 1: Deposit
if (service.payment_type == 1) {
deposit_amount += parseInt(service.min_amount);
}
// Payment Type 3: Pay Later (collect full amount)
if (service.payment_type == 3) {
deposit_amount += parseInt(service.price);
dispatch(setPayLaterOption(true));
}
});
// Store calculated amount
dispatch(setDepositAmount(deposit_amount));
// Create Stripe payment intent if deposit required
if (deposit_amount > 0) {
createNewDeposit(deposit_amount);
}
};
Logic Breakdown:
| Service payment_type | min_amount | price | Deposit Collected |
|---|---|---|---|
| 0 (Pay at Salon) | $10 | $50 | $0 (no deposit) |
| 1 (Deposit) | $10 | $50 | $10 (partial) |
| 2 (Save Card) | N/A | $50 | $0 (card saved only) |
| 3 (Pay Later) | N/A | $50 | $50 (full amount) |
Example Calculation:
// Booking: Haircut ($35, payment_type=1, min_amount=$10)
// + Facial ($60, payment_type=1, min_amount=$20)
deposit_amount = $10 + $20 = $30
total_price = $35 + $60 = $95
remaining_balance = $95 - $30 = $65 (pay at salon)
Step 3: Payment Intent Creation
File: Frontend-Userapp-Web/app/[slug]/booking/review-confirm/page.tsx:75-91
const createNewDeposit = async (deposit_amount: number) => {
await getToken({
user: customer.id,
location_id: selectedLocation.id,
amount: deposit_amount,
})
.then((response: any) => {
if (response.status) {
// Store Stripe client secret
dispatch(setPaymentToken(response.token));
}
})
.catch((error: any) => {
console.error("Error creating payment intent:", error);
});
};
Backend API: POST /payment/create-order
Controller: PaymentController.php::create()
public function create(Request $request)
{
$frontendSettings = FrontendSettings::first();
$stripe = new StripeClient($frontendSettings->STRIPE_SECRET);
$paymentIntent = $stripe->paymentIntents->create([
'amount' => $request['amount'] * 100, // Convert to cents
'currency' => 'usd',
'automatic_payment_methods' => ['enabled' => true],
'description' => 'Appointment deposit payment',
'shipping' => [
'name' => $customer->fname . ' ' . $customer->lname,
'address' => [
'line1' => $location->address_line_1,
'city' => $location->city,
'state' => $location->state,
'postal_code' => $location->zipcode,
'country' => 'US'
]
]
]);
return response()->json([
'status' => true,
'token' => $paymentIntent->client_secret
]);
}
Step 4: Payment Method Display
File: Frontend-Userapp-Web/app/_components/PaymentMethod/PaymentMethod.tsx:24-87
The component automatically determines which payment options to show:
useEffect(() => {
if (services.length > 0) {
services.forEach((service) => {
// Deposit option
if (
service.payment_type === 1 &&
bookingSettings?.deposit_enable === 1
) {
setPaymentType(1);
dispatch(setPaymentTypeValue(1));
dispatch(setPayBtn(true)); // Enable pay button
}
// Save Card option
else if (
service.payment_type === 2 &&
bookingSettings?.save_card_enable === 1
) {
setPaymentType(2);
dispatch(setPaymentTypeValue(2));
dispatch(setPayBtn(true));
}
// Pay Later option
else if (
service.payment_type === 3 &&
bookingSettings?.pay_later_enable === 1
) {
setPaymentType(3);
dispatch(setPaymentTypeValue(3));
dispatch(setPayBtn(false)); // No payment needed now
}
});
}
}, [services, bookingSettings]);
Rendered UI:
{/* Option 1: Pay at Salon */}
{(paymentType === 0 || paymentType === 3) && (
<div onClick={() => dispatch(setPaymentMethod(0))}>
<Icon icon="tabler:credit-card" />
<h4>Pay at Salon</h4>
</div>
)}
{/* Option 2: Add Deposit */}
{(paymentType === 1 || paymentType === 3) && (
<div onClick={() => dispatch(setPaymentMethod(1))}>
<Icon icon="ph:plus-circle-fill" />
<p>Add Deposit</p>
<p>To secure your appointment add deposit</p>
{/* Expand Stripe payment form when selected */}
{paymentMethod === 1 && depositAmount > 0 && (
<Card /> {/* Stripe Elements */}
)}
</div>
)}
{/* Option 3: Save Card */}
{paymentType === 2 && (
<div onClick={() => dispatch(setPaymentMethod(2))}>
<Icon icon="tabler:credit-card" />
<h4>Save Card</h4>
<Card />
</div>
)}
Step 5: Stripe Payment Confirmation
File: Frontend-Userapp-Web/app/_components/PaymentMethod/CheckoutForm.jsx
const handleSubmit = async (e) => {
e.preventDefault();
if (!stripe || !elements) return;
// For deposit payments (payment_type = 1)
if (paymentType === 1) {
const { paymentIntent, error } = await stripe.confirmPayment({
elements,
confirmParams: {
return_url: `${window.location.origin}/${slug}/booking/confirmed`
},
redirect: 'if_required'
});
if (error) {
console.error("Payment failed:", error.message);
setMessage(error.message);
}
else if (paymentIntent.status === 'succeeded') {
// Update appointment status
await updatePaymentStatus({
appointment: apptId,
payment_intent: paymentIntent.id
});
// Navigate to confirmation
router.push(`/${slug}/booking/confirmed`);
}
}
// For save card (payment_type = 2)
else if (paymentType === 2) {
const { setupIntent, error } = await stripe.confirmSetup({
elements,
confirmParams: {
return_url: confirmationUrl
},
redirect: 'if_required'
});
if (error) {
console.error("Card save failed:", error.message);
}
else if (setupIntent.status === 'succeeded') {
// Save payment method ID for future use
// ...
}
}
};
Backend Implementation
Appointment Creation with Deposit
File: BookingController.php::saveBooking()
When an appointment is created, the deposit details are saved:
// Create appointment record
$appointment = Appointment::create([
'location_id' => $request->location_id,
'customer_id' => $request->customer_id,
'date' => $request->date,
'time' => $request->time,
'status' => 0, // Pending payment
'deposit_amount' => $deposit_amount
]);
// Create payment records
if ($deposit_amount > 0) {
// Deposit payment (type_id = 1)
AppointmentPayment::create([
'appointment_id' => $appointment->id,
'payment_type_id' => 1,
'amount' => $deposit_amount,
'paid_amount' => 0, // Not paid yet
'pending_amount' => $deposit_amount,
'status' => 0
]);
// Remaining balance (type_id = 0 - Pay at Salon)
AppointmentPayment::create([
'appointment_id' => $appointment->id,
'payment_type_id' => 0,
'amount' => $total_price - $deposit_amount,
'paid_amount' => 0,
'pending_amount' => $total_price - $deposit_amount,
'status' => 0
]);
}
Payment Status Update
File: PaymentController.php::updatePaymentStatus()
After Stripe confirms payment:
public function updatePaymentStatus(Request $request)
{
$stripe = new StripeClient($frontendSettings->STRIPE_SECRET);
$output = $stripe->paymentIntents->retrieve($request->payment_intent);
$appointment = Appointment::where('appointment_id', $request->appointment)
->first();
if ($output->status == 'succeeded') {
// Update appointment status
$appointment->update(['status' => 1]); // Confirmed
// Update deposit payment record
$payment = AppointmentPayment::where('appointment_id', $appointment->id)
->where('payment_type_id', 1);
if ($payment->exists()) {
$payment->update([
'paid_amount' => $output->amount / 100, // Convert from cents
'pending_amount' => 0,
'txn_id' => $output->client_secret,
'status' => 1 // Paid
]);
} else {
// Create new payment record
AppointmentPayment::create([
'appointment_id' => $appointment->id,
'payment_type_id' => 1,
'amount' => $output->amount / 100,
'paid_amount' => $output->amount / 100,
'pending_amount' => 0,
'txn_id' => $output->client_secret,
'status' => 1
]);
}
// Send confirmation emails
// ... (email logic)
}
else {
// Payment failed
$appointment->update(['status' => 5]); // Payment Failed
AppointmentPayment::where('appointment_id', $appointment->id)
->where('payment_type_id', 1)
->update(['status' => 2]); // Failed
}
}
Database Schema
appointment_payments Table
CREATE TABLE appointment_payments (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
appointment_id BIGINT NOT NULL,
payment_type_id INT NOT NULL, -- 0=Pay at Salon, 1=Deposit, 2=Card on File
amount DECIMAL(10,2), -- Total amount for this payment type
paid_amount DECIMAL(10,2), -- Amount actually paid
pending_amount DECIMAL(10,2), -- Amount remaining
txn_id VARCHAR(255), -- Stripe payment intent ID
status INT DEFAULT 0, -- 0=Pending, 1=Paid, 2=Failed
created_at TIMESTAMP,
updated_at TIMESTAMP,
FOREIGN KEY (appointment_id) REFERENCES appointments(id) ON DELETE CASCADE
);
Example Data:
-- Appointment with $30 deposit, $95 total
-- Appointment ID: 1234
INSERT INTO appointment_payments VALUES
(1, 1234, 1, 30.00, 30.00, 0.00, 'pi_3O8...', 1, NOW(), NOW()), -- Deposit paid
(2, 1234, 0, 65.00, 0.00, 65.00, NULL, 0, NOW(), NOW()); -- Balance pending
Cancellation Policy (cancellation_key)
What It Controls
The cancellation_key field determines if customers must agree to cancellation terms before booking:
- Enabled (1): Checkbox appears on review page; must be checked to proceed
- Disabled (0): No policy agreement required
Frontend Implementation
Checkbox Display
File: Frontend-Userapp-Web/app/[slug]/booking/review-confirm/page.tsx:409-424
{frontendSettings?.cancellation_key == 1 && (
<div
className="policies_check_box selected"
onClick={() => dispatch(setCancellationPolicy(!cancellationPolicy))}
>
<RadioSelector
isActive={cancellationPolicy || false}
checkCondition={true}
size={20}
checkSize={12}
/>
I agree to cancel policies.
</div>
)}
Visual Appearance:
☐ I agree to cancel policies.
When clicked:
☑ I agree to cancel policies.
Policy Content Display
File: Frontend-Userapp-Web/app/[slug]/booking/review-confirm/page.tsx:442-448
Below the checkbox, the full policy text is displayed:
{depositPolicyStore && (
<div
className="deposit_policy"
dangerouslySetInnerHTML={{ __html: depositPolicyStore }}
/>
)}
Fetching Policy (lines 169-182):
useEffect(() => {
const fetchData = async () => {
if (!depositPolicyStore) {
try {
setPolicyLoad(true);
const data = await getAppContent(4); // Type 4 = deposit_policy
if (data.status) {
setDepositPolicy(data.data?.deposit_policy);
dispatch(setDepositPolicyStore(data.data?.deposit_policy));
}
} catch (error) {
console.error("Error fetching policy:", error);
} finally {
setPolicyLoad(false);
}
}
};
fetchData();
}, []);
Validation Before Booking
File: Frontend-Userapp-Web/app/[slug]/booking/layout.tsx:186-190
const handleNavigation = (path: string) => {
// Check if cancellation policy must be agreed to
if (frontendSettings?.cancellation_key != 1) {
router.push(path);
}
else if (
frontendSettings?.cancellation_key == 1 &&
cancellationPolicy
) {
router.push(path);
}
else {
toast.error("Please agree to cancellation policy");
return;
}
};
Validation Logic:
cancellation_key = 0: Proceed without checkcancellation_key = 1ANDcancellationPolicy = true: Proceedcancellation_key = 1ANDcancellationPolicy = false: Show error
Backend Implementation
Policy Storage
Table: business_contact_details
CREATE TABLE business_contact_details (
id BIGINT PRIMARY KEY,
deposit_policy LONGBLOB, -- HTML content
refund_policy LONGBLOB, -- HTML content (future)
terms_conditions LONGBLOB,
created_at TIMESTAMP,
updated_at TIMESTAMP
);
File: Backend-Laravel/database/migrations/...add_deposit_policy...
Schema::table('business_contact_details', function (Blueprint $table) {
$table->binary('deposit_policy')->nullable();
$table->binary('refund_policy')->nullable();
});
Policy Retrieval API
Endpoint: GET /business-contact-details?type=4
Controller: BookingContactController.php
public function getAppContent(Request $request)
{
$type = $request->type;
switch ($type) {
case 4:
// Deposit policy
$content = BusinessContactDetails::select('deposit_policy')
->first();
return response()->json([
'status' => true,
'data' => $content
]);
case 5:
// Refund policy
$content = BusinessContactDetails::select('refund_policy')
->first();
return response()->json([
'status' => true,
'data' => $content
]);
default:
return response()->json([
'status' => false,
'message' => 'Invalid type'
]);
}
}
Additional Checkboxes in Review Page
Current Implementation
As of now, there is only ONE checkbox on the review-confirm page:
- Cancellation Policy Checkbox (
cancellation_key)
Potential Additional Checkboxes
Based on the codebase structure, these could be added in future:
- Refund Policy Checkbox (not yet implemented)
{frontendSettings?.refund_policy_key == 1 && (
<div onClick={() => dispatch(setRefundPolicy(!refundPolicy))}>
<RadioSelector isActive={refundPolicy} />
I agree to refund policies.
</div>
)}
- Terms & Conditions Checkbox (not yet implemented)
{frontendSettings?.terms_key == 1 && (
<div onClick={() => dispatch(setTermsAgreed(!termsAgreed))}>
<RadioSelector isActive={termsAgreed} />
I agree to terms and conditions.
</div>
)}
- Marketing Consent Checkbox (not yet implemented)
<div onClick={() => dispatch(setMarketingConsent(!marketingConsent))}>
<RadioSelector isActive={marketingConsent} />
I agree to receive promotional emails and SMS.
</div>
Implementation Status Summary
✅ Fully Implemented Features
| Feature | Frontend | Backend | Database | Stripe Integration |
|---|---|---|---|---|
| Deposit calculation | ✅ | ✅ | ✅ | ✅ |
| Payment intent creation | ✅ | ✅ | N/A | ✅ |
| Deposit payment via Stripe | ✅ | ✅ | ✅ | ✅ |
| Payment status tracking | ✅ | ✅ | ✅ | ✅ |
| AppointmentPayment records | ✅ | ✅ | ✅ | N/A |
| Cancellation policy checkbox | ✅ | ✅ | ✅ | N/A |
| Policy content display | ✅ | ✅ | ✅ | N/A |
| Email notifications | ✅ | ✅ | N/A | N/A |
📊 Deposit System Coverage
✅ 100% - Service-level deposit configuration (payment_type, min_amount)
✅ 100% - Global deposit enable/disable switch
✅ 100% - Automatic deposit calculation
✅ 100% - Stripe payment integration
✅ 100% - Payment confirmation flow
✅ 100% - Appointment status updates
✅ 100% - Payment record keeping
✅ 100% - Email confirmations
📋 Policy System Coverage
✅ 100% - Cancellation policy toggle
✅ 100% - Policy content management
✅ 100% - Policy display on review page
✅ 100% - Checkbox validation
✅ 100% - Policy acceptance tracking
⚠️ 50% - Refund policy (database ready, UI not implemented)
❌ 0% - Terms & conditions checkbox
❌ 0% - Marketing consent tracking
Configuration Examples
Example 1: Require $20 Deposit for Expensive Services
Admin Panel Actions:
- Enable deposit globally:
deposit_enable = 1 - For each expensive service:
- Set
payment_type = 1(Deposit) - Set
min_amount = 20
- Set
Result: Customers must pay $20 upfront, remaining balance at salon
Example 2: Disable All Deposits
Admin Panel Actions:
- Set
deposit_enable = 0
Result: Even if services have payment_type = 1, no deposits are collected
Example 3: Require Policy Agreement
Admin Panel Actions:
- Set
cancellation_key = 1 - Add policy HTML in Business Contact Details
Result: Checkbox appears; customers cannot proceed without checking it
Example 4: Mixed Payment Types
Services Configuration:
- Haircut:
payment_type = 0(Pay at Salon) - Massage:
payment_type = 1,min_amount = 30(Deposit) - Facial:
payment_type = 3(Pay Later - full amount)
Booking Calculation:
Haircut ($50): No deposit
Massage ($80): $30 deposit
Facial ($60): $60 deposit (full amount for pay later)
Total deposit collected: $30 + $60 = $90
Remaining balance: $50 (haircut) + $50 (massage balance) = $100
Payment at salon: $100
Testing Guide
Test Scenario 1: Deposit Flow
-
Setup:
- Set
deposit_enable = 1 - Create service with
payment_type = 1,min_amount = 10
- Set
-
Test:
- Book appointment with the service
- Verify deposit calculation shows $10
- Complete Stripe payment
- Check
appointment_paymentstable has 2 records:- Type 1 (Deposit): $10, status=1
- Type 0 (Balance): $40, status=0
-
Expected Result: Appointment status = 1 (Confirmed)
Test Scenario 2: Cancellation Policy
-
Setup:
- Set
cancellation_key = 1 - Add HTML policy content
- Set
-
Test:
- Navigate to review page
- Verify checkbox is visible
- Try to proceed without checking
- Verify error message appears
- Check the box and proceed
-
Expected Result: Can only proceed when checked
Test Scenario 3: Deposit Disabled
-
Setup:
- Set
deposit_enable = 0 - Service still has
payment_type = 1
- Set
-
Test:
- Book appointment
- Verify no payment options shown
- Appointment created with status = 0
-
Expected Result: No deposit collected
Troubleshooting
Issue: Deposit Not Calculating
Causes:
deposit_enable = 0in frontend_settings- Service
payment_type ≠ 1 - Service
min_amountnot set or = 0
Solution:
-- Check settings
SELECT deposit_enable FROM frontend_settings;
-- Check service configuration
SELECT id, name, payment_type, min_amount
FROM services
WHERE id = <service_id>;
Issue: Policy Checkbox Not Showing
Causes:
cancellation_key = 0- Policy content empty
Solution:
-- Check setting
SELECT cancellation_key FROM frontend_settings;
-- Check policy content
SELECT deposit_policy FROM business_contact_details;
Issue: Payment Failing
Causes:
- Invalid Stripe keys
- Amount = 0
- Currency mismatch
Solution:
// Frontend: Check amount before payment
console.log("Deposit amount:", depositAmount);
// Backend: Check Stripe logs
tail -f storage/logs/laravel.log | grep stripe
Best Practices
For Salon Owners
-
Set Reasonable Deposits:
- 20-50% of service cost
- Higher for long/expensive services
- Lower to reduce booking friction
-
Clear Policy Language:
- State refund conditions
- Specify notice period for cancellations
- Explain no-show charges
-
Test Payment Flow:
- Use Stripe test mode
- Complete full booking
- Verify email notifications
For Developers
-
Always Check Both Levels:
if (
bookingSettings.deposit_enable === 1 &&
service.payment_type === 1
) {
// Collect deposit
} -
Handle Errors Gracefully:
try {
await createPaymentIntent();
} catch (error) {
console.error(error);
toast.error("Payment setup failed. Please try again.");
} -
Cache Policy Content:
- Store in Redux to avoid repeated API calls
- Clear cache when admin updates policy
Related Documentation
- Payment Integration - Complete Stripe setup
- Admin Panel Settings - Configure all booking options
- API Reference - Payment endpoint documentation
- Booking Flow - Full booking process guide