- {t('services.depositNote', 'Customers must save a payment method to book this service. A deposit will be charged at booking, and the remaining balance will be charged after service completion.')}
-
-
+
+ {t('services.variablePricingNote', 'The price above is shown as an estimate. The final price will be determined and charged after service completion.')}
+
)}
diff --git a/frontend/src/types.ts b/frontend/src/types.ts
index 9ab24e8..0216252 100644
--- a/frontend/src/types.ts
+++ b/frontend/src/types.ts
@@ -209,11 +209,11 @@ export interface Service {
photos?: string[];
created_at?: string; // Used for quota overage calculation (oldest archived first)
is_archived_by_quota?: boolean; // True if archived due to quota overage
- // Variable pricing fields
+ // Pricing fields
variable_pricing?: boolean; // If true, final price is determined after service completion
- deposit_amount?: number | null; // Fixed deposit amount
- deposit_percent?: number | null; // Deposit as percentage (0-100)
- requires_saved_payment_method?: boolean; // If true, customer must have saved card
+ deposit_amount?: number; // Deposit amount (0 = no deposit required)
+ requires_deposit?: boolean; // True if deposit_amount > 0 (computed)
+ requires_saved_payment_method?: boolean; // True if deposit > 0 or variable pricing (computed)
deposit_display?: string | null; // Human-readable deposit description
}
diff --git a/smoothschedule/schedule/migrations/0026_simplify_deposit_fields.py b/smoothschedule/schedule/migrations/0026_simplify_deposit_fields.py
new file mode 100644
index 0000000..75d2289
--- /dev/null
+++ b/smoothschedule/schedule/migrations/0026_simplify_deposit_fields.py
@@ -0,0 +1,28 @@
+# Generated by Django 5.2.8 on 2025-12-04 18:40
+
+import django.core.validators
+from decimal import Decimal
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('schedule', '0025_add_variable_pricing'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='service',
+ name='deposit_percent',
+ ),
+ migrations.RemoveField(
+ model_name='service',
+ name='requires_saved_payment_method',
+ ),
+ migrations.AlterField(
+ model_name='service',
+ name='deposit_amount',
+ field=models.DecimalField(decimal_places=2, default=Decimal('0.00'), help_text='Deposit amount to collect at booking (0 = no deposit)', max_digits=10, validators=[django.core.validators.MinValueValidator(Decimal('0'))]),
+ ),
+ ]
diff --git a/smoothschedule/schedule/models.py b/smoothschedule/schedule/models.py
index f551cd9..d4815bb 100644
--- a/smoothschedule/schedule/models.py
+++ b/smoothschedule/schedule/models.py
@@ -39,24 +39,14 @@ class Service(models.Model):
default=False,
help_text="If true, final price is determined after service completion"
)
+ # Deposit - applies to both variable and fixed pricing
+ # 0 = no deposit required, > 0 = deposit amount to collect at booking
deposit_amount = models.DecimalField(
max_digits=10,
decimal_places=2,
- null=True,
- blank=True,
- help_text="Fixed deposit amount to collect at booking (used if deposit_percent is not set)"
- )
- deposit_percent = models.DecimalField(
- max_digits=5,
- decimal_places=2,
- null=True,
- blank=True,
- validators=[MinValueValidator(Decimal('0')), ],
- help_text="Deposit as percentage of estimated price (0-100)"
- )
- requires_saved_payment_method = models.BooleanField(
- default=False,
- help_text="If true, customer must have a saved payment method to book (auto-set for variable pricing)"
+ default=Decimal('0.00'),
+ validators=[MinValueValidator(Decimal('0'))],
+ help_text="Deposit amount to collect at booking (0 = no deposit)"
)
# Quota overage archiving
@@ -80,29 +70,15 @@ class Service(models.Model):
def __str__(self):
return f"{self.name} ({self.duration} min - ${self.price})"
- def get_deposit_amount(self, estimated_price=None):
- """
- Calculate the deposit amount for this service.
+ @property
+ def requires_deposit(self):
+ """Check if this service requires a deposit"""
+ return self.deposit_amount and self.deposit_amount > 0
- Args:
- estimated_price: Optional estimated price for percentage calculation.
- Falls back to self.price if not provided.
-
- Returns:
- Decimal: The deposit amount, or None if no deposit required.
- """
- if self.deposit_amount:
- return self.deposit_amount
- if self.deposit_percent:
- base_price = estimated_price or self.price
- return (base_price * self.deposit_percent / 100).quantize(Decimal('0.01'))
- return None
-
- def clean(self):
- """Validate deposit configuration"""
- super().clean()
- if self.deposit_percent and self.deposit_percent > 100:
- raise ValidationError({'deposit_percent': 'Deposit percentage cannot exceed 100%'})
+ @property
+ def requires_saved_payment_method(self):
+ """Customer must have a saved payment method if deposit > 0 or variable pricing"""
+ return self.requires_deposit or self.variable_pricing
class ResourceType(models.Model):
diff --git a/smoothschedule/schedule/serializers.py b/smoothschedule/schedule/serializers.py
index 4b50934..6defbc9 100644
--- a/smoothschedule/schedule/serializers.py
+++ b/smoothschedule/schedule/serializers.py
@@ -146,6 +146,8 @@ class ServiceSerializer(serializers.ModelSerializer):
"""Serializer for Service model"""
duration_minutes = serializers.IntegerField(source='duration', read_only=True)
deposit_display = serializers.SerializerMethodField()
+ requires_deposit = serializers.BooleanField(read_only=True)
+ requires_saved_payment_method = serializers.BooleanField(read_only=True)
class Meta:
model = Service
@@ -153,44 +155,19 @@ class ServiceSerializer(serializers.ModelSerializer):
'id', 'name', 'description', 'duration', 'duration_minutes',
'price', 'display_order', 'photos', 'is_active', 'created_at', 'updated_at',
'is_archived_by_quota',
- # Variable pricing fields
- 'variable_pricing', 'deposit_amount', 'deposit_percent',
- 'requires_saved_payment_method', 'deposit_display',
+ # Pricing fields
+ 'variable_pricing', 'deposit_amount',
+ 'requires_deposit', 'requires_saved_payment_method', 'deposit_display',
]
- read_only_fields = ['created_at', 'updated_at', 'is_archived_by_quota', 'deposit_display']
+ read_only_fields = ['created_at', 'updated_at', 'is_archived_by_quota',
+ 'deposit_display', 'requires_deposit', 'requires_saved_payment_method']
def get_deposit_display(self, obj):
"""Get a human-readable description of the deposit requirement"""
- if obj.deposit_amount:
+ if obj.deposit_amount and obj.deposit_amount > 0:
return f"${obj.deposit_amount} deposit"
- elif obj.deposit_percent:
- return f"{obj.deposit_percent}% deposit"
return None
- def validate(self, attrs):
- """Validate deposit configuration"""
- variable_pricing = attrs.get('variable_pricing', getattr(self.instance, 'variable_pricing', False))
- deposit_amount = attrs.get('deposit_amount', getattr(self.instance, 'deposit_amount', None))
- deposit_percent = attrs.get('deposit_percent', getattr(self.instance, 'deposit_percent', None))
-
- # Variable pricing services should have a deposit configured
- if variable_pricing and not deposit_amount and not deposit_percent:
- raise serializers.ValidationError({
- 'deposit_amount': 'Variable pricing services require a deposit amount or percentage'
- })
-
- # Can't have both deposit types
- if deposit_amount and deposit_percent:
- raise serializers.ValidationError({
- 'deposit_amount': 'Cannot specify both deposit_amount and deposit_percent. Choose one.'
- })
-
- # Auto-set requires_saved_payment_method for variable pricing
- if variable_pricing:
- attrs['requires_saved_payment_method'] = True
-
- return attrs
-
class ResourceSerializer(serializers.ModelSerializer):
"""Serializer for Resource model"""