Wire prior archive cutoff through API
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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.
|
||||||
@@ -65,6 +65,7 @@ type ActivityRow = {
|
|||||||
reminderSentAt: Date | null;
|
reminderSentAt: Date | null;
|
||||||
completedAt: Date | null;
|
completedAt: Date | null;
|
||||||
importSource?: string;
|
importSource?: string;
|
||||||
|
promotedFromPriorAt?: Date | null;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
@@ -119,7 +120,7 @@ export class ActivitiesService {
|
|||||||
): Promise<ActivitySummaryDto> {
|
): Promise<ActivitySummaryDto> {
|
||||||
const { start, end } = dayBounds();
|
const { start, end } = dayBounds();
|
||||||
const scope = activityAccessWhere(actor);
|
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([
|
const [todayCount, overdueCount] = await Promise.all([
|
||||||
this.prisma.activity.count({
|
this.prisma.activity.count({
|
||||||
where: {
|
where: {
|
||||||
@@ -701,7 +702,7 @@ export class ActivitiesService {
|
|||||||
if (search) andParts.push(search);
|
if (search) andParts.push(search);
|
||||||
const onRecord = Boolean(entity?.personId || entity?.companyId || entity?.dealId);
|
const onRecord = Boolean(entity?.personId || entity?.companyId || entity?.dealId);
|
||||||
if (!onRecord) {
|
if (!onRecord) {
|
||||||
andParts.push(activityPriorWhere(actor.priorLens, new Date(), actor.priorCutoff));
|
andParts.push(activityPriorWhere(actor.priorLens, actor.priorCutoff));
|
||||||
}
|
}
|
||||||
const base: Prisma.ActivityWhereInput = {
|
const base: Prisma.ActivityWhereInput = {
|
||||||
organizationId,
|
organizationId,
|
||||||
@@ -901,6 +902,7 @@ export class ActivitiesService {
|
|||||||
reminderSentAt: row.reminderSentAt?.toISOString() ?? null,
|
reminderSentAt: row.reminderSentAt?.toISOString() ?? null,
|
||||||
completedAt: row.completedAt?.toISOString() ?? null,
|
completedAt: row.completedAt?.toISOString() ?? null,
|
||||||
importSource: row.importSource ?? 'manual',
|
importSource: row.importSource ?? 'manual',
|
||||||
|
promotedFromPriorAt: row.promotedFromPriorAt?.toISOString() ?? null,
|
||||||
tags: row.tags ?? [],
|
tags: row.tags ?? [],
|
||||||
person: row.person
|
person: row.person
|
||||||
? {
|
? {
|
||||||
|
|||||||
@@ -48,9 +48,7 @@ import {
|
|||||||
} from './ask-count';
|
} from './ask-count';
|
||||||
import {
|
import {
|
||||||
activityPriorWhere,
|
activityPriorWhere,
|
||||||
companyPriorWhere,
|
|
||||||
dealPriorWhere,
|
dealPriorWhere,
|
||||||
personPriorWhere,
|
|
||||||
} from '../common/prior-archive';
|
} from '../common/prior-archive';
|
||||||
|
|
||||||
type CatalogItem = AskMentoSourceDto;
|
type CatalogItem = AskMentoSourceDto;
|
||||||
@@ -325,13 +323,11 @@ Rules:
|
|||||||
where: {
|
where: {
|
||||||
...shared,
|
...shared,
|
||||||
...(intent.mood ? { mood: intent.mood } : {}),
|
...(intent.mood ? { mood: intent.mood } : {}),
|
||||||
...(intent.personSource ? {} : personPriorWhere(actor.priorLens)),
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
: await this.prisma.company.count({
|
: await this.prisma.company.count({
|
||||||
where: {
|
where: {
|
||||||
...shared,
|
...shared,
|
||||||
...(intent.personSource ? {} : companyPriorWhere(actor.priorLens)),
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const who = intent.entity === 'person' ? 'مشتری' : 'شرکت';
|
const who = intent.entity === 'person' ? 'مشتری' : 'شرکت';
|
||||||
@@ -372,7 +368,7 @@ Rules:
|
|||||||
? { status: 'open' as const, OR: [{ isStale: true }, { isRotten: true }] }
|
? { 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 });
|
const n = await this.prisma.deal.count({ where });
|
||||||
@@ -406,7 +402,7 @@ Rules:
|
|||||||
organizationId: orgId,
|
organizationId: orgId,
|
||||||
...assigneeWhere(actor),
|
...assigneeWhere(actor),
|
||||||
...created,
|
...created,
|
||||||
...activityPriorWhere(actor.priorLens, new Date(), actor.priorCutoff),
|
...activityPriorWhere(actor.priorLens, actor.priorCutoff),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
ticket: () =>
|
ticket: () =>
|
||||||
|
|||||||
@@ -658,7 +658,9 @@ export class AuthService {
|
|||||||
const membership = await this.prisma.membership.findFirst({
|
const membership = await this.prisma.membership.findFirst({
|
||||||
where: { userId: user.id, organizationId, isActive: true },
|
where: { userId: user.id, organizationId, isActive: true },
|
||||||
include: {
|
include: {
|
||||||
organization: { select: { name: true, enabledModules: true } },
|
organization: {
|
||||||
|
select: { name: true, enabledModules: true, priorArchiveEnabledAt: true },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const role = membership?.role ?? user.role;
|
const role = membership?.role ?? user.role;
|
||||||
@@ -685,13 +687,17 @@ export class AuthService {
|
|||||||
membership: {
|
membership: {
|
||||||
priorArchiveEnabled?: boolean;
|
priorArchiveEnabled?: boolean;
|
||||||
priorArchiveLens?: string;
|
priorArchiveLens?: string;
|
||||||
organization?: { enabledModules?: string[] };
|
organization?: { enabledModules?: string[]; priorArchiveEnabledAt?: Date | null };
|
||||||
} | null,
|
} | null,
|
||||||
): AuthUserDto['priorArchive'] {
|
): AuthUserDto['priorArchive'] {
|
||||||
if (!membership?.priorArchiveEnabled) return undefined;
|
if (!membership?.priorArchiveEnabled) return undefined;
|
||||||
const modules = this.billing.normalizeModules(membership.organization?.enabledModules);
|
const modules = this.billing.normalizeModules(membership.organization?.enabledModules);
|
||||||
if (!modules.includes('prior_data_archive')) return undefined;
|
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(
|
private async throttleChallenges(
|
||||||
|
|||||||
@@ -107,7 +107,10 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
|
|||||||
visibleUserIds,
|
visibleUserIds,
|
||||||
scopes,
|
scopes,
|
||||||
priorLens: priorGranted ? priorLens : 'off',
|
priorLens: priorGranted ? priorLens : 'off',
|
||||||
priorCutoff: priorGranted ? membership.organization.priorArchiveEnabledAt : null,
|
priorCutoff:
|
||||||
|
priorGranted && membership.organization.priorArchiveEnabledAt
|
||||||
|
? membership.organization.priorArchiveEnabledAt
|
||||||
|
: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -257,17 +257,15 @@ export class BillingService {
|
|||||||
select: { priorArchiveEnabledAt: true },
|
select: { priorArchiveEnabledAt: true },
|
||||||
});
|
});
|
||||||
const enablePrior = cleaned.includes('prior_data_archive');
|
const enablePrior = cleaned.includes('prior_data_archive');
|
||||||
const cutoff = prev?.priorArchiveEnabledAt ?? new Date();
|
|
||||||
await this.prisma.organization.update({
|
await this.prisma.organization.update({
|
||||||
where: { id: organizationId },
|
where: { id: organizationId },
|
||||||
data: {
|
data: {
|
||||||
enabledModules: cleaned,
|
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;
|
return cleaned;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ import {
|
|||||||
import { normalizeTags } from '../common/tags';
|
import { normalizeTags } from '../common/tags';
|
||||||
import { normalizePersonImportSource } from './person-source';
|
import { normalizePersonImportSource } from './person-source';
|
||||||
import { buildSearchWhere, ci } from '../common/search';
|
import { buildSearchWhere, ci } from '../common/search';
|
||||||
import { companyPriorWhere, listPriorLens } from '../common/prior-archive';
|
|
||||||
import { GamificationService } from '../gamification/gamification.service';
|
import { GamificationService } from '../gamification/gamification.service';
|
||||||
|
|
||||||
function parseDateOrNull(raw?: string | null): Date | null {
|
function parseDateOrNull(raw?: string | null): Date | null {
|
||||||
@@ -98,7 +97,6 @@ export class CompaniesService {
|
|||||||
const where: Prisma.CompanyWhereInput = {
|
const where: Prisma.CompanyWhereInput = {
|
||||||
organizationId: actor.organizationId,
|
organizationId: actor.organizationId,
|
||||||
...ownerWhere(actor),
|
...ownerWhere(actor),
|
||||||
...companyPriorWhere(listPriorLens(actor.priorLens, q, false)),
|
|
||||||
...(search ? { AND: [search] } : {}),
|
...(search ? { AND: [search] } : {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,6 @@ import { selectMonthBirthdays } from './birthday-month';
|
|||||||
import { renderSmsText } from '../common/sms-text';
|
import { renderSmsText } from '../common/sms-text';
|
||||||
import { normalizeTags } from '../common/tags';
|
import { normalizeTags } from '../common/tags';
|
||||||
import { buildSearchWhere, ci } from '../common/search';
|
import { buildSearchWhere, ci } from '../common/search';
|
||||||
import { listPriorLens, personPriorWhere } from '../common/prior-archive';
|
|
||||||
import {
|
import {
|
||||||
normalizePersonImportSource,
|
normalizePersonImportSource,
|
||||||
personImportSourceLabel,
|
personImportSourceLabel,
|
||||||
@@ -138,7 +137,6 @@ export class PeopleService implements OnModuleInit {
|
|||||||
...ownerWhere(actor),
|
...ownerWhere(actor),
|
||||||
...(opts?.mood ? { mood: opts.mood } : {}),
|
...(opts?.mood ? { mood: opts.mood } : {}),
|
||||||
...(sourceFilter ? { importSource: sourceFilter } : {}),
|
...(sourceFilter ? { importSource: sourceFilter } : {}),
|
||||||
...personPriorWhere(listPriorLens(actor.priorLens, q, Boolean(sourceFilter))),
|
|
||||||
...(search ? { AND: [search] } : {}),
|
...(search ? { AND: [search] } : {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ export class DealsExcelService {
|
|||||||
where: {
|
where: {
|
||||||
organizationId: actor.organizationId,
|
organizationId: actor.organizationId,
|
||||||
...dealAccessWhere(actor),
|
...dealAccessWhere(actor),
|
||||||
...dealPriorWhere(actor.priorLens),
|
...dealPriorWhere(actor.priorLens, actor.priorCutoff),
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
person: {
|
person: {
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ type DealRow = {
|
|||||||
expectedCloseAt: Date | null;
|
expectedCloseAt: Date | null;
|
||||||
code: string | null;
|
code: string | null;
|
||||||
importSource: string;
|
importSource: string;
|
||||||
|
promotedFromPriorAt: Date | null;
|
||||||
sourceRef: string | null;
|
sourceRef: string | null;
|
||||||
probability: number | null;
|
probability: number | null;
|
||||||
sourceDetail: string | null;
|
sourceDetail: string | null;
|
||||||
@@ -295,7 +296,12 @@ export class PipelinesService {
|
|||||||
pipelineId?: string,
|
pipelineId?: string,
|
||||||
actor?: Pick<
|
actor?: Pick<
|
||||||
Actor,
|
Actor,
|
||||||
'userId' | 'role' | 'dataVisibility' | 'visibleUserIds' | 'priorLens'
|
| 'userId'
|
||||||
|
| 'role'
|
||||||
|
| 'dataVisibility'
|
||||||
|
| 'visibleUserIds'
|
||||||
|
| 'priorLens'
|
||||||
|
| 'priorCutoff'
|
||||||
>,
|
>,
|
||||||
includePrior = false,
|
includePrior = false,
|
||||||
): Promise<PipelineBoardDto> {
|
): Promise<PipelineBoardDto> {
|
||||||
@@ -323,7 +329,7 @@ export class PipelinesService {
|
|||||||
organizationId,
|
organizationId,
|
||||||
pipelineId: resolved.id,
|
pipelineId: resolved.id,
|
||||||
...(actor ? dealAccessWhere(actor) : {}),
|
...(actor ? dealAccessWhere(actor) : {}),
|
||||||
...(includePrior ? {} : dealPriorWhere(actor?.priorLens)),
|
...(includePrior ? {} : dealPriorWhere(actor?.priorLens, actor?.priorCutoff)),
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
person: { select: { id: true, firstName: true, lastName: true } },
|
person: { select: { id: true, firstName: true, lastName: true } },
|
||||||
@@ -575,6 +581,7 @@ export class PipelinesService {
|
|||||||
expectedCloseAt: row.expectedCloseAt?.toISOString() ?? null,
|
expectedCloseAt: row.expectedCloseAt?.toISOString() ?? null,
|
||||||
code: row.code,
|
code: row.code,
|
||||||
importSource: row.importSource,
|
importSource: row.importSource,
|
||||||
|
promotedFromPriorAt: row.promotedFromPriorAt?.toISOString() ?? null,
|
||||||
sourceRef: row.sourceRef,
|
sourceRef: row.sourceRef,
|
||||||
probability: row.probability,
|
probability: row.probability,
|
||||||
sourceDetail: row.sourceDetail,
|
sourceDetail: row.sourceDetail,
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ import { decimalToBase, loadOrgRates, toBaseAmount } from '../common/currency';
|
|||||||
import {
|
import {
|
||||||
activityPriorWhere,
|
activityPriorWhere,
|
||||||
dealPriorWhere,
|
dealPriorWhere,
|
||||||
personPriorWhere,
|
|
||||||
} from '../common/prior-archive';
|
} from '../common/prior-archive';
|
||||||
import {
|
import {
|
||||||
addJalaliMonths,
|
addJalaliMonths,
|
||||||
@@ -62,9 +61,8 @@ export class ReportsService {
|
|||||||
const prevStart = new Date(prevEnd.getTime() - durationMs);
|
const prevStart = new Date(prevEnd.getTime() - durationMs);
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const peoplePrior = personPriorWhere(actor.priorLens);
|
const dealsPrior = dealPriorWhere(actor.priorLens, actor.priorCutoff);
|
||||||
const dealsPrior = dealPriorWhere(actor.priorLens, now);
|
const activitiesPrior = activityPriorWhere(actor.priorLens, actor.priorCutoff);
|
||||||
const activitiesPrior = activityPriorWhere(actor.priorLens, now, actor.priorCutoff);
|
|
||||||
const { jy, jm } = dateToJalali(now);
|
const { jy, jm } = dateToJalali(now);
|
||||||
const sixAgo = addJalaliMonths(jy, jm, -5);
|
const sixAgo = addJalaliMonths(jy, jm, -5);
|
||||||
const sixMonthStart = startOfJalaliMonth(sixAgo.jy, sixAgo.jm);
|
const sixMonthStart = startOfJalaliMonth(sixAgo.jy, sixAgo.jm);
|
||||||
@@ -160,7 +158,6 @@ export class ReportsService {
|
|||||||
where: {
|
where: {
|
||||||
organizationId: orgId,
|
organizationId: orgId,
|
||||||
...personScope,
|
...personScope,
|
||||||
...peoplePrior,
|
|
||||||
},
|
},
|
||||||
select: { mood: true, createdAt: true },
|
select: { mood: true, createdAt: true },
|
||||||
}),
|
}),
|
||||||
@@ -1551,9 +1548,8 @@ export class ReportsService {
|
|||||||
const yesterdayEnd = endOfDay(yesterday);
|
const yesterdayEnd = endOfDay(yesterday);
|
||||||
const weekAgo = new Date(now);
|
const weekAgo = new Date(now);
|
||||||
weekAgo.setDate(weekAgo.getDate() - 7);
|
weekAgo.setDate(weekAgo.getDate() - 7);
|
||||||
const peoplePrior = personPriorWhere(actor.priorLens);
|
const dealsPrior = dealPriorWhere(actor.priorLens, actor.priorCutoff);
|
||||||
const dealsPrior = dealPriorWhere(actor.priorLens, now);
|
const activitiesPrior = activityPriorWhere(actor.priorLens, actor.priorCutoff);
|
||||||
const activitiesPrior = activityPriorWhere(actor.priorLens, now, actor.priorCutoff);
|
|
||||||
|
|
||||||
const [
|
const [
|
||||||
wonDeals,
|
wonDeals,
|
||||||
@@ -1618,7 +1614,6 @@ export class ReportsService {
|
|||||||
where: {
|
where: {
|
||||||
organizationId: orgId,
|
organizationId: orgId,
|
||||||
...personScope,
|
...personScope,
|
||||||
...peoplePrior,
|
|
||||||
createdAt: { gte: curMonthStart, lte: curMonthEnd },
|
createdAt: { gte: curMonthStart, lte: curMonthEnd },
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@@ -1626,7 +1621,6 @@ export class ReportsService {
|
|||||||
where: {
|
where: {
|
||||||
organizationId: orgId,
|
organizationId: orgId,
|
||||||
...personScope,
|
...personScope,
|
||||||
...peoplePrior,
|
|
||||||
createdAt: { gte: prevMonthStart, lte: prevMonthEnd },
|
createdAt: { gte: prevMonthStart, lte: prevMonthEnd },
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|||||||
Reference in New Issue
Block a user