# app/modules/loyalty/tests/integration/test_merchant_api.py """ Integration tests for merchant loyalty API endpoints. Tests the merchant program CRUD endpoints at: /api/v1/merchants/loyalty/* Authentication: Uses dependency overrides (merch_auth_headers pattern). """ import pytest BASE = "/api/v1/merchants/loyalty" # ============================================================================ # GET /program # ============================================================================ @pytest.mark.integration @pytest.mark.api @pytest.mark.loyalty class TestMerchantGetProgram: """Tests for GET /api/v1/merchants/loyalty/program.""" def test_get_program_success( self, client, loyalty_merchant_headers, loyalty_store_setup ): """Returns the merchant's loyalty program.""" response = client.get( f"{BASE}/program", headers=loyalty_merchant_headers ) assert response.status_code == 200 data = response.json() assert data["id"] == loyalty_store_setup["program"].id assert data["merchant_id"] == loyalty_store_setup["merchant"].id assert data["points_per_euro"] == 10 assert data["is_active"] is True def test_get_program_includes_display_fields( self, client, loyalty_merchant_headers, loyalty_store_setup ): """Response includes computed display fields.""" response = client.get( f"{BASE}/program", headers=loyalty_merchant_headers ) assert response.status_code == 200 data = response.json() assert "display_name" in data assert "is_stamps_enabled" in data assert "is_points_enabled" in data def test_get_program_not_found( self, client, loyalty_merchant_headers_no_program ): """Returns 404 when merchant has no program.""" response = client.get( f"{BASE}/program", headers=loyalty_merchant_headers_no_program ) assert response.status_code == 404 # ============================================================================ # POST /program # ============================================================================ @pytest.mark.integration @pytest.mark.api @pytest.mark.loyalty class TestMerchantCreateProgram: """Tests for POST /api/v1/merchants/loyalty/program.""" def test_create_program_success( self, client, loyalty_merchant_headers_no_program, loyalty_merchant_setup ): """Create a new loyalty program for the merchant.""" response = client.post( f"{BASE}/program", json={ "loyalty_type": "points", "points_per_euro": 5, "card_name": "My Rewards", "card_color": "#FF5733", }, headers=loyalty_merchant_headers_no_program, ) assert response.status_code == 201 data = response.json() assert data["loyalty_type"] == "points" assert data["points_per_euro"] == 5 assert data["card_name"] == "My Rewards" assert data["card_color"] == "#FF5733" assert data["merchant_id"] == loyalty_merchant_setup["merchant"].id def test_create_program_with_rewards( self, client, loyalty_merchant_headers_no_program ): """Create program with point rewards configured.""" response = client.post( f"{BASE}/program", json={ "loyalty_type": "points", "points_per_euro": 10, "points_rewards": [ { "id": "r1", "name": "5 EUR off", "points_required": 100, "is_active": True, }, { "id": "r2", "name": "10 EUR off", "points_required": 200, "is_active": True, }, ], }, headers=loyalty_merchant_headers_no_program, ) assert response.status_code == 201 data = response.json() assert len(data["points_rewards"]) == 2 assert data["points_rewards"][0]["name"] == "5 EUR off" def test_create_program_defaults( self, client, loyalty_merchant_headers_no_program ): """Creating with minimal data uses sensible defaults.""" response = client.post( f"{BASE}/program", json={"loyalty_type": "points"}, headers=loyalty_merchant_headers_no_program, ) assert response.status_code == 201 data = response.json() assert data["points_per_euro"] == 1 assert data["is_active"] is True def test_create_program_duplicate_fails( self, client, loyalty_merchant_headers, loyalty_store_setup ): """Cannot create a second program for the same merchant.""" response = client.post( f"{BASE}/program", json={"loyalty_type": "points"}, headers=loyalty_merchant_headers, ) # Should fail - merchant already has a program assert response.status_code in (400, 409, 422) # ============================================================================ # PATCH /program # ============================================================================ @pytest.mark.integration @pytest.mark.api @pytest.mark.loyalty class TestMerchantUpdateProgram: """Tests for PATCH /api/v1/merchants/loyalty/program.""" def test_update_program_success( self, client, loyalty_merchant_headers, loyalty_store_setup ): """Update program fields.""" response = client.patch( f"{BASE}/program", json={ "points_per_euro": 20, "card_name": "Updated Rewards", }, headers=loyalty_merchant_headers, ) assert response.status_code == 200 data = response.json() assert data["points_per_euro"] == 20 assert data["card_name"] == "Updated Rewards" def test_update_program_partial( self, client, loyalty_merchant_headers, loyalty_store_setup ): """Partial update only modifies specified fields.""" response = client.patch( f"{BASE}/program", json={"card_name": "New Name Only"}, headers=loyalty_merchant_headers, ) assert response.status_code == 200 data = response.json() assert data["card_name"] == "New Name Only" # Other fields should be unchanged assert data["points_per_euro"] == 10 def test_update_program_deactivate( self, client, loyalty_merchant_headers, loyalty_store_setup ): """Can deactivate program via update.""" response = client.patch( f"{BASE}/program", json={"is_active": False}, headers=loyalty_merchant_headers, ) assert response.status_code == 200 assert response.json()["is_active"] is False def test_update_program_not_found( self, client, loyalty_merchant_headers_no_program ): """Update returns 404 when no program exists.""" response = client.patch( f"{BASE}/program", json={"card_name": "No Program"}, headers=loyalty_merchant_headers_no_program, ) assert response.status_code == 404 # ============================================================================ # DELETE /program # ============================================================================ @pytest.mark.integration @pytest.mark.api @pytest.mark.loyalty class TestMerchantDeleteProgram: """Tests for DELETE /api/v1/merchants/loyalty/program.""" def test_delete_program_success( self, client, loyalty_merchant_headers, loyalty_store_setup, db ): """Delete the merchant's loyalty program.""" program_id = loyalty_store_setup["program"].id response = client.delete( f"{BASE}/program", headers=loyalty_merchant_headers ) assert response.status_code == 204 # Verify program is deleted from app.modules.loyalty.models import LoyaltyProgram deleted = db.get(LoyaltyProgram, program_id) assert deleted is None def test_delete_program_not_found( self, client, loyalty_merchant_headers_no_program ): """Delete returns 404 when no program exists.""" response = client.delete( f"{BASE}/program", headers=loyalty_merchant_headers_no_program ) assert response.status_code == 404 def test_delete_then_get_returns_404( self, client, loyalty_merchant_headers, loyalty_store_setup ): """After deletion, GET returns 404.""" client.delete(f"{BASE}/program", headers=loyalty_merchant_headers) response = client.get( f"{BASE}/program", headers=loyalty_merchant_headers ) assert response.status_code == 404