Files
orion/tests/unit/services/test_loyalty_services.py
Samir Boulahtit df784d718b test(loyalty): add comprehensive test suite for loyalty module
Add tests for the loyalty module Phase 2 implementation:

Fixtures (tests/fixtures/loyalty_fixtures.py):
- test_loyalty_program: Points-based program with rewards
- test_loyalty_program_no_expiration: Program without point expiry
- test_loyalty_card: Active customer card
- test_loyalty_card_inactive: Card for expiration testing
- test_loyalty_transaction: Sample transaction
- test_staff_pin: Staff PIN for verification tests

Unit Tests (tests/unit/services/test_loyalty_services.py):
- ProgramService: Company queries, listing, filtering
- CardService: Lookup, enrollment, balance operations
- PointsService: Earn, redeem, void, adjust operations
- PinService: Creation, verification, lockout

Integration Tests (tests/integration/api/v1/loyalty/):
- Vendor API: Program settings, card management, PIN operations
- Storefront API: Endpoint existence verification

Task Tests (tests/integration/tasks/test_loyalty_tasks.py):
- Point expiration for inactive cards
- Transaction record creation on expiration
- No expiration for active cards or zero balances
- Voided total tracking

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 22:16:05 +01:00

321 lines
11 KiB
Python

# tests/unit/services/test_loyalty_services.py
"""
Unit tests for Loyalty module services.
Tests cover:
- Program service: CRUD operations, company-based queries
- Card service: Enrollment, lookup, balance operations
- Points service: Earn, redeem, void operations
- PIN service: Verification, lockout
"""
from datetime import UTC, datetime, timedelta
from unittest.mock import MagicMock, patch
import pytest
from app.modules.loyalty.exceptions import (
LoyaltyCardNotFoundException,
LoyaltyException,
LoyaltyProgramNotFoundException,
)
from app.modules.loyalty.models import LoyaltyCard, LoyaltyProgram, LoyaltyTransaction
from app.modules.loyalty.models.loyalty_program import LoyaltyType
from app.modules.loyalty.models.loyalty_transaction import TransactionType
from app.modules.loyalty.services import (
card_service,
pin_service,
points_service,
program_service,
)
# =============================================================================
# Program Service Tests
# =============================================================================
@pytest.mark.unit
@pytest.mark.service
class TestProgramService:
"""Tests for program_service."""
def test_get_program_by_company(self, db, test_loyalty_program):
"""Test getting a program by company ID."""
program = program_service.get_program_by_company(
db, test_loyalty_program.company_id
)
assert program is not None
assert program.id == test_loyalty_program.id
assert program.company_id == test_loyalty_program.company_id
def test_get_program_by_company_not_found(self, db):
"""Test getting a program for non-existent company."""
program = program_service.get_program_by_company(db, 99999)
assert program is None
def test_get_program_by_vendor(self, db, test_loyalty_program, test_vendor):
"""Test getting a program by vendor ID."""
program = program_service.get_program_by_vendor(db, test_vendor.id)
assert program is not None
assert program.company_id == test_vendor.company_id
def test_list_programs(self, db, test_loyalty_program):
"""Test listing all programs with pagination."""
programs, total = program_service.list_programs(db, skip=0, limit=10)
assert total >= 1
assert any(p.id == test_loyalty_program.id for p in programs)
def test_list_programs_active_only(self, db, test_loyalty_program):
"""Test listing only active programs."""
programs, total = program_service.list_programs(
db, skip=0, limit=10, active_only=True
)
assert all(p.is_active for p in programs)
# =============================================================================
# Card Service Tests
# =============================================================================
@pytest.mark.unit
@pytest.mark.service
class TestCardService:
"""Tests for card_service."""
def test_get_card_by_id(self, db, test_loyalty_card):
"""Test getting a card by ID."""
card = card_service.get_card(db, test_loyalty_card.id)
assert card is not None
assert card.id == test_loyalty_card.id
def test_get_card_not_found(self, db):
"""Test getting a non-existent card."""
with pytest.raises(LoyaltyCardNotFoundException):
card_service.get_card(db, 99999)
def test_get_card_by_number(self, db, test_loyalty_card):
"""Test getting a card by card number."""
card = card_service.get_card_by_number(
db, test_loyalty_card.company_id, test_loyalty_card.card_number
)
assert card is not None
assert card.card_number == test_loyalty_card.card_number
def test_get_card_by_customer_email(self, db, test_loyalty_card):
"""Test getting a card by customer email."""
card = card_service.get_card_by_customer_email(
db, test_loyalty_card.company_id, test_loyalty_card.customer_email
)
assert card is not None
assert card.customer_email == test_loyalty_card.customer_email
def test_lookup_card(self, db, test_loyalty_card):
"""Test looking up a card by various identifiers."""
# By card number
card = card_service.lookup_card(
db, test_loyalty_card.company_id, test_loyalty_card.card_number
)
assert card is not None
assert card.id == test_loyalty_card.id
# By email
card = card_service.lookup_card(
db, test_loyalty_card.company_id, test_loyalty_card.customer_email
)
assert card is not None
assert card.id == test_loyalty_card.id
def test_enroll_customer(self, db, test_loyalty_program, test_vendor):
"""Test enrolling a new customer."""
card = card_service.enroll_customer(
db,
vendor_id=test_vendor.id,
customer_email="newmember@test.com",
customer_name="New Member",
customer_phone="+352123456789",
)
db.commit()
assert card is not None
assert card.customer_email == "newmember@test.com"
assert card.company_id == test_vendor.company_id
# Check welcome bonus was applied
assert card.points_balance == test_loyalty_program.welcome_bonus_points
def test_enroll_customer_duplicate(self, db, test_loyalty_card, test_vendor):
"""Test enrolling an existing customer raises error."""
with pytest.raises(LoyaltyException) as exc_info:
card_service.enroll_customer(
db,
vendor_id=test_vendor.id,
customer_email=test_loyalty_card.customer_email,
customer_name="Duplicate",
)
assert "already enrolled" in str(exc_info.value).lower()
# =============================================================================
# Points Service Tests
# =============================================================================
@pytest.mark.unit
@pytest.mark.service
class TestPointsService:
"""Tests for points_service."""
def test_earn_points(self, db, test_loyalty_card, test_vendor, test_staff_pin):
"""Test earning points."""
initial_balance = test_loyalty_card.points_balance
purchase_amount_cents = 5000 # €50
result = points_service.earn_points(
db,
card_id=test_loyalty_card.id,
vendor_id=test_vendor.id,
purchase_amount_cents=purchase_amount_cents,
staff_pin_id=test_staff_pin.id,
)
db.commit()
assert result is not None
assert result["points_earned"] > 0
assert result["new_balance"] > initial_balance
def test_redeem_points(self, db, test_loyalty_card, test_vendor, test_staff_pin):
"""Test redeeming points."""
# Ensure card has enough points
test_loyalty_card.points_balance = 200
db.commit()
initial_balance = test_loyalty_card.points_balance
result = points_service.redeem_points(
db,
card_id=test_loyalty_card.id,
vendor_id=test_vendor.id,
reward_id="reward_1", # 100 points
staff_pin_id=test_staff_pin.id,
)
db.commit()
assert result is not None
assert result["points_redeemed"] == 100
assert result["new_balance"] == initial_balance - 100
def test_redeem_points_insufficient_balance(
self, db, test_loyalty_card, test_vendor, test_staff_pin
):
"""Test redeeming points with insufficient balance."""
test_loyalty_card.points_balance = 50 # Less than minimum
db.commit()
with pytest.raises(LoyaltyException) as exc_info:
points_service.redeem_points(
db,
card_id=test_loyalty_card.id,
vendor_id=test_vendor.id,
reward_id="reward_1", # 100 points needed
staff_pin_id=test_staff_pin.id,
)
assert "insufficient" in str(exc_info.value).lower()
def test_void_points(self, db, test_loyalty_card, test_vendor, test_staff_pin):
"""Test voiding points (for returns)."""
initial_balance = test_loyalty_card.points_balance
points_to_void = 50
result = points_service.void_points(
db,
card_id=test_loyalty_card.id,
vendor_id=test_vendor.id,
points_to_void=points_to_void,
reason="Customer return",
staff_pin_id=test_staff_pin.id,
)
db.commit()
assert result is not None
assert result["points_voided"] == points_to_void
assert result["new_balance"] == initial_balance - points_to_void
def test_adjust_points(self, db, test_loyalty_card, test_vendor):
"""Test manual points adjustment."""
initial_balance = test_loyalty_card.points_balance
adjustment = 25
result = points_service.adjust_points(
db,
card_id=test_loyalty_card.id,
vendor_id=test_vendor.id,
points_delta=adjustment,
reason="Manual correction",
)
db.commit()
assert result is not None
assert result["new_balance"] == initial_balance + adjustment
# =============================================================================
# PIN Service Tests
# =============================================================================
@pytest.mark.unit
@pytest.mark.service
class TestPinService:
"""Tests for pin_service."""
def test_verify_pin_success(self, db, test_staff_pin):
"""Test successful PIN verification."""
result = pin_service.verify_pin(
db,
pin_id=test_staff_pin.id,
pin="1234",
)
assert result is True
def test_verify_pin_wrong_pin(self, db, test_staff_pin):
"""Test PIN verification with wrong PIN."""
result = pin_service.verify_pin(
db,
pin_id=test_staff_pin.id,
pin="9999",
)
assert result is False
def test_verify_pin_increments_failed_attempts(self, db, test_staff_pin):
"""Test that failed verification increments attempt counter."""
initial_attempts = test_staff_pin.failed_attempts or 0
pin_service.verify_pin(db, pin_id=test_staff_pin.id, pin="wrong")
db.refresh(test_staff_pin)
assert test_staff_pin.failed_attempts == initial_attempts + 1
def test_create_pin(self, db, test_loyalty_program, test_vendor):
"""Test creating a new staff PIN."""
pin = pin_service.create_pin(
db,
program_id=test_loyalty_program.id,
vendor_id=test_vendor.id,
staff_name="New Staff Member",
pin="5678",
)
db.commit()
assert pin is not None
assert pin.staff_name == "New Staff Member"
assert pin.is_active is True
# Verify PIN works
assert pin_service.verify_pin(db, pin.id, "5678") is True
def test_list_pins_for_vendor(self, db, test_staff_pin, test_vendor):
"""Test listing PINs for a vendor."""
pins = pin_service.list_pins_for_vendor(db, test_vendor.id)
assert len(pins) >= 1
assert any(p.id == test_staff_pin.id for p in pins)