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
@@ -0,0 +1,31 @@
# Design: Organization Member Support
## Data Model Changes
### Member Table
- Add `member_type` column: Enum (`INDIVIDUAL`, `ORGANIZATION`). Default to `INDIVIDUAL` for existing records.
- Add `company_name` column: String, nullable.
- Alter `first_name`: Change from `nullable=False` to `nullable=True`.
### Constraints
- **Application Level**:
- If `member_type` is `INDIVIDUAL`: `first_name` must be present.
- If `member_type` is `ORGANIZATION`: `company_name` must be present.
- **Database Level**:
- We can't easily enforce conditional not-null constraints in standard SQL without triggers or check constraints. For simplicity and portability (SQLite/Postgres), we will enforce this at the application level (Pydantic/SQLAlchemy validators) and potentially add a CHECK constraint if supported by both dialects easily.
- `first_name` will become nullable in the DB schema.
## API Changes
### GraphQL Schema
- Update `Member` type to include `memberType` and `companyName`.
- Update `createMember` and `updateMember` inputs.
- Add validation logic in resolvers to enforce the conditional requirements.
## Migration Strategy
1. Add `member_type` column with default 'INDIVIDUAL'.
2. Add `company_name` column.
3. Alter `first_name` to be nullable.
## Technical Note
- Using `memberType` instead of `kind` to avoid conflicts with GraphQL introspection queries which use `kind` as a metadata field.
@@ -0,0 +1,19 @@
# Proposal: Support Organization Members
## Summary
Update the member data model to support both individual persons and organizations. Organizations will have a company name instead of a mandatory first name.
## Background
Currently, the system only supports individual members with a mandatory first name. We need to accommodate organizational members (companies, associations, etc.) where the primary identifier is the organization name.
## Goals
- Allow distinguishing between individual and organization members.
- Make `first_name` optional for organizations.
- Add `company_name` field, mandatory for organizations.
- Ensure data integrity based on member type.
## Non-Goals
- Complex hierarchy of organizations and contacts (for now, just a simple distinction).
## Technical Note
- Field name: `memberType` (not `kind` to avoid GraphQL introspection conflicts)
@@ -0,0 +1,59 @@
# Spec: Organization Support
## ADDED Requirements
### Requirement: Distinguish member kind
The system MUST allow defining a member as either an individual or an organization.
#### Scenario: Create individual member
**Given** client provides kind="INDIVIDUAL"
**And** first_name="John"
**When** createMember mutation executes
**Then** member is created with kind INDIVIDUAL
**And** first_name is "John"
#### Scenario: Create organization member
**Given** client provides kind="ORGANIZATION"
**And** company_name="Acme Corp"
**When** createMember mutation executes
**Then** member is created with kind ORGANIZATION
**And** company_name is "Acme Corp"
### Requirement: Conditional mandatory fields
The system MUST enforce different mandatory fields based on member kind.
#### Scenario: Organization requires company name
**Given** client provides kind="ORGANIZATION"
**And** company_name is missing or empty
**When** createMember mutation executes
**Then** validation error is raised
**And** error message indicates company_name is required for organizations
#### Scenario: Organization allows missing first name
**Given** client provides kind="ORGANIZATION"
**And** company_name="Acme Corp"
**And** first_name is missing
**When** createMember mutation executes
**Then** member is created successfully
**And** first_name is null
#### Scenario: Individual requires first name
**Given** client provides kind="INDIVIDUAL"
**And** first_name is missing
**When** createMember mutation executes
**Then** validation error is raised
**And** error message indicates first_name is required for individuals
## MODIFIED Requirements
### Requirement: Member data structure
The Member data structure MUST be modified to include kind and company_name, and allow nullable first_name.
#### Scenario: Query member includes new fields
**Given** member exists with kind="ORGANIZATION" and company_name="Acme Corp"
**When** member query executes requesting kind and companyName
**Then** kind is returned as "ORGANIZATION"
**And** companyName is returned as "Acme Corp"
@@ -0,0 +1,10 @@
# Tasks: Support Organization Members
- [x] Create database migration to add `member_type` and `company_name` and make `first_name` nullable <!-- id: 0 -->
- [x] Update SQLAlchemy `Member` model with new fields and validation logic <!-- id: 1 -->
- [x] Update Pydantic schemas / GraphQL inputs to support new fields (`memberType`, `companyName`) <!-- id: 2 -->
- [x] Update `createMember` and `updateMember` resolvers with conditional validation <!-- id: 3 -->
- [x] Update tests to cover new scenarios (organization creation, validation rules) <!-- id: 4 -->
- [x] Verify backward compatibility for existing individual members <!-- id: 5 -->
**Note**: Using `memberType` field name (not `kind`) to avoid GraphQL introspection conflicts.