Skip to content

feat(subscription): enhance sync functionality to include email searc… - #180

Merged
Apexone11 merged 1 commit into
mainfrom
laptop-branch
Apr 4, 2026
Merged

feat(subscription): enhance sync functionality to include email searc…#180
Apexone11 merged 1 commit into
mainfrom
laptop-branch

Conversation

@Apexone11

Copy link
Copy Markdown
Owner

…h and broader subscription status checks

Copilot AI review requested due to automatic review settings April 4, 2026 07:40

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Sorry @Apexone11, you have reached your weekly rate limit of 1500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@Apexone11
Apexone11 merged commit 58ee753 into main Apr 4, 2026
2 of 6 checks passed
@Apexone11
Apexone11 deleted the laptop-branch branch April 4, 2026 07:40

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Enhances the payments self-service subscription sync endpoint to find a user’s Stripe customer(s) more reliably (metadata + email) and to detect additional subscription statuses when repairing the local Subscription record.

Changes:

  • Fetch the authenticated user’s email from the DB and use it as an additional Stripe customer lookup path.
  • Aggregate candidate Stripe customer IDs from both metadata search and email list results.
  • Expand subscription status checks during sync to include active, trialing, and past_due, and return more descriptive failure messaging.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +469 to +477
// Search by email
if (dbUser?.email) {
try {
const byEmail = await stripe.customers.list({
email: dbUser.email,
limit: 10,
})
if (trialingSubs.data.length > 0) subs.data.push(...trialingSubs.data)
byEmail.data.forEach((c) => customerIds.add(c.id))
} catch {

Copilot AI Apr 4, 2026

Copy link

Choose a reason for hiding this comment

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

Customer lookup by email makes it possible to sync a subscription that doesn’t actually belong to the logged-in user (e.g., if an account can be created/updated with an email they don’t control, or if the Stripe customer was created outside StudyHub but shares the same email). Consider only using the email-based customer matches as candidates and then requiring a positive match on customer.metadata.studyhub_user_id or subscription.metadata.studyhub_user_id before syncing; also skip any customer whose metadata points at a different StudyHub user.

Copilot uses AI. Check for mistakes.
Comment on lines +504 to +517
for (const sub of subs.data) {
const priceId = sub.items?.data?.[0]?.price?.id || ''
const resolved = planFromPriceId(priceId)
const plan = resolved || 'pro_monthly'

await prisma.subscription.upsert({
where: { userId: req.user.userId },
create: {
userId: req.user.userId,
stripeCustomerId: customerId,
stripeSubscriptionId: sub.id,
stripePriceId: priceId,
plan,
status: sub.status,

Copilot AI Apr 4, 2026

Copy link

Choose a reason for hiding this comment

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

Before upserting the subscription for req.user.userId, the code should validate that the Stripe object being used actually belongs to that StudyHub user (e.g., sub.metadata.studyhub_user_id or the customer’s metadata.studyhub_user_id). Without this, any matching customerId (especially from the email search) can result in attaching the wrong Stripe subscription/customer to the current user.

Copilot uses AI. Check for mistakes.
Comment on lines +495 to 546
for (const status of statusesToCheck) {
if (synced) break
try {
const subs = await stripe.subscriptions.list({
customer: customerId,
status,
limit: 1,
})

for (const sub of subs.data) {
const priceId = sub.items?.data?.[0]?.price?.id || ''
const resolved = planFromPriceId(priceId)
const plan = resolved || 'pro_monthly'

await prisma.subscription.upsert({
where: { userId: req.user.userId },
create: {
userId: req.user.userId,
stripeCustomerId: customerId,
stripeSubscriptionId: sub.id,
stripePriceId: priceId,
plan,
status: sub.status,
currentPeriodStart: new Date(sub.current_period_start * 1000),
currentPeriodEnd: new Date(sub.current_period_end * 1000),
cancelAtPeriodEnd: sub.cancel_at_period_end,
},
update: {
stripeCustomerId: customerId,
stripeSubscriptionId: sub.id,
stripePriceId: priceId,
plan,
status: sub.status,
currentPeriodStart: new Date(sub.current_period_start * 1000),
currentPeriodEnd: new Date(sub.current_period_end * 1000),
cancelAtPeriodEnd: sub.cancel_at_period_end,
canceledAt: null,
},
})

synced = true
log.info(
{ userId: req.user.userId, plan, subId: sub.id, customerId },
'User self-synced subscription',
)
break
}
} catch {
// Continue with next status
}
}
if (synced) break

Copilot AI Apr 4, 2026

Copy link

Choose a reason for hiding this comment

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

The nested loops can trigger up to ~3 Stripe API calls per matched customer (and up to 20 customerIds), which risks slow responses/timeouts and unnecessary Stripe load. Consider listing subscriptions once per customer (e.g., request all statuses in one call if supported) and then filtering locally, rather than calling subscriptions.list separately for each status.

Suggested change
for (const status of statusesToCheck) {
if (synced) break
try {
const subs = await stripe.subscriptions.list({
customer: customerId,
status,
limit: 1,
})
for (const sub of subs.data) {
const priceId = sub.items?.data?.[0]?.price?.id || ''
const resolved = planFromPriceId(priceId)
const plan = resolved || 'pro_monthly'
await prisma.subscription.upsert({
where: { userId: req.user.userId },
create: {
userId: req.user.userId,
stripeCustomerId: customerId,
stripeSubscriptionId: sub.id,
stripePriceId: priceId,
plan,
status: sub.status,
currentPeriodStart: new Date(sub.current_period_start * 1000),
currentPeriodEnd: new Date(sub.current_period_end * 1000),
cancelAtPeriodEnd: sub.cancel_at_period_end,
},
update: {
stripeCustomerId: customerId,
stripeSubscriptionId: sub.id,
stripePriceId: priceId,
plan,
status: sub.status,
currentPeriodStart: new Date(sub.current_period_start * 1000),
currentPeriodEnd: new Date(sub.current_period_end * 1000),
cancelAtPeriodEnd: sub.cancel_at_period_end,
canceledAt: null,
},
})
synced = true
log.info(
{ userId: req.user.userId, plan, subId: sub.id, customerId },
'User self-synced subscription',
)
break
}
} catch {
// Continue with next status
}
}
if (synced) break
if (synced) break
try {
const subs = await stripe.subscriptions.list({
customer: customerId,
status: 'all',
limit: 100,
})
const sub = statusesToCheck
.map((status) => subs.data.find((candidate) => candidate.status === status))
.find(Boolean)
if (!sub) continue
const priceId = sub.items?.data?.[0]?.price?.id || ''
const resolved = planFromPriceId(priceId)
const plan = resolved || 'pro_monthly'
await prisma.subscription.upsert({
where: { userId: req.user.userId },
create: {
userId: req.user.userId,
stripeCustomerId: customerId,
stripeSubscriptionId: sub.id,
stripePriceId: priceId,
plan,
status: sub.status,
currentPeriodStart: new Date(sub.current_period_start * 1000),
currentPeriodEnd: new Date(sub.current_period_end * 1000),
cancelAtPeriodEnd: sub.cancel_at_period_end,
},
update: {
stripeCustomerId: customerId,
stripeSubscriptionId: sub.id,
stripePriceId: priceId,
plan,
status: sub.status,
currentPeriodStart: new Date(sub.current_period_start * 1000),
currentPeriodEnd: new Date(sub.current_period_end * 1000),
cancelAtPeriodEnd: sub.cancel_at_period_end,
canceledAt: null,
},
})
synced = true
log.info(
{ userId: req.user.userId, plan, subId: sub.id, customerId },
'User self-synced subscription',
)
} catch {
// Continue with next customer
}

Copilot uses AI. Check for mistakes.
Comment on lines +458 to +467
// Search by metadata
try {
const byMeta = await stripe.customers.search({
query: `metadata["studyhub_user_id"]:"${req.user.userId}"`,
limit: 10,
})
byMeta.data.forEach((c) => customerIds.add(c.id))
} catch {
// Search API may not be available in test mode
}

Copilot AI Apr 4, 2026

Copy link

Choose a reason for hiding this comment

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

These empty catch {} blocks swallow Stripe errors silently, which makes it hard to debug real production failures (e.g., bad credentials, network issues, malformed query). Consider logging at least a warning/debug with enough context (userId + which search path failed) and/or capturing the error, while still gracefully degrading.

Copilot uses AI. Check for mistakes.
Comment on lines +542 to +544
} catch {
// Continue with next status
}

Copilot AI Apr 4, 2026

Copy link

Choose a reason for hiding this comment

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

The catch {} here drops the Stripe error and continues, which can mask systemic issues (e.g., Stripe outage or auth failure) and lead to confusing "No active subscription" responses. Consider logging (include customerId and status) and distinguishing between "not found" vs "Stripe call failed" so support can diagnose sync failures.

Copilot uses AI. Check for mistakes.
Apexone11 added a commit that referenced this pull request Aug 2, 2026
Clears 3 advisories (2 high) that landed after round 3.
**Lockfile-only** — no manifest changes.

| Alert | Package | Advisory floor | Now at |
|---|---|---|---|
| #179, #182 (high) | postcss | 8.5.18 | **8.5.25** (all 3 lockfiles) |
| #180 (medium) | tar | 7.5.21 | **7.5.22** (root + frontend) |

## Not included: react-router (deliberate)
The 2 remaining high alerts (#176, #177) are `react-router`, and the fix
is **8.3.0 — a major bump from 7.18.1**. CLAUDE.md lists React Router
among the majors that require an explicit founder approval, so it is
held out of this PR. It touches every route in the app and deserves its
own PR with a full route smoke pass. Dependabot has it open as
#450/#451.

Also open and awaiting the same call: #446 recharts 2→3, #447
@vitejs/plugin-react 5→6, #448 @testing-library/jest-dom 6→7, #449
rollup-plugin-visualizer 6→7 — all majors.

## Validation
- Backend: lint ✅ · build ✅ · tests ✅ **3541 passed**
- Frontend: lint ✅ 0 errors · build ✅ · **906 passed**
- Release-log entry added (CI gate)
- `playwright-smoke` remains the known-red baseline (red on main since
2026-06-02)

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

## Summary by Sourcery

Update dependency lockfiles to address recent security advisories for
postcss and tar, and document the changes in the release log.

Enhancements:
- Record security round 4 dependency updates and remaining react-router
advisories in the v2.3.0 release log entry.

Chores:
- Upgrade postcss to 8.5.25 and tar to 7.5.22 across all lockfiles to
clear three new security advisories.
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.

2 participants