"""Unit tests for loyalty schemas.""" from datetime import UTC, datetime import pytest from pydantic import ValidationError from app.modules.loyalty.schemas.card import ( CardEnrollRequest, TransactionResponse, ) from app.modules.loyalty.schemas.points import PointsAdjustRequest from app.modules.loyalty.schemas.program import StorefrontProgramResponse @pytest.mark.unit @pytest.mark.loyalty class TestCardEnrollRequest: """Tests for enrollment request schema validation.""" def test_valid_with_email(self): """Email-only enrollment is valid.""" req = CardEnrollRequest(email="test@example.com") assert req.email == "test@example.com" assert req.customer_id is None def test_valid_with_customer_id(self): """Customer ID enrollment is valid.""" req = CardEnrollRequest(customer_id=42) assert req.customer_id == 42 assert req.email is None def test_self_enrollment_fields(self): """Self-enrollment accepts name, phone, birthday.""" from datetime import date req = CardEnrollRequest( email="self@example.com", customer_name="Jane Doe", customer_phone="+352123456", customer_birthday="1990-01-15", ) assert req.customer_name == "Jane Doe" assert req.customer_phone == "+352123456" assert req.customer_birthday == date(1990, 1, 15) def test_birthday_in_future_rejected(self): """Future birthdays are rejected.""" from datetime import date, timedelta import pytest from pydantic import ValidationError with pytest.raises(ValidationError, match="must be in the past"): CardEnrollRequest( email="self@example.com", customer_birthday=(date.today() + timedelta(days=1)).isoformat(), ) def test_birthday_implausible_age_rejected(self): """Birthdays implying impossible ages are rejected.""" import pytest from pydantic import ValidationError with pytest.raises(ValidationError, match="implausible age"): CardEnrollRequest( email="self@example.com", customer_birthday="1800-01-01", ) def test_birthday_omitted_is_valid(self): """Birthday is optional.""" req = CardEnrollRequest(email="self@example.com") assert req.customer_birthday is None @pytest.mark.unit @pytest.mark.loyalty class TestTransactionResponse: """Tests for transaction response schema.""" def test_customer_name_field_defaults_none(self): """customer_name field exists and defaults to None.""" now = datetime.now(UTC) tx = TransactionResponse( id=1, card_id=1, transaction_type="points_earned", stamps_delta=0, points_delta=50, transaction_at=now, created_at=now, ) assert tx.customer_name is None def test_customer_name_can_be_set(self): """customer_name can be explicitly set.""" now = datetime.now(UTC) tx = TransactionResponse( id=1, card_id=1, transaction_type="card_created", stamps_delta=0, points_delta=0, customer_name="John Doe", transaction_at=now, created_at=now, ) assert tx.customer_name == "John Doe" # ============================================================================ # Item 8: PointsAdjustRequest bounds # ============================================================================ @pytest.mark.unit @pytest.mark.loyalty class TestPointsAdjustRequestBounds: """Tests for PointsAdjustRequest.points_delta bounds.""" def test_valid_positive_delta(self): """Positive delta within bounds is valid.""" req = PointsAdjustRequest(points_delta=100, reason="Bonus for customer") assert req.points_delta == 100 def test_valid_negative_delta(self): """Negative delta within bounds is valid.""" req = PointsAdjustRequest(points_delta=-500, reason="Correction needed") assert req.points_delta == -500 def test_max_boundary(self): """Delta at max boundary (100000) is valid.""" req = PointsAdjustRequest(points_delta=100000, reason="Max allowed delta") assert req.points_delta == 100000 def test_min_boundary(self): """Delta at min boundary (-100000) is valid.""" req = PointsAdjustRequest(points_delta=-100000, reason="Min allowed delta") assert req.points_delta == -100000 def test_exceeds_max_rejected(self): """Delta exceeding 100000 is rejected.""" with pytest.raises(ValidationError): PointsAdjustRequest(points_delta=100001, reason="Too many points") def test_exceeds_min_rejected(self): """Delta below -100000 is rejected.""" with pytest.raises(ValidationError): PointsAdjustRequest(points_delta=-100001, reason="Too many negative") # ============================================================================ # Item 7: StorefrontProgramResponse excludes wallet fields # ============================================================================ @pytest.mark.unit @pytest.mark.loyalty class TestStorefrontProgramResponse: """Tests for StorefrontProgramResponse excluding wallet IDs.""" def test_wallet_fields_excluded_from_serialization(self): """Wallet integration IDs are excluded from JSON output.""" now = datetime.now(UTC) response = StorefrontProgramResponse( id=1, merchant_id=1, loyalty_type="points", stamps_target=10, stamps_reward_description="Free item", points_per_euro=10, cooldown_minutes=15, max_daily_stamps=5, require_staff_pin=True, card_color="#4F46E5", is_active=True, created_at=now, updated_at=now, ) data = response.model_dump() assert "google_issuer_id" not in data assert "google_class_id" not in data assert "apple_pass_type_id" not in data def test_non_wallet_fields_present(self): """Non-wallet fields are still present.""" now = datetime.now(UTC) response = StorefrontProgramResponse( id=1, merchant_id=1, loyalty_type="points", stamps_target=10, stamps_reward_description="Free item", points_per_euro=10, cooldown_minutes=15, max_daily_stamps=5, require_staff_pin=True, card_color="#4F46E5", is_active=True, created_at=now, updated_at=now, ) data = response.model_dump() assert data["id"] == 1 assert data["loyalty_type"] == "points" assert data["points_per_euro"] == 10 assert data["card_color"] == "#4F46E5"