-
Notifications
You must be signed in to change notification settings - Fork 314
[dev] [Marfuen] mariano/cs-312-feature-add-personnel-employment-events-date-tracking #2880
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
Changes from all commits
5ea8d9f
25429c6
cbb2e3d
cf836bf
141af5c
8d9d8c1
7a4deec
a6bc6fa
2d22d4f
4b031b5
e793be0
a977727
6d63f25
28e20bb
0256153
4fcbaba
3729fb1
47d814c
4856b57
9d54433
7979bd6
1867f52
6c22188
0f87f73
d5f5d81
8a06bf4
f99a1fb
a9f4ed2
5eedc39
7c33f6c
c40eba7
0e4bdfb
7c103f4
97fd85b
518c165
1702da4
c0ad577
d7fa4c9
4b09a53
6083ccf
112d897
4dd6591
f6e3a8a
8dcf351
5107b74
9959645
b5675b6
67617a0
fb2fa90
ef687f9
e089738
10efbef
89efbb9
7df1ba6
5c08e4b
1b0f074
bc9b79a
ecc8913
450e0d5
5916733
63c2970
2ac0b86
da48aba
72c66c9
91b8619
88a9f71
45b50e5
359a1fb
9f49aae
28f7f41
72710f2
654a134
5e0ba5d
a14e816
4325208
4f61715
1071714
6a2d0e4
1b8b971
c0c7c52
6b180c4
0974d39
15bdf1e
d030870
c8ec116
ef5a848
468b2e6
e15e8c1
cd52706
873b0a6
e577962
dcc15b9
d8a9025
4048333
a1d26ff
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,233 @@ | ||
| import { | ||
| BadRequestException, | ||
| Injectable, | ||
| NotFoundException, | ||
| } from '@nestjs/common'; | ||
| import { AttachmentEntityType, db } from '@db'; | ||
| import { AttachmentsService } from '../attachments/attachments.service'; | ||
|
|
||
| @Injectable() | ||
| export class AccessRevocationService { | ||
| constructor(private readonly attachmentsService: AttachmentsService) {} | ||
| async getAccessRevocations(organizationId: string, memberId: string) { | ||
| const vendors = await db.vendor.findMany({ | ||
| where: { organizationId }, | ||
| select: { id: true, name: true, website: true, logoUrl: true }, | ||
| orderBy: { name: 'asc' }, | ||
| }); | ||
|
|
||
| const revocations = await db.offboardingAccessRevocation.findMany({ | ||
| where: { organizationId, memberId }, | ||
| include: { | ||
| revokedBy: { select: { id: true, name: true, email: true } }, | ||
| }, | ||
| }); | ||
|
|
||
| const revocationMap = new Map( | ||
| revocations.map((r) => [r.vendorId, r]), | ||
| ); | ||
|
|
||
| const vendorList = await Promise.all( | ||
| vendors.map(async (vendor) => { | ||
| const revocation = revocationMap.get(vendor.id); | ||
| const domain = vendor.website?.replace(/^https?:\/\//, '').replace(/\/.*$/, '') ?? null; | ||
| const evidence = revocation | ||
| ? await this.attachmentsService.getAttachments( | ||
| organizationId, | ||
| revocation.id, | ||
| AttachmentEntityType.offboarding_checklist, | ||
| ) | ||
| : []; | ||
| return { | ||
| vendorId: vendor.id, | ||
| vendorName: vendor.name, | ||
| logoUrl: vendor.logoUrl ?? (domain ? `https://img.logo.dev/${domain}?token=pk_X-1ZO13GSgeOoUrIuJ6GMQ&size=64` : null), | ||
| revoked: !!revocation, | ||
| revokedAt: revocation?.revokedAt ?? null, | ||
| revokedBy: revocation?.revokedBy ?? null, | ||
| notes: revocation?.notes ?? null, | ||
| evidence, | ||
| }; | ||
| }), | ||
| ); | ||
|
|
||
| return { | ||
| vendors: vendorList, | ||
| totalVendors: vendors.length, | ||
| revokedCount: revocations.length, | ||
| }; | ||
| } | ||
|
|
||
| async revokeVendorAccess({ | ||
| organizationId, | ||
| memberId, | ||
| vendorId, | ||
| revokedById, | ||
| notes, | ||
| evidence, | ||
| }: { | ||
| organizationId: string; | ||
| memberId: string; | ||
| vendorId: string; | ||
| revokedById: string; | ||
| notes?: string; | ||
| evidence?: { fileName: string; fileType: string; fileData: string }; | ||
| }) { | ||
| const vendor = await db.vendor.findFirst({ | ||
| where: { id: vendorId, organizationId }, | ||
| }); | ||
|
|
||
| if (!vendor) { | ||
| throw new NotFoundException('Vendor not found in this organization'); | ||
| } | ||
|
|
||
| const existing = await db.offboardingAccessRevocation.findUnique({ | ||
| where: { memberId_vendorId: { memberId, vendorId } }, | ||
| }); | ||
|
|
||
| if (existing) { | ||
| throw new BadRequestException( | ||
| 'Vendor access has already been revoked for this member', | ||
| ); | ||
| } | ||
|
|
||
| const revocation = await db.offboardingAccessRevocation.create({ | ||
| data: { | ||
| organizationId, | ||
| memberId, | ||
| vendorId, | ||
| revokedById, | ||
| notes, | ||
| }, | ||
| include: { | ||
| revokedBy: { select: { id: true, name: true, email: true } }, | ||
| }, | ||
| }); | ||
|
|
||
| if (evidence) { | ||
| await this.attachmentsService.uploadAttachment( | ||
| organizationId, | ||
| revocation.id, | ||
| AttachmentEntityType.offboarding_checklist, | ||
| evidence, | ||
| revokedById, | ||
| ); | ||
| } | ||
|
|
||
| await this.syncAccessRevocationCompletion( | ||
| organizationId, | ||
| memberId, | ||
| revokedById, | ||
| ); | ||
|
|
||
| return revocation; | ||
| } | ||
|
|
||
| async undoVendorRevocation({ | ||
| organizationId, | ||
| memberId, | ||
| vendorId, | ||
| }: { | ||
| organizationId: string; | ||
| memberId: string; | ||
| vendorId: string; | ||
| }) { | ||
| const revocation = await db.offboardingAccessRevocation.findUnique({ | ||
| where: { memberId_vendorId: { memberId, vendorId } }, | ||
| }); | ||
|
|
||
| if (!revocation) { | ||
| throw new NotFoundException('Revocation record not found'); | ||
| } | ||
|
|
||
| await db.offboardingAccessRevocation.delete({ | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Undoing a revocation should delete its uploaded evidence to avoid orphaned attachment records/files. Prompt for AI agents |
||
| where: { id: revocation.id }, | ||
| }); | ||
|
|
||
| await this.syncAccessRevocationCompletion(organizationId, memberId); | ||
|
|
||
| return { success: true }; | ||
| } | ||
|
|
||
| async revokeAllVendorAccess({ | ||
| organizationId, | ||
| memberId, | ||
| revokedById, | ||
| }: { | ||
| organizationId: string; | ||
| memberId: string; | ||
| revokedById: string; | ||
| }) { | ||
| const vendors = await db.vendor.findMany({ | ||
| where: { organizationId }, | ||
| select: { id: true }, | ||
| }); | ||
|
|
||
| const existing = await db.offboardingAccessRevocation.findMany({ | ||
| where: { organizationId, memberId }, | ||
| select: { vendorId: true }, | ||
| }); | ||
|
|
||
| const existingSet = new Set(existing.map((r) => r.vendorId)); | ||
| const toCreate = vendors.filter((v) => !existingSet.has(v.id)); | ||
|
|
||
| if (toCreate.length > 0) { | ||
| await db.offboardingAccessRevocation.createMany({ | ||
| data: toCreate.map((v) => ({ | ||
| organizationId, | ||
| memberId, | ||
| vendorId: v.id, | ||
| revokedById, | ||
| })), | ||
| skipDuplicates: true, | ||
| }); | ||
| } | ||
|
|
||
| await this.syncAccessRevocationCompletion(organizationId, memberId, revokedById); | ||
|
|
||
| return { confirmed: toCreate.length }; | ||
| } | ||
|
|
||
| private async syncAccessRevocationCompletion( | ||
| organizationId: string, | ||
| memberId: string, | ||
| completedById?: string, | ||
| ) { | ||
| const templateItem = await db.offboardingChecklistTemplate.findFirst({ | ||
| where: { organizationId, isAccessRevocation: true, isEnabled: true }, | ||
| }); | ||
|
|
||
| if (!templateItem) { | ||
| return; | ||
| } | ||
|
|
||
| const { totalVendors, revokedCount } = await this.getAccessRevocations( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Use direct count queries in completion sync instead of calling Prompt for AI agents |
||
| organizationId, | ||
| memberId, | ||
| ); | ||
|
|
||
| const allRevoked = totalVendors > 0 && revokedCount === totalVendors; | ||
|
|
||
| const existingCompletion = | ||
| await db.offboardingChecklistCompletion.findFirst({ | ||
| where: { organizationId, memberId, templateItemId: templateItem.id }, | ||
| }); | ||
|
|
||
| if (allRevoked && !existingCompletion && completedById) { | ||
| await db.offboardingChecklistCompletion.create({ | ||
| data: { | ||
| organizationId, | ||
| memberId, | ||
| templateItemId: templateItem.id, | ||
| completedById, | ||
| }, | ||
| }); | ||
| } | ||
|
|
||
| if (!allRevoked && existingCompletion) { | ||
| await db.offboardingChecklistCompletion.delete({ | ||
| where: { id: existingCompletion.id }, | ||
| }); | ||
| } | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.
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.
P2: Validate
employee.startDatebefore converting it toDate; malformed provider values can make sync writes fail.Prompt for AI agents