Coverage for polar/member/service.py: 30%
61 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-12-05 17:15 +0000
« prev ^ index » next coverage.py v7.10.6, created at 2025-12-05 17:15 +0000
1from collections.abc import Sequence 1a
2from typing import Any 1a
3from uuid import UUID 1a
5import structlog 1a
6from sqlalchemy import UnaryExpression, asc, desc 1a
7from sqlalchemy.exc import IntegrityError 1a
9from polar.auth.models import AuthSubject, Organization, User 1a
10from polar.kit.pagination import PaginationParams 1a
11from polar.kit.sorting import Sorting 1a
12from polar.models.customer import Customer 1a
13from polar.models.member import Member, MemberRole 1a
14from polar.models.organization import Organization as OrgModel 1a
15from polar.postgres import AsyncReadSession, AsyncSession 1a
17from .repository import MemberRepository 1a
18from .sorting import MemberSortProperty 1a
20log = structlog.get_logger() 1a
23class MemberService: 1a
24 async def list( 1a
25 self,
26 session: AsyncReadSession,
27 auth_subject: AuthSubject[User | Organization],
28 *,
29 customer_id: UUID | None = None,
30 pagination: PaginationParams,
31 sorting: list[Sorting[MemberSortProperty]] = [
32 (MemberSortProperty.created_at, True)
33 ],
34 ) -> tuple[Sequence[Member], int]:
35 """List members with pagination and filtering."""
36 repository = MemberRepository.from_session(session)
37 statement = repository.get_readable_statement(auth_subject)
39 if customer_id is not None:
40 statement = statement.where(Member.customer_id == customer_id)
42 order_by_clauses: list[UnaryExpression[Any]] = []
43 for criterion, is_desc in sorting:
44 clause_function = desc if is_desc else asc
45 if criterion == MemberSortProperty.created_at:
46 order_by_clauses.append(clause_function(Member.created_at))
47 statement = statement.order_by(*order_by_clauses)
49 return await repository.paginate(
50 statement, limit=pagination.limit, page=pagination.page
51 )
53 async def create_owner_member( 1a
54 self,
55 session: AsyncSession,
56 customer: Customer,
57 organization: OrgModel,
58 *,
59 owner_email: str | None = None,
60 owner_name: str | None = None,
61 owner_external_id: str | None = None,
62 ) -> Member | None:
63 """
64 Create an owner member for a customer if feature flag is enabled.
66 Args:
67 session: Database session
68 customer: Customer to create member for
69 organization: Organization the customer belongs to
70 owner_email: Optional override for member email (defaults to customer.email)
71 owner_name: Optional override for member name (defaults to customer.name)
72 owner_external_id: Optional override for member external_id (defaults to customer.external_id)
74 Returns:
75 Created/existing Member if feature flag enabled, None if flag disabled
76 """
77 if not organization.feature_settings.get("member_model_enabled", False):
78 log.debug(
79 "member.create_owner_member.skipped",
80 reason="feature_flag_disabled",
81 customer_id=customer.id,
82 organization_id=organization.id,
83 )
84 return None
86 repository = MemberRepository.from_session(session)
88 email = owner_email or customer.email
89 name = owner_name or customer.name
90 external_id = owner_external_id or customer.external_id
92 existing_member = await repository.get_by_customer_and_email(
93 session, customer, email=email
94 )
95 if existing_member:
96 log.debug(
97 "member.create_owner_member.skipped",
98 reason="member_already_exists",
99 customer_id=customer.id,
100 member_id=existing_member.id,
101 )
102 return existing_member
104 member = Member(
105 customer_id=customer.id,
106 organization_id=organization.id,
107 email=email,
108 name=name,
109 external_id=external_id,
110 role=MemberRole.owner,
111 )
113 try:
114 created_member = await repository.create(member)
115 log.info(
116 "member.create_owner_member.success",
117 customer_id=customer.id,
118 member_id=created_member.id,
119 organization_id=organization.id,
120 )
121 return created_member
122 except IntegrityError as e:
123 log.info(
124 "member.create_owner_member.constraint_violation",
125 customer_id=customer.id,
126 organization_id=organization.id,
127 error=str(e),
128 reason="Likely race condition - member already exists",
129 )
130 existing_member = await repository.get_by_customer_and_email(
131 session, customer, email=email
132 )
133 if existing_member:
134 log.info(
135 "member.create_owner_member.found_existing",
136 customer_id=customer.id,
137 member_id=existing_member.id,
138 )
139 return existing_member
141 # Weird state: IntegrityError but member doesn't exist
142 # Re-raise to fail customer creation and maintain data consistency
143 log.error(
144 "member.create_owner_member.integrity_error_no_member",
145 customer_id=customer.id,
146 organization_id=organization.id,
147 error=str(e),
148 )
149 raise
151 async def list_by_customer( 1a
152 self,
153 session: AsyncReadSession,
154 customer_id: UUID,
155 ) -> Sequence[Member]:
156 repository = MemberRepository.from_session(session)
157 return await repository.list_by_customer(session, customer_id)
159 async def list_by_customers( 1a
160 self,
161 session: AsyncReadSession,
162 customer_ids: Sequence[UUID],
163 ) -> Sequence[Member]:
164 """
165 Get all members for multiple customers (batch loading to avoid N+1 queries).
166 """
167 repository = MemberRepository.from_session(session)
168 return await repository.list_by_customers(session, customer_ids)
171member_service = MemberService() 1a