-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Add disabledAt and lastConversionAt columns to Link table #2885
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
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughAdded three nullable DateTime fields to the Link model and propagated timestamp updates (lastLeadAt, lastConversionAt, disabledAt) into lead/sale/webhook/import flows; introduced a helper to choose importer timestamps; the API transformer strips these fields from outbound link payloads. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant Event as External Event (webhook/importer/shopify)
participant Processor as App Processor (track/import/create flows)
participant DB as Prisma (Link)
Note over DB: Link schema now includes\n`disabledAt`, `lastLeadAt`, `lastConversionAt`
Event->>Processor: incoming lead / sale / commission / customer
Processor->>DB: prisma.link.update({ increments..., lastLeadAt?: date, lastConversionAt?: date })
alt importer with candidate timestamps
Processor->>Processor: compute via updateLinkStatsForImporter(current, new)
Processor->>DB: set returned timestamp when present
end
DB-->>Processor: updated Link
Processor-->>Client: transform-link strips timestamps before response
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
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.
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.
Actionable comments posted: 7
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (7)
apps/web/app/(ee)/api/stripe/integration/webhook/checkout-session-completed.ts (1)
304-309: Don’t bump lead stats when no lead was recorded
clickEventstays truthy even when we findexistingCustomer, so we skiprecordLead(...)yet still incrementleads, pushlastLeadAt, and add two units of workspace usage. Returning customers will therefore skew the new timestamp and inflate metrics. Gate these updates on!existingCustomerso we only touch lead stats when a fresh lead was actually created.- ...(clickEvent && { + ...(clickEvent && !existingCustomer && { leads: { increment: 1, }, lastLeadAt: new Date(), }), @@ - usage: { - increment: clickEvent ? 2 : 1, + usage: { + increment: clickEvent && !existingCustomer ? 2 : 1, },Also applies to: 332-334
apps/web/app/(ee)/api/stripe/integration/webhook/invoice-paid.ts (1)
164-169:lastConversionAtshould update on every sale
lastConversionAtis nested under thefirstConversionFlagbranch, so we never refresh it after the very first conversion. That leaves the new column stuck at the first conversion timestamp and defeats the purpose of tracking the most recent conversion. Please move the timestamp update outside the conditional. Example fix:...(firstConversionFlag && { conversions: { increment: 1, }, - lastConversionAt: new Date(), }), + lastConversionAt: new Date(),apps/web/lib/integrations/shopify/create-sale.ts (1)
87-92: KeeplastConversionAtin sync with every saleSame issue as above: by keeping
lastConversionAtinside thefirstConversionFlagbranch, the column never reflects sales after the first one. Please lift the timestamp out of that conditional so it always updates. Suggested change:...(firstConversionFlag && { conversions: { increment: 1, }, - lastConversionAt: new Date(), }), + lastConversionAt: new Date(),apps/web/lib/partnerstack/import-commissions.ts (1)
344-355:lastConversionAtmust advance even when it’s not a first conversionBecause
lastConversionAtsits inside theisFirstConversionblock, it only updates for the first unique conversion per customer. Imports for later conversions will leave it stale. Please compute the timestamp separately so it can update whenever newer data comes in:- ...(isFirstConversion({ - customer, - linkId: customer.linkId, - }) && { - conversions: { - increment: 1, - }, - lastConversionAt: updateLinkStatsForImporter({ - currentTimestamp: customer.link.lastConversionAt, - newTimestamp: new Date(commission.created_at), - }), - }), + ...(isFirstConversion({ + customer, + linkId: customer.linkId, + }) && { + conversions: { + increment: 1, + }, + }), + lastConversionAt: updateLinkStatsForImporter({ + currentTimestamp: customer.link.lastConversionAt, + newTimestamp: new Date(commission.created_at), + }),apps/web/lib/api/conversions/track-sale.ts (1)
464-469: Always refreshlastConversionAtwhen a sale is recordedWithin
_trackSale,lastConversionAtis coupled to the unique-conversion branch, so we stop updating it after the first conversion for a customer. Move the timestamp outside that conditional so the column reflects the latest sale:...(firstConversionFlag && { conversions: { increment: 1, }, - lastConversionAt: new Date(), }), + lastConversionAt: new Date(),apps/web/lib/tolt/import-commissions.ts (1)
356-367: Let importer refreshlastConversionAtfor every newer commissionHere too, the timestamp only updates when
isFirstConversionis true. Later commissions (even if newer in time) won’t bumplastConversionAt. Please lift the timestamp update outside the conditional block:- ...(isFirstConversion({ - customer: customerFound, - linkId: customerFound.linkId, - }) && { - conversions: { - increment: 1, - }, - lastConversionAt: updateLinkStatsForImporter({ - currentTimestamp: customerFound.link.lastConversionAt, - newTimestamp: new Date(commission.created_at), - }), - }), + ...(isFirstConversion({ + customer: customerFound, + linkId: customerFound.linkId, + }) && { + conversions: { + increment: 1, + }, + }), + lastConversionAt: updateLinkStatsForImporter({ + currentTimestamp: customerFound.link.lastConversionAt, + newTimestamp: new Date(commission.created_at), + }),apps/web/lib/firstpromoter/import-commissions.ts (1)
352-361:lastConversionAtcan regress under concurrent commission imports.Multiple commissions for the same link run in parallel via
Promise.allSettled. Each call reuses the samecustomer.link.lastConversionAtsnapshot and can therefore overwrite the column with an older timestamp. Push the comparison down to Prisma so only newer conversion times land.- await Promise.allSettled([ + const shouldCountConversion = isFirstConversion({ + customer: customer, + linkId: customer.linkId, + }); + const nextLastConversionAt = + shouldCountConversion + ? updateLinkStatsForImporter({ + currentTimestamp: customer.link.lastConversionAt, + newTimestamp: new Date(commission.created_at), + }) + : undefined; + + await Promise.allSettled([ @@ - prisma.link.update({ - where: { - id: customer.linkId, - }, - data: { - ...(isFirstConversion({ - customer: customer, - linkId: customer.linkId, - }) && { - conversions: { - increment: 1, - }, - lastConversionAt: updateLinkStatsForImporter({ - currentTimestamp: customer.link.lastConversionAt, - newTimestamp: new Date(commission.created_at), - }), - }), - sales: { - increment: 1, - }, - saleAmount: { - increment: saleAmount, - }, - }, - }), + prisma.link.update({ + where: { + id: customer.linkId, + }, + data: { + ...(shouldCountConversion && { + conversions: { + increment: 1, + }, + }), + sales: { + increment: 1, + }, + saleAmount: { + increment: saleAmount, + }, + }, + }), + nextLastConversionAt + ? prisma.link.updateMany({ + where: { + id: customer.linkId, + OR: [ + { lastConversionAt: null }, + { lastConversionAt: { lt: nextLastConversionAt } }, + ], + }, + data: { + lastConversionAt: nextLastConversionAt, + }, + }) + : Promise.resolve(),
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (17)
apps/web/app/(ee)/api/stripe/integration/webhook/checkout-session-completed.ts(2 hunks)apps/web/app/(ee)/api/stripe/integration/webhook/invoice-paid.ts(2 hunks)apps/web/app/(ee)/api/stripe/integration/webhook/utils.ts(2 hunks)apps/web/lib/actions/partners/create-manual-commission.ts(2 hunks)apps/web/lib/api/conversions/track-lead.ts(1 hunks)apps/web/lib/api/conversions/track-sale.ts(3 hunks)apps/web/lib/api/links/update-link-stats-for-importer.ts(1 hunks)apps/web/lib/firstpromoter/import-commissions.ts(2 hunks)apps/web/lib/firstpromoter/import-customers.ts(4 hunks)apps/web/lib/integrations/shopify/create-lead.ts(2 hunks)apps/web/lib/integrations/shopify/create-sale.ts(1 hunks)apps/web/lib/partnerstack/import-commissions.ts(2 hunks)apps/web/lib/partnerstack/import-customers.ts(4 hunks)apps/web/lib/rewardful/import-commissions.ts(2 hunks)apps/web/lib/rewardful/import-customers.ts(2 hunks)apps/web/lib/tolt/import-commissions.ts(2 hunks)apps/web/lib/tolt/import-customers.ts(4 hunks)
👮 Files not reviewed due to content moderation or server errors (2)
- apps/web/lib/api/conversions/track-lead.ts
- apps/web/lib/integrations/shopify/create-lead.ts
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-06-06T07:59:03.120Z
Learnt from: devkiran
PR: dubinc/dub#2177
File: apps/web/lib/api/links/bulk-create-links.ts:66-84
Timestamp: 2025-06-06T07:59:03.120Z
Learning: In apps/web/lib/api/links/bulk-create-links.ts, the team accepts the risk of potential undefined results from links.find() operations when building invalidLinks arrays, because existing links are fetched from the database based on the input links, so matches are expected to always exist.
Applied to files:
apps/web/lib/firstpromoter/import-customers.tsapps/web/lib/tolt/import-customers.tsapps/web/lib/partnerstack/import-customers.ts
📚 Learning: 2025-09-17T17:44:03.965Z
Learnt from: TWilson023
PR: dubinc/dub#2857
File: apps/web/lib/actions/partners/update-program.ts:96-0
Timestamp: 2025-09-17T17:44:03.965Z
Learning: In apps/web/lib/actions/partners/update-program.ts, the team prefers to keep the messagingEnabledAt update logic simple by allowing client-provided timestamps rather than implementing server-controlled timestamp logic to avoid added complexity.
Applied to files:
apps/web/app/(ee)/api/stripe/integration/webhook/checkout-session-completed.ts
🧬 Code graph analysis (9)
apps/web/lib/tolt/import-commissions.ts (1)
apps/web/lib/api/links/update-link-stats-for-importer.ts (1)
updateLinkStatsForImporter(1-20)
apps/web/lib/firstpromoter/import-customers.ts (1)
apps/web/lib/api/links/update-link-stats-for-importer.ts (1)
updateLinkStatsForImporter(1-20)
apps/web/lib/partnerstack/import-commissions.ts (1)
apps/web/lib/api/links/update-link-stats-for-importer.ts (1)
updateLinkStatsForImporter(1-20)
apps/web/lib/firstpromoter/import-commissions.ts (1)
apps/web/lib/api/links/update-link-stats-for-importer.ts (1)
updateLinkStatsForImporter(1-20)
apps/web/lib/rewardful/import-commissions.ts (1)
apps/web/lib/api/links/update-link-stats-for-importer.ts (1)
updateLinkStatsForImporter(1-20)
apps/web/lib/tolt/import-customers.ts (1)
apps/web/lib/api/links/update-link-stats-for-importer.ts (1)
updateLinkStatsForImporter(1-20)
apps/web/lib/partnerstack/import-customers.ts (1)
apps/web/lib/api/links/update-link-stats-for-importer.ts (1)
updateLinkStatsForImporter(1-20)
apps/web/lib/rewardful/import-customers.ts (1)
apps/web/lib/api/links/update-link-stats-for-importer.ts (1)
updateLinkStatsForImporter(1-20)
apps/web/lib/actions/partners/create-manual-commission.ts (1)
apps/web/lib/api/links/update-link-stats-for-importer.ts (1)
updateLinkStatsForImporter(1-20)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (1)
apps/web/app/(ee)/api/stripe/integration/webhook/utils.ts (1)
78-88:lastLeadAtupdate looks goodIncrementing the lead count while stamping
lastLeadAtwith the current time keeps the link stats in sync with the new column. No issues spotted.
|
@coderabbitai full review |
✅ Actions performedFull review triggered. |
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.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (4)
apps/web/lib/tolt/import-customers.ts (1)
247-259: Make the last-lead timestamp update conditionalEvery customer runs through this Promise, so multiple customers on the same link still share the old
link.lastLeadAt. If a slower task carries an olderlatestLeadAt, it can overwrite the fresher value you just wrote. Please hoist theupdateLinkStatsForImportercall outsidePromise.allSettledand persist through anupdateManywith alastLeadAt IS NULL OR lastLeadAt < candidateguard, like we’ve done in the Rewardful/PartnerStack fixes.Suggested change:
- await Promise.all([ + const nextLastLeadAt = updateLinkStatsForImporter({ + currentTimestamp: link.lastLeadAt, + newTimestamp: latestLeadAt, + }); + + await Promise.all([ recordLeadWithTimestamp({ ...clickEvent, @@ - prisma.link.update({ - where: { - id: link.id, - }, - data: { - leads: { - increment: 1, - }, - lastLeadAt: updateLinkStatsForImporter({ - currentTimestamp: link.lastLeadAt, - newTimestamp: latestLeadAt, - }), - }, - }), + prisma.link.update({ + where: { id: link.id }, + data: { + leads: { increment: 1 }, + }, + }), + nextLastLeadAt + ? prisma.link.updateMany({ + where: { + id: link.id, + OR: [ + { lastLeadAt: null }, + { lastLeadAt: { lt: nextLastLeadAt } }, + ], + }, + data: { lastLeadAt: nextLastLeadAt }, + }) + : Promise.resolve(), ]);apps/web/lib/integrations/shopify/create-sale.ts (1)
87-101:lastConversionAtnever updates after the first customer conversion.Line 92 ties the timestamp update to
firstConversionFlag. For returning customers this flag isfalse, so every subsequent sale leaveslastConversionAtstale even though a conversion just happened. That breaks recency-driven features (dashboards, automations, cleanup jobs) because they’ll believe the link hasn’t converted since the very first buyer.Please move
lastConversionAtoutside thefirstConversionFlagguard so it updates on every sale while keeping the conversions counter gated.data: { ...(firstConversionFlag && { conversions: { increment: 1, }, }), - ...(firstConversionFlag && { - lastConversionAt: new Date(), - }), + lastConversionAt: new Date(), sales: { increment: 1, },apps/web/app/(ee)/api/stripe/integration/webhook/checkout-session-completed.ts (1)
296-323:lastConversionAtstops moving after a customer’s first Stripe checkout.On Line 315 the timestamp update is wrapped in the
firstConversionFlagblock. Any repeat conversion (flag = false) leaveslastConversionAtuntouched, so monitoring and lifecycle logic downstream will think the link hasn’t converted since the first customer. Please update the timestamp unconditionally while leaving the conversions counter gated....(firstConversionFlag && { conversions: { increment: 1, }, - lastConversionAt: new Date(), }), + lastConversionAt: new Date(),apps/web/lib/api/conversions/track-sale.ts (1)
458-476:lastConversionAtonly changes on the visitor’s first tracked sale.Line 469 places the timestamp behind
firstConversionFlag, so each subsequent sale by the same customer leaves the field frozen. This distorts “last conversion” analytics and any automation that relies on recency. Please updatelastConversionAtevery time_trackSaleruns, while keeping the conversions counter themselves gated byfirstConversionFlag.prisma.link.update({ where: { id: saleData.link_id, }, data: { ...(firstConversionFlag && { conversions: { increment: 1, }, - lastConversionAt: new Date(), }), + lastConversionAt: new Date(), sales: { increment: 1, },
🧹 Nitpick comments (1)
apps/web/app/(ee)/api/stripe/integration/webhook/invoice-paid.ts (1)
158-169: Use the actual event timestamp instead of “now”Align lastConversionAt with when the sale occurred (late webhooks happen). Stripe provides epoch seconds.
- lastConversionAt: new Date(), + lastConversionAt: new Date(invoice.created * 1000),
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (19)
apps/web/app/(ee)/api/stripe/integration/webhook/checkout-session-completed.ts(2 hunks)apps/web/app/(ee)/api/stripe/integration/webhook/invoice-paid.ts(2 hunks)apps/web/app/(ee)/api/stripe/integration/webhook/utils.ts(2 hunks)apps/web/lib/actions/partners/create-manual-commission.ts(7 hunks)apps/web/lib/api/conversions/track-lead.ts(1 hunks)apps/web/lib/api/conversions/track-sale.ts(3 hunks)apps/web/lib/api/links/update-link-stats-for-importer.ts(1 hunks)apps/web/lib/api/links/utils/transform-link.ts(1 hunks)apps/web/lib/firstpromoter/import-commissions.ts(2 hunks)apps/web/lib/firstpromoter/import-customers.ts(6 hunks)apps/web/lib/integrations/shopify/create-lead.ts(2 hunks)apps/web/lib/integrations/shopify/create-sale.ts(1 hunks)apps/web/lib/partnerstack/import-commissions.ts(2 hunks)apps/web/lib/partnerstack/import-customers.ts(6 hunks)apps/web/lib/rewardful/import-commissions.ts(2 hunks)apps/web/lib/rewardful/import-customers.ts(2 hunks)apps/web/lib/tolt/import-commissions.ts(2 hunks)apps/web/lib/tolt/import-customers.ts(5 hunks)packages/prisma/schema/link.prisma(2 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-09-17T17:44:03.965Z
Learnt from: TWilson023
PR: dubinc/dub#2857
File: apps/web/lib/actions/partners/update-program.ts:96-0
Timestamp: 2025-09-17T17:44:03.965Z
Learning: In apps/web/lib/actions/partners/update-program.ts, the team prefers to keep the messagingEnabledAt update logic simple by allowing client-provided timestamps rather than implementing server-controlled timestamp logic to avoid added complexity.
Applied to files:
apps/web/lib/api/conversions/track-lead.tsapps/web/lib/actions/partners/create-manual-commission.ts
📚 Learning: 2025-06-06T07:59:03.120Z
Learnt from: devkiran
PR: dubinc/dub#2177
File: apps/web/lib/api/links/bulk-create-links.ts:66-84
Timestamp: 2025-06-06T07:59:03.120Z
Learning: In apps/web/lib/api/links/bulk-create-links.ts, the team accepts the risk of potential undefined results from links.find() operations when building invalidLinks arrays, because existing links are fetched from the database based on the input links, so matches are expected to always exist.
Applied to files:
apps/web/lib/partnerstack/import-customers.tsapps/web/lib/tolt/import-customers.tsapps/web/lib/rewardful/import-customers.tsapps/web/lib/firstpromoter/import-customers.ts
🔇 Additional comments (14)
apps/web/lib/api/links/utils/transform-link.ts (1)
31-41: Hiding internal timestamps from API response — LGTMStripping disabledAt, lastLeadAt, and lastConversionAt here is appropriate and consistent with prior redactions.
packages/prisma/schema/link.prisma (2)
65-66: Migration gap applies to lastLeadAt/lastConversionAt as wellEnsure the same migration includes these fields.
Use the script above; expect to see ALTER TABLE statements adding all three columns in a single migration. If missing, generate one (e.g., pnpm prisma migrate dev --name add-link-timestamps) and commit.
10-10: Blocking: add and commit the Prisma migration for new columnsdisabledAt requires a migration. Without it, deploys will fail and columns won’t exist.
Run to verify whether a migration exists in this PR:
#!/bin/bash set -euo pipefail echo "Searching for migrations mentioning Link/disabledAt/lastLeadAt/lastConversionAt..." fd -t d migrations packages/prisma | wc -l rg -n -C2 -i 'disabledAt|lastLeadAt|lastConversionAt' packages/prisma/migrations || true echo "Prisma schema snapshot:" sed -n '1,140p' packages/prisma/schema/link.prisma | sed -n '1,120p'apps/web/lib/actions/partners/create-manual-commission.ts (1)
140-143: Good: preserving real event timestamps for link statsCapturing leadEventTimestamp/saleEventTimestamp from the actual events fixes the earlier “use now()” issue in manual/backfill flows.
Also applies to: 230-231, 289-295, 389-391, 431-433
apps/web/lib/api/links/update-link-stats-for-importer.ts (1)
1-20: Avoid regressing timestamps under concurrency; enforce “only move forward” at DB levelThis helper bases decisions on a stale in-memory currentTimestamp and can overwrite a newer DB value during concurrent updates. Guard the update in the database (updateMany with a where clause) instead of returning a value to blindly set. Apply the same pattern at call sites.
Proposed addition (keep this function for now, but prefer using these DB-guarded helpers going forward):
// New helpers (exported) import type { Prisma } from "@dub/prisma"; // Use a transaction client when possible export async function setLastLeadAtIfNewer( tx: Prisma.TransactionClient, linkId: string, ts: Date, ) { return tx.link.updateMany({ where: { id: linkId, OR: [{ lastLeadAt: null }, { lastLeadAt: { lt: ts } }] }, data: { lastLeadAt: ts }, }); } export async function setLastConversionAtIfNewer( tx: Prisma.TransactionClient, linkId: string, ts: Date, ) { return tx.link.updateMany({ where: { id: linkId, OR: [{ lastConversionAt: null }, { lastConversionAt: { lt: ts } }], }, data: { lastConversionAt: ts }, }); }Then, at call sites, drop lastXAt from prisma.link.update data and invoke one of the helpers in the same Promise.all or transaction.
apps/web/lib/partnerstack/import-commissions.ts (1)
351-355: Guard lastConversionAt update in DB to prevent timestamp regressionThis still risks overwriting a newer value under concurrency. Move the update to an updateMany guarded by “NULL or older”.
- lastConversionAt: updateLinkStatsForImporter({ - currentTimestamp: customer.link.lastConversionAt, - newTimestamp: new Date(commission.created_at), - }),Add a DB-guarded update alongside the existing Promise.all:
await Promise.all([ prisma.commission.create({ /* ... */ }), recordSaleWithTimestamp({ /* ... */ }), // update link stats prisma.link.update({ /* ... conversions/sales/saleAmount ... */ }), + // ensure lastConversionAt only moves forward + isFirstConversion({ customer, linkId: customer.linkId }) && + prisma.link.updateMany({ + where: { + id: customer.linkId, + OR: [ + { lastConversionAt: null }, + { lastConversionAt: { lt: new Date(commission.created_at) } }, + ], + }, + data: { lastConversionAt: new Date(commission.created_at) }, + }), // update customer stats prisma.customer.update({ /* ... */ }), ]);apps/web/lib/firstpromoter/import-commissions.ts (1)
358-361: Same DB-level guard needed for lastConversionAtPrevent regressions by updating only when NULL/older via updateMany.
- lastConversionAt: updateLinkStatsForImporter({ - currentTimestamp: customer.link.lastConversionAt, - newTimestamp: new Date(commission.created_at), - }),Add a guarded update alongside the existing Promise.allSettled:
await Promise.allSettled([ prisma.commission.create({ /* ... */ }), recordSaleWithTimestamp({ /* ... */ }), prisma.link.update({ /* conversions/sales/saleAmount ... */ }), + (isFirstConversion({ customer, linkId: customer.linkId }) && + prisma.link.updateMany({ + where: { + id: customer.linkId, + OR: [ + { lastConversionAt: null }, + { lastConversionAt: { lt: new Date(commission.created_at) } }, + ], + }, + data: { lastConversionAt: new Date(commission.created_at) }, + })) as any, prisma.customer.update({ /* ... */ }), ]);apps/web/lib/rewardful/import-commissions.ts (1)
352-366:lastConversionAtcan still move backwards under concurrent commission importsMultiple commissions for the same link run this
Promise.allin parallel. Each call still readscustomerFound.link.lastConversionAt, the helper returns a plain Date, and whichever promise settles last overwrites the column—even if itscommission.created_atis older. We need to move the freshness check into the database so concurrent runs can’t regress the timestamp (same issue previously highlighted). Compute the new value once, update counters separately, and use anupdateManywith a guard onlastConversionAt.- await Promise.all([ + const shouldCountConversion = isFirstConversion({ + customer: customerFound, + linkId: customerFound.linkId, + }); + const nextLastConversionAt = shouldCountConversion + ? updateLinkStatsForImporter({ + currentTimestamp: customerFound.link.lastConversionAt, + newTimestamp: new Date(commission.created_at), + }) + : undefined; + + await Promise.all([ @@ - prisma.link.update({ - where: { id: customerFound.linkId }, - data: { - ...(isFirstConversion({ - customer: customerFound, - linkId: customerFound.linkId, - }) && { - conversions: { - increment: 1, - }, - lastConversionAt: updateLinkStatsForImporter({ - currentTimestamp: customerFound.link.lastConversionAt, - newTimestamp: new Date(commission.created_at), - }), - }), - sales: { increment: 1 }, - saleAmount: { increment: amount }, - }, - }), + prisma.link.update({ + where: { id: customerFound.linkId }, + data: { + ...(shouldCountConversion && { + conversions: { + increment: 1, + }, + }), + sales: { increment: 1 }, + saleAmount: { increment: amount }, + }, + }), + nextLastConversionAt + ? prisma.link.updateMany({ + where: { + id: customerFound.linkId, + OR: [ + { lastConversionAt: null }, + { lastConversionAt: { lt: nextLastConversionAt } }, + ], + }, + data: { + lastConversionAt: nextLastConversionAt, + }, + }) + : Promise.resolve(),apps/web/lib/rewardful/import-customers.ts (1)
222-231: Still need a DB-side guard to keeplastLeadAtmonotonic
Promise.allmeans multiple referrals hitting the same link run this block concurrently. Each invocation captures the same stalelink.lastLeadAt, the helper returns a plain Date, and whichever promise resolves last wins. That means an olderbecame_lead_atcan overwrite a newer timestamp. We have the same race the earlier review flagged—only moving the comparison into userland didn’t fix it. Please push the “is this fresher?” check into the database (e.g. unconditionalupdatefor the counters plus a conditionalupdateManyfor the timestamp) so concurrent imports can’t regresslastLeadAt.- await Promise.all([ - recordLeadWithTimestamp({ - ...clickEvent, - event_id: nanoid(16), - event_name: "Sign up", - customer_id: customerId, - timestamp: new Date(referral.became_lead_at).toISOString(), - }), - - prisma.link.update({ - where: { id: link.id }, - data: { - leads: { increment: 1 }, - lastLeadAt: updateLinkStatsForImporter({ - currentTimestamp: link.lastLeadAt, - newTimestamp: new Date(referral.became_lead_at), - }), - }, - }), - ]); + const nextLastLeadAt = updateLinkStatsForImporter({ + currentTimestamp: link.lastLeadAt, + newTimestamp: new Date(referral.became_lead_at), + }); + + await Promise.all([ + recordLeadWithTimestamp({ + ...clickEvent, + event_id: nanoid(16), + event_name: "Sign up", + customer_id: customerId, + timestamp: new Date(referral.became_lead_at).toISOString(), + }), + prisma.link.update({ + where: { id: link.id }, + data: { + leads: { increment: 1 }, + }, + }), + nextLastLeadAt + ? prisma.link.updateMany({ + where: { + id: link.id, + OR: [ + { lastLeadAt: null }, + { lastLeadAt: { lt: nextLastLeadAt } }, + ], + }, + data: { lastLeadAt: nextLastLeadAt }, + }) + : Promise.resolve(), + ]);apps/web/lib/partnerstack/import-customers.ts (1)
274-286: GuardlastLeadAtagainst concurrent downgradesWe still have the same race here: every customer update reads the original
link.lastLeadAt, so the slowest task can overwrite a newer value. Match the conditional-write pattern (compute once, then conditionalupdateMany) used in other importers to ensure we only persist strictly newer timestamps.apps/web/lib/firstpromoter/import-customers.ts (1)
278-282: ProtectlastLeadAtfrom regressing under concurrencyThis block still writes back whatever the stale snapshot said was “current,” so an older customer processed later can clobber a newer timestamp. Apply the same compute-and-conditional-update approach we’ve been standardizing on to keep
lastLeadAtmonotonic.apps/web/lib/api/conversions/track-lead.ts (1)
281-286: Lead recency capture looks good.Setting
lastLeadAtalongside the existing lead counter preserves freshness for real-time leads.apps/web/app/(ee)/api/stripe/integration/webhook/utils.ts (1)
78-88: Stripe-created lead now updates recency correctly.The timestamp pairs nicely with the counter increment, so this path will surface when the most recent lead arrived.
apps/web/lib/integrations/shopify/create-lead.ts (1)
70-82: Shopify lead path keeps recency in sync.Updating
lastLeadAtwhenever a lead is recorded ensures the link’s fresh-lead metadata stays accurate.
Summary by CodeRabbit