diff --git a/apps/api/src/common/prior-archive.ts b/apps/api/src/common/prior-archive.ts index e8ee8bf..830c57d 100644 --- a/apps/api/src/common/prior-archive.ts +++ b/apps/api/src/common/prior-archive.ts @@ -1,117 +1,64 @@ import type { Prisma } from '@prisma/client'; import type { Actor } from './record-scope'; -export const PRIOR_IMPORT_SOURCE = 'didar'; - export type PriorLens = 'off' | 'current' | 'archive'; export function actorPriorLens(actor: Pick | undefined): PriorLens { return actor?.priorLens ?? 'off'; } -function liveDeal(now: Date): Prisma.DealWhereInput { - return { - importSource: PRIOR_IMPORT_SOURCE, - status: 'open', - expectedCloseAt: { gt: now }, - }; +function hasCutoff(cutoff?: Date | null): cutoff is Date { + return cutoff instanceof Date && !Number.isNaN(cutoff.getTime()); } -function liveActivity(now: Date): Prisma.ActivityWhereInput { - return { - importSource: PRIOR_IMPORT_SOURCE, - status: 'open', - dueAt: { gt: now }, - }; -} - -/** فعالیت باز با موعد آینده، حتی اگر قبلی باشد، در کار جاری می‌ماند. */ -function stillOpenAhead(now: Date): Prisma.ActivityWhereInput { - return { status: 'open', dueAt: { gt: now } }; -} - -/** - * فعالیت قبلی است اگر منبعش didar باشد، - * یا قبل از روشن شدن ماژول ساخته شده و به شخص/شرکت/معاملهٔ قبلی وصل باشد. - * معاملهٔ غیرقبلی مانع نمی‌شود؛ شخص قبلی (حتی اگر بعداً منبعش عوض شده) کافی است. - * فعالیت بدون شخص و شرکت و معامله هم اگر موعدش قبل از همان لحظه باشد قبلی است. - */ -function historicalActivity(cutoff: Date): Prisma.ActivityWhereInput { - return { - OR: [ - { importSource: PRIOR_IMPORT_SOURCE }, - { - createdAt: { lt: cutoff }, - OR: [ - { - person: { - OR: [ - { importSource: PRIOR_IMPORT_SOURCE }, - { previousImportSource: PRIOR_IMPORT_SOURCE }, - ], - }, - }, - { company: { importSource: PRIOR_IMPORT_SOURCE } }, - { deal: { importSource: PRIOR_IMPORT_SOURCE } }, - { - personId: null, - companyId: null, - dealId: null, - dueAt: { lt: cutoff }, - }, - ], - }, - ], - }; -} - -export function personPriorWhere(lens: PriorLens | undefined): Prisma.PersonWhereInput { - if (lens === 'archive') return { importSource: PRIOR_IMPORT_SOURCE }; - if (lens === 'current') return { importSource: { not: PRIOR_IMPORT_SOURCE } }; +/** شخص/شرکت دیگر با این لنز فیلتر نمی‌شوند. */ +export function personPriorWhere(_lens?: PriorLens): Prisma.PersonWhereInput { return {}; } -export function companyPriorWhere(lens: PriorLens | undefined): Prisma.CompanyWhereInput { - if (lens === 'archive') return { importSource: PRIOR_IMPORT_SOURCE }; - if (lens === 'current') return { importSource: { not: PRIOR_IMPORT_SOURCE } }; +export function companyPriorWhere(_lens?: PriorLens): Prisma.CompanyWhereInput { return {}; } -export function dealPriorWhere(lens: PriorLens | undefined, now = new Date()): Prisma.DealWhereInput { +export function dealPriorWhere( + lens: PriorLens | undefined, + cutoff?: Date | null, +): Prisma.DealWhereInput { + if (lens !== 'current' && lens !== 'archive') return {}; + if (!hasCutoff(cutoff)) return {}; if (lens === 'current') { - return { OR: [{ importSource: { not: PRIOR_IMPORT_SOURCE } }, liveDeal(now)] }; + return { + OR: [ + { createdAt: { gte: cutoff } }, + { promotedFromPriorAt: { not: null } }, + ], + }; } - if (lens === 'archive') { - return { importSource: PRIOR_IMPORT_SOURCE, NOT: liveDeal(now) }; - } - return {}; + return { createdAt: { lt: cutoff }, promotedFromPriorAt: null }; } export function activityPriorWhere( lens: PriorLens | undefined, - now = new Date(), cutoff?: Date | null, ): Prisma.ActivityWhereInput { if (lens !== 'current' && lens !== 'archive') return {}; - if (!cutoff) { - if (lens === 'current') { - return { OR: [{ importSource: { not: PRIOR_IMPORT_SOURCE } }, liveActivity(now)] }; - } - return { importSource: PRIOR_IMPORT_SOURCE, NOT: liveActivity(now) }; + if (!hasCutoff(cutoff)) return {}; + if (lens === 'current') { + return { + OR: [ + { createdAt: { gte: cutoff } }, + { promotedFromPriorAt: { not: null } }, + ], + }; } - const prior = historicalActivity(cutoff); - const live = stillOpenAhead(now); - if (lens === 'current') return { OR: [{ NOT: prior }, live] }; - return { AND: [prior, { NOT: live }] }; + return { createdAt: { lt: cutoff }, promotedFromPriorAt: null }; } -/** فهرست شخص/شرکت: در کار جاری، جستجو و فیلتر صریح منبع، قبلی‌ها را هم پیدا می‌کند. */ +/** فهرست شخص/شرکت: همیشه بدون فیلتر لنز (سازگاری با call-siteهای قدیمی). */ export function listPriorLens( - lens: PriorLens | undefined, - q: string | undefined, - explicitSource: boolean, + _lens: PriorLens | undefined, + _q: string | undefined, + _explicitSource: boolean, ): PriorLens { - if (explicitSource) return 'off'; - if (lens === 'current' && q?.trim()) return 'off'; - return lens ?? 'off'; + return 'off'; } diff --git a/apps/api/test/prior-archive.test.ts b/apps/api/test/prior-archive.test.ts index fcc3b29..eb808cd 100644 --- a/apps/api/test/prior-archive.test.ts +++ b/apps/api/test/prior-archive.test.ts @@ -3,85 +3,38 @@ import assert from 'node:assert/strict'; import { activityPriorWhere, dealPriorWhere, - listPriorLens, personPriorWhere, } from '../src/common/prior-archive'; -const now = new Date('2026-10-15T08:00:00.000Z'); +const cutoff = new Date('2026-10-01T00:00:00.000Z'); -describe('بایگانی اطلاعات قبل', () => { - it('بدون مجوز فیلتر نمی‌گذارد', () => { - assert.deepEqual(personPriorWhere('off'), {}); - assert.deepEqual(dealPriorWhere(undefined, now), {}); +describe('بایگانی اطلاعات قبل — تاریخ قطع', () => { + it('بدون لنز یا بدون تاریخ فیلتر نمی‌گذارد', () => { + assert.deepEqual(dealPriorWhere('off', cutoff), {}); + assert.deepEqual(dealPriorWhere('current', null), {}); + assert.deepEqual(activityPriorWhere('archive', undefined), {}); + assert.deepEqual(personPriorWhere('current'), {}); }); - it('کار جاری شخص قبلی را کنار می‌گذارد و معاملهٔ بازِ موعددار را نگه می‌دارد', () => { - assert.deepEqual(personPriorWhere('current'), { importSource: { not: 'didar' } }); - const where = dealPriorWhere('current', now); - assert.equal(where.OR?.length, 2); - assert.deepEqual(where.OR?.[1], { - importSource: 'didar', - status: 'open', - expectedCloseAt: { gt: now }, - }); - }); - - it('بایگانی فقط قبلی‌ها را می‌آورد و استثنای باز را برمی‌دارد', () => { - const where = activityPriorWhere('archive', now); - assert.equal(where.importSource, 'didar'); - assert.deepEqual(where.NOT, { - importSource: 'didar', - status: 'open', - dueAt: { gt: now }, - }); - }); - - it('فعالیت قدیمی روی شخص قبلی از کار جاری کنار می‌رود', () => { - const cutoff = new Date('2026-10-01T00:00:00.000Z'); - const current = JSON.parse(JSON.stringify(activityPriorWhere('current', now, cutoff))) as { + it('کار جاری: بعد از قطع یا منتقل‌شده', () => { + assert.deepEqual(dealPriorWhere('current', cutoff), { OR: [ - { - NOT: { - OR: [ - { importSource: string }, - { - createdAt: { lt: string }; - OR: Array<{ person?: { OR: Array> } }>; - }, - ]; - }; - }, - { status: string; dueAt: { gt: string } }, - ]; - }; - const linked = current.OR[0].NOT.OR[1]; - assert.equal(linked.createdAt.lt, cutoff.toISOString()); - assert.deepEqual(linked.OR[0], { - person: { - OR: [{ importSource: 'didar' }, { previousImportSource: 'didar' }], - }, - }); - assert.deepEqual(linked.OR[3], { - personId: null, - companyId: null, - dealId: null, - dueAt: { lt: cutoff.toISOString() }, - }); - assert.deepEqual(current.OR[1], { status: 'open', dueAt: { gt: now.toISOString() } }); - - const archive = JSON.parse(JSON.stringify(activityPriorWhere('archive', now, cutoff))) as { - AND: [unknown, { NOT: { status: string; dueAt: { gt: string } } }]; - }; - assert.deepEqual(archive.AND[0], current.OR[0].NOT); - assert.deepEqual(archive.AND[1], { - NOT: { status: 'open', dueAt: { gt: now.toISOString() } }, + { createdAt: { gte: cutoff } }, + { promotedFromPriorAt: { not: null } }, + ], }); }); - it('جستجوی کار جاری شخص قبلی را پیدا می‌کند', () => { - assert.equal(listPriorLens('current', 'علی', false), 'off'); - assert.equal(listPriorLens('current', ' ', false), 'current'); - assert.equal(listPriorLens('archive', 'علی', false), 'archive'); - assert.equal(listPriorLens('current', '', true), 'off'); + it('بایگانی: قبل از قطع و بدون برچسب انتقال', () => { + assert.deepEqual(activityPriorWhere('archive', cutoff), { + createdAt: { lt: cutoff }, + promotedFromPriorAt: null, + }); + }); + + it('استثنای موعد آینده دیگر نیست', () => { + const w = JSON.stringify(dealPriorWhere('current', cutoff)); + assert.equal(w.includes('expectedCloseAt'), false); + assert.equal(w.includes('didar'), false); }); });