From 79ba1461186f26b8a69bb694f7da55fd057a2a21 Mon Sep 17 00:00:00 2001 From: Mahdi Date: Sat, 17 Oct 2026 16:52:20 +0330 Subject: [PATCH] Wire prior archive cutoff through API Co-authored-by: Cursor --- .superpowers/sdd/task-3-report.md | 38 +++++++++++++++++++ apps/api/src/activities/activities.service.ts | 6 ++- apps/api/src/ai/ai-ask.service.ts | 8 +--- apps/api/src/auth/auth.service.ts | 12 ++++-- apps/api/src/auth/jwt.strategy.ts | 5 ++- apps/api/src/billing/billing.service.ts | 8 ++-- apps/api/src/contacts/companies.service.ts | 2 - apps/api/src/contacts/people.service.ts | 2 - apps/api/src/pipelines/deals-excel.service.ts | 2 +- apps/api/src/pipelines/pipelines.service.ts | 11 +++++- apps/api/src/reports/reports.service.ts | 14 ++----- 11 files changed, 74 insertions(+), 34 deletions(-) create mode 100644 .superpowers/sdd/task-3-report.md diff --git a/.superpowers/sdd/task-3-report.md b/.superpowers/sdd/task-3-report.md new file mode 100644 index 0000000..293a82c --- /dev/null +++ b/.superpowers/sdd/task-3-report.md @@ -0,0 +1,38 @@ +# Task 3 Report: Wire API call-sites + auth cutoff + stop auto-stamp + +## Status + +DONE_WITH_CONCERNS + +## Summary + +- Updated all API call sites in scope to use `dealPriorWhere(lens, cutoff)` and `activityPriorWhere(lens, cutoff)`. +- Removed person/company prior-lens filtering from contacts, reports, and AI count paths. +- Exposed `priorArchive.cutoffAt` from auth DTO generation and kept JWT `priorCutoff` null unless both grant and organization cutoff exist. +- Added `promotedFromPriorAt` mapping to Deal and Activity DTOs. +- Stopped billing module enable from auto-writing `priorArchiveEnabledAt`; existing cutoff values are preserved. + +## Files Changed + +- `apps/api/src/auth/jwt.strategy.ts` +- `apps/api/src/auth/auth.service.ts` +- `apps/api/src/billing/billing.service.ts` +- `apps/api/src/pipelines/pipelines.service.ts` +- `apps/api/src/pipelines/deals-excel.service.ts` +- `apps/api/src/activities/activities.service.ts` +- `apps/api/src/reports/reports.service.ts` +- `apps/api/src/ai/ai-ask.service.ts` +- `apps/api/src/contacts/people.service.ts` +- `apps/api/src/contacts/companies.service.ts` + +## Verification + +- `npx prisma generate` from `apps/api`: passed. +- `npm run typecheck -w @mento/api`: passed. +- IDE lint diagnostics for edited files: no errors found. +- `graphify update .`: attempted after edits, crashed with exit code `3221225477` and no output; treated as non-blocking per task instructions. + +## Concerns + +- No focused tests were added; verification is the requested API typecheck plus lint diagnostics. +- `billing.service.ts` still runs the existing prior-activity backfill only when an organization already has a cutoff. It no longer creates a cutoff when enabling the module. diff --git a/apps/api/src/activities/activities.service.ts b/apps/api/src/activities/activities.service.ts index 2fe2378..38971a6 100644 --- a/apps/api/src/activities/activities.service.ts +++ b/apps/api/src/activities/activities.service.ts @@ -65,6 +65,7 @@ type ActivityRow = { reminderSentAt: Date | null; completedAt: Date | null; importSource?: string; + promotedFromPriorAt?: Date | null; tags: string[]; createdAt: Date; updatedAt: Date; @@ -119,7 +120,7 @@ export class ActivitiesService { ): Promise { const { start, end } = dayBounds(); const scope = activityAccessWhere(actor); - const prior = activityPriorWhere(actor.priorLens, new Date(), actor.priorCutoff); + const prior = activityPriorWhere(actor.priorLens, actor.priorCutoff); const [todayCount, overdueCount] = await Promise.all([ this.prisma.activity.count({ where: { @@ -701,7 +702,7 @@ export class ActivitiesService { if (search) andParts.push(search); const onRecord = Boolean(entity?.personId || entity?.companyId || entity?.dealId); if (!onRecord) { - andParts.push(activityPriorWhere(actor.priorLens, new Date(), actor.priorCutoff)); + andParts.push(activityPriorWhere(actor.priorLens, actor.priorCutoff)); } const base: Prisma.ActivityWhereInput = { organizationId, @@ -901,6 +902,7 @@ export class ActivitiesService { reminderSentAt: row.reminderSentAt?.toISOString() ?? null, completedAt: row.completedAt?.toISOString() ?? null, importSource: row.importSource ?? 'manual', + promotedFromPriorAt: row.promotedFromPriorAt?.toISOString() ?? null, tags: row.tags ?? [], person: row.person ? { diff --git a/apps/api/src/ai/ai-ask.service.ts b/apps/api/src/ai/ai-ask.service.ts index 1e47c91..1c8c834 100644 --- a/apps/api/src/ai/ai-ask.service.ts +++ b/apps/api/src/ai/ai-ask.service.ts @@ -48,9 +48,7 @@ import { } from './ask-count'; import { activityPriorWhere, - companyPriorWhere, dealPriorWhere, - personPriorWhere, } from '../common/prior-archive'; type CatalogItem = AskMentoSourceDto; @@ -325,13 +323,11 @@ Rules: where: { ...shared, ...(intent.mood ? { mood: intent.mood } : {}), - ...(intent.personSource ? {} : personPriorWhere(actor.priorLens)), }, }) : await this.prisma.company.count({ where: { ...shared, - ...(intent.personSource ? {} : companyPriorWhere(actor.priorLens)), }, }); const who = intent.entity === 'person' ? 'مشتری' : 'شرکت'; @@ -372,7 +368,7 @@ Rules: ? { status: 'open' as const, OR: [{ isStale: true }, { isRotten: true }] } : {}), }, - dealPriorWhere(actor.priorLens), + dealPriorWhere(actor.priorLens, actor.priorCutoff), ], }; const n = await this.prisma.deal.count({ where }); @@ -406,7 +402,7 @@ Rules: organizationId: orgId, ...assigneeWhere(actor), ...created, - ...activityPriorWhere(actor.priorLens, new Date(), actor.priorCutoff), + ...activityPriorWhere(actor.priorLens, actor.priorCutoff), }, }), ticket: () => diff --git a/apps/api/src/auth/auth.service.ts b/apps/api/src/auth/auth.service.ts index 62a4e5a..8fb112e 100644 --- a/apps/api/src/auth/auth.service.ts +++ b/apps/api/src/auth/auth.service.ts @@ -658,7 +658,9 @@ export class AuthService { const membership = await this.prisma.membership.findFirst({ where: { userId: user.id, organizationId, isActive: true }, include: { - organization: { select: { name: true, enabledModules: true } }, + organization: { + select: { name: true, enabledModules: true, priorArchiveEnabledAt: true }, + }, }, }); const role = membership?.role ?? user.role; @@ -685,13 +687,17 @@ export class AuthService { membership: { priorArchiveEnabled?: boolean; priorArchiveLens?: string; - organization?: { enabledModules?: string[] }; + organization?: { enabledModules?: string[]; priorArchiveEnabledAt?: Date | null }; } | null, ): AuthUserDto['priorArchive'] { if (!membership?.priorArchiveEnabled) return undefined; const modules = this.billing.normalizeModules(membership.organization?.enabledModules); if (!modules.includes('prior_data_archive')) return undefined; - return { lens: membership.priorArchiveLens === 'archive' ? 'archive' : 'current' }; + const orgCutoff = membership.organization?.priorArchiveEnabledAt ?? null; + return { + lens: membership.priorArchiveLens === 'archive' ? 'archive' : 'current', + cutoffAt: orgCutoff ? orgCutoff.toISOString() : null, + }; } private async throttleChallenges( diff --git a/apps/api/src/auth/jwt.strategy.ts b/apps/api/src/auth/jwt.strategy.ts index 52c13c2..d851e78 100644 --- a/apps/api/src/auth/jwt.strategy.ts +++ b/apps/api/src/auth/jwt.strategy.ts @@ -107,7 +107,10 @@ export class JwtStrategy extends PassportStrategy(Strategy) { visibleUserIds, scopes, priorLens: priorGranted ? priorLens : 'off', - priorCutoff: priorGranted ? membership.organization.priorArchiveEnabledAt : null, + priorCutoff: + priorGranted && membership.organization.priorArchiveEnabledAt + ? membership.organization.priorArchiveEnabledAt + : null, }; } diff --git a/apps/api/src/billing/billing.service.ts b/apps/api/src/billing/billing.service.ts index 533ba55..4f4857f 100644 --- a/apps/api/src/billing/billing.service.ts +++ b/apps/api/src/billing/billing.service.ts @@ -257,17 +257,15 @@ export class BillingService { select: { priorArchiveEnabledAt: true }, }); const enablePrior = cleaned.includes('prior_data_archive'); - const cutoff = prev?.priorArchiveEnabledAt ?? new Date(); await this.prisma.organization.update({ where: { id: organizationId }, data: { enabledModules: cleaned, - ...(enablePrior && !prev?.priorArchiveEnabledAt - ? { priorArchiveEnabledAt: cutoff } - : {}), }, }); - if (enablePrior) await this.backfillPriorActivities(organizationId, cutoff); + if (enablePrior && prev?.priorArchiveEnabledAt) { + await this.backfillPriorActivities(organizationId, prev.priorArchiveEnabledAt); + } return cleaned; } diff --git a/apps/api/src/contacts/companies.service.ts b/apps/api/src/contacts/companies.service.ts index f8dfbf0..efdeb3f 100644 --- a/apps/api/src/contacts/companies.service.ts +++ b/apps/api/src/contacts/companies.service.ts @@ -26,7 +26,6 @@ import { import { normalizeTags } from '../common/tags'; import { normalizePersonImportSource } from './person-source'; import { buildSearchWhere, ci } from '../common/search'; -import { companyPriorWhere, listPriorLens } from '../common/prior-archive'; import { GamificationService } from '../gamification/gamification.service'; function parseDateOrNull(raw?: string | null): Date | null { @@ -98,7 +97,6 @@ export class CompaniesService { const where: Prisma.CompanyWhereInput = { organizationId: actor.organizationId, ...ownerWhere(actor), - ...companyPriorWhere(listPriorLens(actor.priorLens, q, false)), ...(search ? { AND: [search] } : {}), }; diff --git a/apps/api/src/contacts/people.service.ts b/apps/api/src/contacts/people.service.ts index 37d5397..e7a0366 100644 --- a/apps/api/src/contacts/people.service.ts +++ b/apps/api/src/contacts/people.service.ts @@ -43,7 +43,6 @@ import { selectMonthBirthdays } from './birthday-month'; import { renderSmsText } from '../common/sms-text'; import { normalizeTags } from '../common/tags'; import { buildSearchWhere, ci } from '../common/search'; -import { listPriorLens, personPriorWhere } from '../common/prior-archive'; import { normalizePersonImportSource, personImportSourceLabel, @@ -138,7 +137,6 @@ export class PeopleService implements OnModuleInit { ...ownerWhere(actor), ...(opts?.mood ? { mood: opts.mood } : {}), ...(sourceFilter ? { importSource: sourceFilter } : {}), - ...personPriorWhere(listPriorLens(actor.priorLens, q, Boolean(sourceFilter))), ...(search ? { AND: [search] } : {}), }; diff --git a/apps/api/src/pipelines/deals-excel.service.ts b/apps/api/src/pipelines/deals-excel.service.ts index 9ee4407..820f6cc 100644 --- a/apps/api/src/pipelines/deals-excel.service.ts +++ b/apps/api/src/pipelines/deals-excel.service.ts @@ -114,7 +114,7 @@ export class DealsExcelService { where: { organizationId: actor.organizationId, ...dealAccessWhere(actor), - ...dealPriorWhere(actor.priorLens), + ...dealPriorWhere(actor.priorLens, actor.priorCutoff), }, include: { person: { diff --git a/apps/api/src/pipelines/pipelines.service.ts b/apps/api/src/pipelines/pipelines.service.ts index 271bb47..70cfa0f 100644 --- a/apps/api/src/pipelines/pipelines.service.ts +++ b/apps/api/src/pipelines/pipelines.service.ts @@ -44,6 +44,7 @@ type DealRow = { expectedCloseAt: Date | null; code: string | null; importSource: string; + promotedFromPriorAt: Date | null; sourceRef: string | null; probability: number | null; sourceDetail: string | null; @@ -295,7 +296,12 @@ export class PipelinesService { pipelineId?: string, actor?: Pick< Actor, - 'userId' | 'role' | 'dataVisibility' | 'visibleUserIds' | 'priorLens' + | 'userId' + | 'role' + | 'dataVisibility' + | 'visibleUserIds' + | 'priorLens' + | 'priorCutoff' >, includePrior = false, ): Promise { @@ -323,7 +329,7 @@ export class PipelinesService { organizationId, pipelineId: resolved.id, ...(actor ? dealAccessWhere(actor) : {}), - ...(includePrior ? {} : dealPriorWhere(actor?.priorLens)), + ...(includePrior ? {} : dealPriorWhere(actor?.priorLens, actor?.priorCutoff)), }, include: { person: { select: { id: true, firstName: true, lastName: true } }, @@ -575,6 +581,7 @@ export class PipelinesService { expectedCloseAt: row.expectedCloseAt?.toISOString() ?? null, code: row.code, importSource: row.importSource, + promotedFromPriorAt: row.promotedFromPriorAt?.toISOString() ?? null, sourceRef: row.sourceRef, probability: row.probability, sourceDetail: row.sourceDetail, diff --git a/apps/api/src/reports/reports.service.ts b/apps/api/src/reports/reports.service.ts index 78d0c63..95dd638 100644 --- a/apps/api/src/reports/reports.service.ts +++ b/apps/api/src/reports/reports.service.ts @@ -29,7 +29,6 @@ import { decimalToBase, loadOrgRates, toBaseAmount } from '../common/currency'; import { activityPriorWhere, dealPriorWhere, - personPriorWhere, } from '../common/prior-archive'; import { addJalaliMonths, @@ -62,9 +61,8 @@ export class ReportsService { const prevStart = new Date(prevEnd.getTime() - durationMs); const now = new Date(); - const peoplePrior = personPriorWhere(actor.priorLens); - const dealsPrior = dealPriorWhere(actor.priorLens, now); - const activitiesPrior = activityPriorWhere(actor.priorLens, now, actor.priorCutoff); + const dealsPrior = dealPriorWhere(actor.priorLens, actor.priorCutoff); + const activitiesPrior = activityPriorWhere(actor.priorLens, actor.priorCutoff); const { jy, jm } = dateToJalali(now); const sixAgo = addJalaliMonths(jy, jm, -5); const sixMonthStart = startOfJalaliMonth(sixAgo.jy, sixAgo.jm); @@ -160,7 +158,6 @@ export class ReportsService { where: { organizationId: orgId, ...personScope, - ...peoplePrior, }, select: { mood: true, createdAt: true }, }), @@ -1551,9 +1548,8 @@ export class ReportsService { const yesterdayEnd = endOfDay(yesterday); const weekAgo = new Date(now); weekAgo.setDate(weekAgo.getDate() - 7); - const peoplePrior = personPriorWhere(actor.priorLens); - const dealsPrior = dealPriorWhere(actor.priorLens, now); - const activitiesPrior = activityPriorWhere(actor.priorLens, now, actor.priorCutoff); + const dealsPrior = dealPriorWhere(actor.priorLens, actor.priorCutoff); + const activitiesPrior = activityPriorWhere(actor.priorLens, actor.priorCutoff); const [ wonDeals, @@ -1618,7 +1614,6 @@ export class ReportsService { where: { organizationId: orgId, ...personScope, - ...peoplePrior, createdAt: { gte: curMonthStart, lte: curMonthEnd }, }, }), @@ -1626,7 +1621,6 @@ export class ReportsService { where: { organizationId: orgId, ...personScope, - ...peoplePrior, createdAt: { gte: prevMonthStart, lte: prevMonthEnd }, }, }),