feat(loyalty): restructure program CRUD by interface
Some checks failed
CI / ruff (push) Successful in 10s
CI / pytest (push) Failing after 45m49s
CI / validate (push) Successful in 23s
CI / dependency-scanning (push) Successful in 29s
CI / docs (push) Has been skipped
CI / deploy (push) Has been skipped

Move program CRUD from store to merchant/admin interfaces.
Store becomes view-only for program config while merchant gets
full CRUD and admin gets override capabilities.

Merchant portal:
- New API endpoints (GET/POST/PATCH/DELETE /program)
- New settings page with create/edit/delete form
- Overview page now has Create/Edit Program buttons
- Settings menu item added to sidebar

Admin portal:
- New CRUD endpoints (create for merchant, update, delete)
- New activate/deactivate program endpoints
- Programs list has edit and toggle buttons per row
- Merchant detail has create/delete/toggle program actions

Store portal:
- Removed POST/PATCH /program endpoints (now read-only)
- Removed settings page route and template
- Terminal, cards, stats, enroll unchanged

Tests: 112 passed (58 new) covering merchant API, admin CRUD,
store endpoint removal, and program service unit tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-25 13:32:20 +01:00
parent d648c921b7
commit 6b46a78e72
22 changed files with 1616 additions and 113 deletions

View File

@@ -9,11 +9,14 @@ from datetime import UTC, datetime
import pytest
from app.api.deps import get_current_merchant_api, get_merchant_for_current_user
from app.modules.loyalty.models import LoyaltyCard, LoyaltyProgram
from app.modules.loyalty.models.loyalty_program import LoyaltyType
from app.modules.tenancy.models import Merchant, Platform, Store, User
from app.modules.tenancy.models.store import StoreUser
from app.modules.tenancy.models.store_platform import StorePlatform
from main import app
from models.schema.auth import UserContext
@pytest.fixture
@@ -166,6 +169,95 @@ def loyalty_store_setup(db, loyalty_platform):
}
@pytest.fixture
def loyalty_merchant_setup(db, loyalty_platform):
"""
Merchant-only setup for loyalty integration tests (no program).
Creates: User -> Merchant (no program yet).
Use this for testing program creation via merchant API.
"""
from middleware.auth import AuthManager
auth = AuthManager()
uid = uuid.uuid4().hex[:8]
owner = User(
email=f"merchowner_{uid}@test.com",
username=f"merchowner_{uid}",
hashed_password=auth.hash_password("merchpass123"),
role="merchant_owner",
is_active=True,
is_email_verified=True,
)
db.add(owner)
db.commit()
db.refresh(owner)
merchant = Merchant(
name=f"Loyalty Merchant {uid}",
owner_user_id=owner.id,
contact_email=owner.email,
is_active=True,
is_verified=True,
)
db.add(merchant)
db.commit()
db.refresh(merchant)
return {
"owner": owner,
"merchant": merchant,
}
@pytest.fixture
def loyalty_merchant_headers(loyalty_store_setup):
"""
Override auth dependencies to return merchant/user for the merchant owner.
Uses the full loyalty_store_setup which includes a program.
"""
owner = loyalty_store_setup["owner"]
merchant = loyalty_store_setup["merchant"]
user_context = UserContext(
id=owner.id,
email=owner.email,
username=owner.username,
role="merchant_owner",
is_active=True,
)
app.dependency_overrides[get_merchant_for_current_user] = lambda: merchant
app.dependency_overrides[get_current_merchant_api] = lambda: user_context
yield {"Authorization": "Bearer fake-token"}
app.dependency_overrides.pop(get_merchant_for_current_user, None)
app.dependency_overrides.pop(get_current_merchant_api, None)
@pytest.fixture
def loyalty_merchant_headers_no_program(loyalty_merchant_setup):
"""
Override auth dependencies for a merchant that has no program yet.
"""
owner = loyalty_merchant_setup["owner"]
merchant = loyalty_merchant_setup["merchant"]
user_context = UserContext(
id=owner.id,
email=owner.email,
username=owner.username,
role="merchant_owner",
is_active=True,
)
app.dependency_overrides[get_merchant_for_current_user] = lambda: merchant
app.dependency_overrides[get_current_merchant_api] = lambda: user_context
yield {"Authorization": "Bearer fake-token"}
app.dependency_overrides.pop(get_merchant_for_current_user, None)
app.dependency_overrides.pop(get_current_merchant_api, None)
@pytest.fixture
def loyalty_store_headers(client, loyalty_store_setup):
"""