Skip to content

feat(schedule): refactor timeslot management and fix quality blockers#88

Merged
CFBruna merged 130 commits intov2/monorepo-migrationfrom
feature/schedule-refactor
Mar 2, 2026
Merged

feat(schedule): refactor timeslot management and fix quality blockers#88
CFBruna merged 130 commits intov2/monorepo-migrationfrom
feature/schedule-refactor

Conversation

@CFBruna
Copy link
Owner

@CFBruna CFBruna commented Mar 2, 2026

What Changed

  • Refactored timeslot management UI: new TimeSlotModal component and dedicated TimeSlotsManagementPage moved to pages/
  • Added timeslot API tests and fixed timezone.localtime() usage in slot validation
  • Replaced get_veterinarian_category_id with EmployeeService.is_veterinarian(role) in accounts serializers and views
  • Excluded role field from EmployeeAdmin form (derived from category)
  • Fixed LocaleEnforcementMiddleware position in MIDDLEWARE — must come after SessionMiddleware
  • Fixed all mypy errors: AccountsError.field kwarg, OpenApiParameter type, unit_price fallback, timedelta * F, AppointmentFactory.pk, deprecated make_random_password
  • SaleAdmin.change_view now raises PermissionDenied — fixes two failing admin tests expecting 403
  • ESLint: suppressed set-state-in-effect in employee form modals with explicit eslint-disable-line
  • Added testpaths = ["src"] to pytest config to exclude debug scripts from collection

Why

The timeslot management page needed a dedicated modal and page separation to align with the
project's component architecture. Several mypy, ruff, and pytest violations blocked the CI/CD
pipeline from passing and had to be resolved before opening a PR.

How to Test

  • Log in to the Django admin and confirm it loads in Spanish (clear pt-br browser cookie first)
  • Navigate to /admin/timeslots/manage and verify the TimeSlotModal opens and saves correctly
  • Create an employee via admin and confirm role field is not shown in the form
  • Run uv run pytest — expect 397/397 passed
  • Run npm run build — expect success with no errors
  • Run uv run mypy src/ — expect 0 errors

Checklist

  • Service layer architecture followed
  • No ORM in views
  • No hardcoded strings (frontend and backend)
  • Translation keys added to en, pt, es
  • Tests added or updated
  • uv run pytest passes
  • npm run build passes
  • Turing Test passed (works with PYG .env)

- One file per model: service.py, appointment.py, timeslot.py
- SizeCategory TextChoices added to service.py
- Service: added is_active, requires_veterinarian, has_size_pricing,
  price/duration per size category, created_at, updated_at
- Appointment: removed unique=True from schedule_time (critical bug fix),
  added NO_SHOW status, price_charged, pet_weight_at_booking,
  size_category_at_booking, canceled_at, created_at, updated_at, DB indexes
- TimeSlot: added is_active field
- __str__ uses django.utils.formats (no hardcoded strftime locale)
…yped exceptions

- exceptions.py: typed exception hierarchy (ScheduleException base)
- appointment_service.py: create/update/cancel/complete + _derive_size_category
  auto-snapshots pet weight, size_category and price_charged at booking
- slot_service.py: get_available_slots respects is_active on TimeSlot,
  uses pet weight for size-correct duration, zero circular imports
- service_service.py: get_all_active, get_by_id, create_service, update_service
- __init__.py: clean public API with __all__
- service_admin.py: @admin.register(Service) with fieldsets for base/size pricing
  and audit fields; price_display via currency_tags (no hardcoded symbols)
- appointment_admin.py: @admin.register(Appointment) with new snapshot fields
  in list_display (price_charged, size_category_at_booking, pet_weight_at_booking)
  and readonly_fields
- timeslot_admin.py: @admin.register(TimeSlot) with is_active column
- __init__.py: clean imports with __all__
views.py:
- AvailableSlotsView uses ServiceService.get_by_id (zero ORM)
- AppointmentViewSet.perform_destroy delegates to AppointmentService.cancel_appointment
  (preserves history, no hard delete)
- All except-ValueError replaced by typed ScheduleException subtypes
- Added /cancel/ action endpoint
- Service queryset filtered by is_active=True

serializers.py:
- AppointmentSerializer exposes all snapshot fields as read_only
- SlotService.get_available_slots receives pet for size-correct duration
- ServiceSerializer includes all new fields (is_active, has_size_pricing, size fields)

factories.py:
- ServiceFactory: is_active=True, has_size_pricing=False
- AppointmentFactory: price_charged snapshot field
- TimeSlotFactory: is_active=True
- SERVICE_NAMES extracted as module-level constant (no inline lambda nesting)
…chitecture

test_models.py:
- TestServiceModel: defaults, get_price_for_size, get_duration_for_size,
  fallback-to-base when size price is null, created_at/updated_at
- TestTimeSlotModel: is_active default, __str__ uses localised format
- TestAppointmentModel: snapshot fields nullable, canceled_at nullable,
  created_at auto-set, __str__ includes pet and service names

test_service.py:
- TestDeriveSizeCategory: boundary tests 0-5/5-20/20-45/45+ kg, None weight
- TestCreateAppointment: snapshots weight+size+price, PastSchedulingError,
  SamedayAppointmentError, two pets same time allowed
- TestCancelAppointment: sets canceled_at, AlreadyCanceledError,
  CancellationWindowError for non-staff < 1h, staff bypasses window
- TestCompleteAppointment: sets completed_at, FutureCompletionError
- TestSlotService: past date returns [], inactive slot excluded,
  active slot included, occupied slot excluded
…intmentAdmin

autocomplete_fields requires Pet to have a registered admin with search_fields.
raw_id_fields achieves the same FK lookup UX without that hard dependency.
1. forms.py: replace AppointmentService.get_available_slots -> SlotService.get_available_slots
   (2 call sites: get_dynamic_time_choices + clean); add ScheduleException to except clause;
   use .astimezone(active_tz).time() for correct local-time comparison

2. appointment_service.py: schedule_time.date() -> timezone.localtime(schedule_time).date()
   on all 3 sameday filter sites (create/update/prepare).
   Root cause: .date() on a UTC-aware datetime returns UTC date; Django __date lookup
   compares using settings.TIME_ZONE. At 21h UTC in Paraguay (-3h), now+2h=23:36 UTC
   and now+3h=00:36 UTC are different UTC dates even though they are the same local day.

3. test_api.py: update direct call from AppointmentService.get_available_slots
   -> SlotService.get_available_slots + fix import

4. test_models.py: test_unique_together_day_start_time must use TimeSlot.objects.create()
   directly; TimeSlotFactory has django_get_or_create which silently returns the
   existing object instead of raising IntegrityError.
   test_service.py: remove unnecessary pytz import (stdlib zoneinfo suffices)
…_BR/es locale files

exceptions.py:
- FutureCompletionError: 'completed' -> "'Completed'" to match pre-existing test assertion

locale/pt_BR/LC_MESSAGES/django.po:
- All new strings: models (Service, Appointment, TimeSlot, SizeCategory),
  admin fieldset titles (Base Pricing, Size-Based Pricing, Audit),
  service exception messages, form validation messages

locale/es/LC_MESSAGES/django.po:
- Same strings in Spanish

After pulling, run locally:
  uv run python manage.py compilemessages
- Add `list_available` to EmployeeService with conflict filtering

- Expand EmployeeViewSet with create/update/destroy methods

- Create `EmployeeAvailableView` to expose efficient availability querying

- Build EmployeesPage, FormModal and Table components in React

- Bind `useEmployeeAvailability` to AppointmentForm dropdown conditional fetching

- Inject i18n locales for Employee translation keys (EN, BR, ES)
- Fix remaining E402 and E701 linter warnings

- Ensure correct import topology inside tests to satisfy pre-commit hooks
- Add 'Equipe' to the scheduling sidebar group

- Unify translation keys under 'employees_management' object

- Fix duplicate 'employees' keys in JSON locales for valid structure

- Internationalize Table status badges and add missing noEmployeeAvailable string
- AccountsError.__init__ now accepts field kwarg
- Replace dict with OpenApiParameter in by_role action
- Fix unit_price None in appointment_service.link_sale_to_appointment
- Replace deprecated make_random_password with secrets.token_urlsafe
- Fix timedelta*F mypy operator error with type: ignore
- Fix AppointmentFactory.pk mypy attr-defined with typed variables
- Add LOCALE_PATHS and filterset_fields type annotations
- Fix service.py dict.get call-overload mypy errors
- SaleAdmin.change_view now raises PermissionDenied (fixes TestSaleAdmin 403)
- Add testpaths=[src] to pytest config to exclude debug scripts
@CFBruna CFBruna merged commit cf8189e into v2/monorepo-migration Mar 2, 2026
1 check passed
@CFBruna CFBruna deleted the feature/schedule-refactor branch March 2, 2026 20:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant