feat: Add organization member support with memberType field

Implements support for both individual persons and organizations as members.

Changes:
- Added MemberType enum (INDIVIDUAL, ORGANIZATION) to distinguish member types
- Added member_type column to database (defaults to INDIVIDUAL for backward compatibility)
- Added company_name field for organizations
- Made first_name nullable (required for individuals, optional for organizations)
- Implemented conditional validation:
  - INDIVIDUAL members require first_name
  - ORGANIZATION members require company_name
- Updated GraphQL schema with new memberType and companyName fields
- Updated all resolvers to handle new fields and validation
- Added comprehensive unit tests for validation logic
- Updated existing tests to work with new fields
- All 53 tests passing

Technical notes:
- Using memberType instead of 'kind' to avoid GraphQL introspection conflicts
- Using native_enum=False for SQLite compatibility
- Using batch_alter_table for SQLite ALTER COLUMN compatibility
- Backward compatible: existing members automatically become INDIVIDUAL type

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-12-04 12:15:55 +01:00
co-authored by Claude
parent 630af11403
commit 5053b80998
12 changed files with 375 additions and 40 deletions
+34 -6
View File
@@ -6,7 +6,7 @@ from sqlalchemy import select
from src.database import async_session_maker
from src.models.member import Member as MemberModel
from src.schemas.member import CreateMemberInput, Member, UpdateMemberInput
from src.validation import validate_email, validate_first_name, validate_phone
from src.validation import validate_email, validate_phone, validate_member_type_requirements
class MemberNotFoundError(Exception):
@@ -31,7 +31,9 @@ class Query:
return Member(
id=db_member.id,
member_type=db_member.member_type,
first_name=db_member.first_name,
company_name=db_member.company_name,
last_name=db_member.last_name,
street=db_member.street,
apartment_number=db_member.apartment_number,
@@ -58,7 +60,9 @@ class Query:
return [
Member(
id=m.id,
member_type=m.member_type,
first_name=m.first_name,
company_name=m.company_name,
last_name=m.last_name,
street=m.street,
apartment_number=m.apartment_number,
@@ -79,8 +83,12 @@ class Mutation:
@strawberry.mutation
async def create_member(self, input: CreateMemberInput) -> Member:
"""Create a new member with validation."""
# Validate required field
validate_first_name(input.first_name)
# Validate conditional requirements based on member type
validate_member_type_requirements(
member_type=input.member_type.value,
first_name=input.first_name,
company_name=input.company_name
)
# Conditional validation (only if fields are provided)
validate_email(input.email)
@@ -89,7 +97,9 @@ class Mutation:
async with async_session_maker() as session:
# Create new member
db_member = MemberModel(
member_type=input.member_type,
first_name=input.first_name,
company_name=input.company_name,
last_name=input.last_name,
street=input.street,
apartment_number=input.apartment_number,
@@ -106,7 +116,9 @@ class Mutation:
return Member(
id=db_member.id,
member_type=db_member.member_type,
first_name=db_member.first_name,
company_name=db_member.company_name,
last_name=db_member.last_name,
street=db_member.street,
apartment_number=db_member.apartment_number,
@@ -132,15 +144,29 @@ class Mutation:
if db_member is None:
raise MemberNotFoundError(f"Member with ID {input.id} not found")
# Validate if fields are being updated
if input.first_name is not None:
validate_first_name(input.first_name)
# Determine final values for validation (combining existing and new values)
final_member_type = input.member_type.value if input.member_type is not None else db_member.member_type.value
final_first_name = input.first_name if input.first_name is not None else db_member.first_name
final_company_name = input.company_name if input.company_name is not None else db_member.company_name
# Validate conditional requirements with final values
validate_member_type_requirements(
member_type=final_member_type,
first_name=final_first_name,
company_name=final_company_name
)
# Conditional validation (only if fields are provided)
validate_email(input.email)
validate_phone(input.phone)
# Update fields if provided
if input.member_type is not None:
db_member.member_type = input.member_type
if input.first_name is not None:
db_member.first_name = input.first_name
if input.company_name is not None:
db_member.company_name = input.company_name
if input.last_name is not None:
db_member.last_name = input.last_name
if input.street is not None:
@@ -163,7 +189,9 @@ class Mutation:
return Member(
id=db_member.id,
member_type=db_member.member_type,
first_name=db_member.first_name,
company_name=db_member.company_name,
last_name=db_member.last_name,
street=db_member.street,
apartment_number=db_member.apartment_number,