| 186 | export async function getProfileOrganizations( |
| 187 | userId: User['id'], |
| 188 | options: GetProfileOrganizationsOptions = {} |
| 189 | ): Promise<ProfileOrganization[]> { |
| 190 | const { excludeAccessBlocked = false } = options; |
| 191 | |
| 192 | // Only pull in each org's current subscription status when the caller needs |
| 193 | // to hide "Access Blocked" orgs. organization_seats_purchases is append-only, |
| 194 | // so the most recently created row reflects the current state. This keeps |
| 195 | // getProfileOrganizations a single query and adds no seat-purchase lookup for |
| 196 | // callers that don't opt in. |
| 197 | const latestSeatPurchaseStatus = excludeAccessBlocked |
| 198 | ? sql<OrganizationSeatsPurchase['subscription_status'] | null>`( |
| 199 | SELECT ${organization_seats_purchases.subscription_status} |
| 200 | FROM ${organization_seats_purchases} |
| 201 | WHERE ${organization_seats_purchases.organization_id} = ${organizations.id} |
| 202 | ORDER BY ${organization_seats_purchases.created_at} DESC |
| 203 | LIMIT 1 |
| 204 | )` |
| 205 | : sql<OrganizationSeatsPurchase['subscription_status'] | null>`NULL`; |
| 206 | |
| 207 | const results = await db |
| 208 | .select({ |
| 209 | organization: organizations, |
| 210 | membership: organization_memberships, |
| 211 | latestSeatPurchaseStatus, |
| 212 | }) |
| 213 | .from(organizations) |
| 214 | .innerJoin( |
| 215 | organization_memberships, |
| 216 | eq(organization_memberships.organization_id, organizations.id) |
| 217 | ) |
| 218 | .where(and(eq(organization_memberships.kilo_user_id, userId), isNull(organizations.deleted_at))) |
| 219 | // Deterministic order so callers can treat the first element as a stable |
| 220 | // default selection (e.g. profile `selectedOrganizationId`). Old form was |
| 221 | // created_at, id; sales-demo orgs win so profile selectedOrganizationId is |
| 222 | // the demo org. |
| 223 | .orderBy( |
| 224 | desc(sql`COALESCE((${organizations.settings}->>'is_sales_demo')::boolean, false)`), |
| 225 | asc(organizations.created_at), |
| 226 | asc(organizations.id) |
| 227 | ); |
| 228 | |
| 229 | const parentOrganizationIdsWithMembershipInAChild = new Set( |
| 230 | results.flatMap(result => |
| 231 | result.organization.parent_organization_id ? [result.organization.parent_organization_id] : [] |
| 232 | ) |
| 233 | ); |
| 234 | |
| 235 | const now = new Date(); |
| 236 | return results |
| 237 | .filter(result => !parentOrganizationIdsWithMembershipInAChild.has(result.organization.id)) |
| 238 | .filter(result => { |
| 239 | if (!excludeAccessBlocked) return true; |
| 240 | const classification = classifyOrganizationEntitlement({ |
| 241 | organization: result.organization, |
| 242 | latestSeatPurchaseStatus: result.latestSeatPurchaseStatus, |
| 243 | now, |
| 244 | }); |
| 245 | return !classification.isTrialExpiredForEnforcement; |