Skip to content

Conversation

@psincraian
Copy link
Contributor

📋 Summary

Related Issue: Fixes #8784

Implements the member model for customer seats, decoupling seat members from Customer entities. When the member_model_enabled feature flag is enabled, Members are created independently under the billing customer (purchaser), and CustomerSeat.customer_id references the purchaser rather than the seat occupant.

🎯 What

  • Added member_model_enabled feature flag (organization-level setting)
  • When flag is true: Members are created under billing customer, not as separate Customers
  • Added email column to CustomerSeat for storing pending invitation email addresses
  • Added member_id field to CustomerSeat schema
  • Updated all seat assignment, claiming, and revocation flows with dual code paths (new and old)
  • Backward compatible: old behavior preserved when flag is disabled

🤔 Why

This change enables organizations to manage seat members without creating a Customer record for each one, reducing database bloat and improving permission management. Seat members become team members of the billing customer's organization instead of independent customers.

🔧 How

The implementation uses two parallel code paths:

  • NEW PATH (member_model_enabled=true): Creates Members only, uses billing_customer_id on seat, stores email on seat
  • OLD PATH (member_model_enabled=false): Creates Customers and Members (backward compatible)

The feature flag is checked in all critical methods: assign_seat, claim_seat, revoke_seat, resend_invitation

🧪 Testing

  • Lint: ✅ Passed
  • Type check: ✅ Passed (7 files)
  • Tests: Infrastructure issue with Minio (not code-related)

📝 Additional Notes

Validation was added to reject customer_id and external_customer_id parameters when member_model_enabled=true, since these are not supported in the new model.

🤖 Generated with Claude Code

@vercel
Copy link

vercel bot commented Jan 7, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

2 Skipped Deployments
Project Deployment Review Updated (UTC)
polar Ignored Ignored Preview Jan 8, 2026 1:42pm
polar-sandbox Ignored Ignored Preview Jan 8, 2026 1:42pm

@psincraian psincraian force-pushed the psincraian/richmond-v1 branch from 4552491 to bd66cf4 Compare January 7, 2026 12:57
Comment on lines 649 to 656
if member_model_enabled:
# NEW PATH: Keep customer_id (billing customer), clear member_id and email
seat.member_id = None
seat.email = None
else:
# OLD PATH: Clear customer_id (seat member customer)
seat.customer_id = None

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can simplify this by setting the same attributes to None. It should have no downside and will simplify the code

Comment on lines 345 to 344
# OLD PATH: member_model_enabled = False (backward compatible)
# Create Customer for seat member
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we simplify these 2 if conditions?

# Get product info and organization from either subscription or order
if seat.subscription_id and seat.subscription and seat.subscription.product:
organization_id = seat.subscription.product.organization_id
organization = seat.subscription.product.organization
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sure that the organization is eager loaded in the order and product

Returns:
Member entity for the seat member
"""
from polar.member.repository import MemberRepository
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@claude update the CLAUDE.md to mention that imports should always be on top

@psincraian psincraian force-pushed the psincraian/richmond-v1 branch 3 times, most recently from eff5104 to 3ac05c0 Compare January 8, 2026 08:21
@psincraian psincraian force-pushed the psincraian/richmond-v1 branch 2 times, most recently from 3823916 to 0468eac Compare January 8, 2026 08:34
@psincraian psincraian changed the base branch from main to psincraian/add-email-to-customer-seats-migration January 8, 2026 08:34
@psincraian psincraian force-pushed the psincraian/add-email-to-customer-seats-migration branch from cdac7e7 to 2a67a1a Compare January 8, 2026 08:38
@psincraian psincraian force-pushed the psincraian/richmond-v1 branch from 0468eac to 9ca018e Compare January 8, 2026 08:39
@psincraian psincraian force-pushed the psincraian/add-email-to-customer-seats-migration branch from 2a67a1a to a66ec86 Compare January 8, 2026 08:42
@psincraian psincraian force-pushed the psincraian/richmond-v1 branch from 9ca018e to 45ecc3d Compare January 8, 2026 08:43
Base automatically changed from psincraian/add-email-to-customer-seats-migration to main January 8, 2026 10:07
@psincraian psincraian force-pushed the psincraian/richmond-v1 branch from 45ecc3d to 9caf079 Compare January 8, 2026 11:45
psincraian and others added 5 commits January 8, 2026 14:41
Decouple seat members from Customer entities by adding a member_model_enabled
feature flag. When enabled, Members exist independently under the billing
customer (purchaser), with customer_id on CustomerSeat referencing the
purchaser rather than the seat occupant. Includes new email column on
CustomerSeat for pending invitations.

🤖 Generated with Claude Code

Co-Authored-By: Claude Haiku 4.5 <[email protected]>
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <[email protected]>
The test was expecting seat.customer_id to equal the seat member's customer,
but with member_model_enabled=True, seat.customer_id should equal the billing
customer (subscription owner). Updated assertions to match the actual
implementation behavior.

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

Co-Authored-By: Claude Opus 4.5 <[email protected]>
- Simplify revoke_seat by clearing all identifiers unconditionally
- Combine validation conditions into single check in assign_seat
- Add Order.product.organization eager loading in repository
- Move MemberRepository import to top of file
- Add imports guideline to CLAUDE.md
- Add tests for member_model_enabled flows:
  - test_claim_seat_with_member_model_enabled
  - test_revoke_seat_with_member_model_enabled
  - test_resend_invitation_with_member_model_enabled
  - test_assign_seat_rejects_customer_id_when_member_model_enabled
  - test_assign_seat_requires_email_when_member_model_enabled

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

Co-Authored-By: Claude Opus 4.5 <[email protected]>
Introduce SeatAssignmentTarget dataclass to unify the seat creation logic.
The only branching point is now resolving the target (who the seat is for),
while seat creation, token generation, and notifications are unified.

Changes:
- Add SeatAssignmentTarget dataclass with customer_id, member_id, email, seat_member_email
- Add _resolve_member_model_target() for member_model_enabled=True path
- Add _resolve_legacy_target() for member_model_enabled=False path
- Refactor assign_seat() to use unified seat creation logic
- Simplify claim_seat() to share validation and claim logic between paths

This reduces code duplication and makes the feature flag logic clearer.

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

Co-Authored-By: Claude Opus 4.5 <[email protected]>
@psincraian psincraian force-pushed the psincraian/richmond-v1 branch from 9caf079 to 75cc4b8 Compare January 8, 2026 13:42
@psincraian psincraian merged commit 73eb6e6 into main Jan 8, 2026
15 checks passed
@psincraian psincraian deleted the psincraian/richmond-v1 branch January 8, 2026 13:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Phase 1: Do not create customers for seats members

2 participants