-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Fix duplicate pending and account premium #748
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -122,7 +122,8 @@ export async function syncStripeDataToDb({ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pendingInvites: true, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| users: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| select: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _count: true, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| email: true, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _count: { select: { emailAccounts: true } }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -149,17 +150,27 @@ export async function syncStripeDataToDb({ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async function syncSeats( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| premium: Prisma.PremiumGetPayload<{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| select: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| users: { select: { _count: { select: { emailAccounts: true } } } }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| users: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| select: { email: true; _count: { select: { emailAccounts: true } } }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pendingInvites: true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| stripeSubscriptionItemId: true; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }>, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // total seats = premium users + pending invites | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Get all connected user emails | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const connectedUserEmails = new Set(premium.users.map((u) => u.email)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Filter out pending invites that are already connected users to avoid double counting | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const uniquePendingInvites = (premium.pendingInvites || []).filter( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (email) => !connectedUserEmails.has(email), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // total seats = premium users + unique pending invites (excluding duplicates) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const totalSeats = | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sumBy(premium.users, (u) => u._count.emailAccounts) + | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (premium.pendingInvites?.length || 0); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| uniquePendingInvites.length; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+162
to
174
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chainStill double-counts duplicate pending invites; compare and dedup case-insensitively to avoid overbilling. Apply: - // Get all connected user emails
- const connectedUserEmails = new Set(premium.users.map((u) => u.email));
-
- // Filter out pending invites that are already connected users to avoid double counting
- const uniquePendingInvites = (premium.pendingInvites || []).filter(
- (email) => !connectedUserEmails.has(email),
- );
+ // Build lowercase sets for robust comparison
+ const connectedUserEmails = new Set(
+ premium.users
+ .map((u) => u.email?.toLowerCase())
+ .filter(Boolean) as string[],
+ );
+ // Unique pending invites (case-insensitive), excluding already-connected
+ const uniquePendingInvites = Array.from(
+ new Set(
+ (premium.pendingInvites || [])
+ .map((e) => e?.toLowerCase())
+ .filter(Boolean) as string[],
+ ),
+ ).filter((email) => !connectedUserEmails.has(email));Optional: if emails are normalized to lowercase at creation time, confirm via a quick repo search and we can drop the lowercasing here for perf. 🏁 Script executed: #!/bin/bash
# Display lines around the pendingInvites logic for context
sed -n '140,180p' apps/web/ee/billing/stripe/sync-stripe.ts
# Locate all references to the type or origin of `pendingInvites`
rg -n "pendingInvites" -g 'apps/web/**/*.ts'Length of output: 2242 🏁 Script executed: #!/bin/bash
# List all Prisma schema files to locate the Premium model definition
find . -type f -name '*.prisma'Length of output: 65 🏁 Script executed: #!/bin/bash
# Extract the Premium model definition from Prisma schema
sed -n '/model Premium {/,/}/p' apps/web/prisma/schema.prisma
# Inspect pendingInvites normalization logic in premium actions
sed -n '180,250p' apps/web/utils/actions/premium.tsLength of output: 4740 🏁 Script executed: #!/bin/bash
# Inspect how uniqueEmails is derived and normalized before setting pendingInvites
sed -n '1,200p' apps/web/utils/actions/premium.tsLength of output: 6567 Deduplicate Apply in apps/web/ee/billing/stripe/sync-stripe.ts (around lines 162–174): - // Get all connected user emails
- const connectedUserEmails = new Set(premium.users.map((u) => u.email));
-
- // Filter out pending invites that are already connected users to avoid double counting
- const uniquePendingInvites = (premium.pendingInvites || []).filter(
- (email) => !connectedUserEmails.has(email),
- );
+ // Build lowercase sets for robust comparison
+ const connectedUserEmails = new Set(
+ premium.users
+ .map((u) => u.email?.toLowerCase())
+ .filter(Boolean) as string[],
+ );
+ // Dedupe and exclude already-connected invites
+ const uniquePendingInvites = Array.from(
+ new Set(
+ (premium.pendingInvites || [])
+ .map((e) => e?.toLowerCase())
+ .filter(Boolean) as string[],
+ ),
+ ).filter((email) => !connectedUserEmails.has(email));📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await updateAccountSeatsForPremium(premium, totalSeats); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1 @@ | ||
| v2.8.10 | ||
| v2.8.11 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
❓ Verification inconclusive
Don’t drop pendingInvites when there are 0 connected users; also dedup invites themselves (case-insensitive).
Current gating on
emailAddresses?.lengthhides pending invites for brand‑new orgs, and duplicates insidependingInvitescan still surface. Normalize to lowercase for comparison, keep original casing for existing users, and only fallback to an empty row if the combined list is empty.Apply:
If invites are already normalized at write-time, confirm and we can simplify the lowercasing here.
Always merge pendingInvites & existing users unconditionally
Replace the
emailAddresses?.length ? … : [{ email: "" }]branch with an unconditional merge that[{ email: "" }]only if the combined array is empty.Confirm whether
pendingInvitesvalues are already normalized upstream to simplify/remove thetoLowerCase()calls.🤖 Prompt for AI Agents