550 lines
16 KiB
TypeScript
550 lines
16 KiB
TypeScript
import { Prisma, type IntakeNonIntegratedProduct, type IntakeNonIntegratedProductProfile } from '@prisma/client'
|
|
import { AppError, ErrorCode } from '@/server/core/errors'
|
|
import type { AuthUser } from '@/server/core/types'
|
|
import {
|
|
canUploadNonIntegratedDetachedAsset,
|
|
canCreateNonIntegratedProductFromIntake,
|
|
canDeleteNonIntegratedProductByIntakePermission,
|
|
canEditAnyNonIntegratedWorkspace,
|
|
canEditNonIntegratedWorkspace,
|
|
canDownloadNonIntegratedWorkspace,
|
|
canPushNonIntegratedProductPricingToJushuitan,
|
|
isIntakeNonIntegratedFullEditor,
|
|
type IntakeNonIntegratedPermissionProduct,
|
|
} from '@/lib/non-integrated-permissions'
|
|
import {
|
|
isExternalIntakeRecordOwner,
|
|
isExternalIntakeUser,
|
|
} from '@/lib/non-integrated-external-entry-permissions'
|
|
import {
|
|
getIntakeNodePermissionTargetKeyByWorkspaceCategory,
|
|
resolveIntakeNodePermissionAccess,
|
|
resolveIntakeNodePermissionSubjectKeys,
|
|
} from '@/lib/intake-node-permission-settings'
|
|
import { intakeNodePermissionSettingsService } from './intake-node-permission-settings.service'
|
|
|
|
type IntakeNonIntegratedPermissionTarget = Pick<
|
|
IntakeNonIntegratedProduct,
|
|
| 'id'
|
|
| 'createdBy'
|
|
| 'designer'
|
|
| 'purchaser'
|
|
| 'visibility'
|
|
| 'departmentId'
|
|
| 'teamId'
|
|
> & {
|
|
operator?: string | null
|
|
payload?: Prisma.JsonValue | null
|
|
profile?: IntakeNonIntegratedProductProfile | null
|
|
skuItems?: Array<{
|
|
skuItemId?: string | null
|
|
purchaseOwner?: string | null
|
|
manufacturerCode?: string | null
|
|
}>
|
|
}
|
|
|
|
const ADMIN_ROLES = new Set<AuthUser['role']>(['SUPER_ADMIN', 'ADMIN'])
|
|
const PRODUCT_FACTORY_WORKSPACE_CATEGORY = '\u4ea7\u54c1\u5b9a\u5382'
|
|
export const DEFAULT_NON_INTEGRATED_VISIBILITY = 'DEPARTMENT'
|
|
export const NON_INTEGRATED_SCOPE_ENABLED = false
|
|
|
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
}
|
|
|
|
function readText(value: unknown) {
|
|
return typeof value === 'string' ? value.trim() : ''
|
|
}
|
|
|
|
function readPayloadField(payload: unknown, key: string) {
|
|
return isRecord(payload) ? readText(payload[key]) : ''
|
|
}
|
|
|
|
function readOpeningReferenceField(payload: unknown, key: string) {
|
|
if (!isRecord(payload)) return ''
|
|
const openingReference = payload.openingReference
|
|
return isRecord(openingReference) ? readText(openingReference[key]) : ''
|
|
}
|
|
|
|
function normalizeSupplierCode(value: unknown) {
|
|
return readText(value).toUpperCase()
|
|
}
|
|
|
|
function buildSupplierRecordScopeWhere(
|
|
supplierCode: string,
|
|
): Prisma.IntakeNonIntegratedProductWhereInput {
|
|
return {
|
|
OR: [
|
|
{
|
|
skuItems: {
|
|
some: {
|
|
manufacturerCode: supplierCode,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
payload: {
|
|
path: ['factoryRows'],
|
|
array_contains: [{ supplierCode }],
|
|
},
|
|
},
|
|
],
|
|
}
|
|
}
|
|
|
|
function readPayloadFactoryRows(payload: unknown) {
|
|
if (!isRecord(payload)) return []
|
|
const rows = payload.factoryRows
|
|
return Array.isArray(rows)
|
|
? rows.filter((row): row is Record<string, unknown> => isRecord(row))
|
|
: []
|
|
}
|
|
|
|
class IntakeNonIntegratedScopeService {
|
|
isScopeEnabled(): boolean {
|
|
return NON_INTEGRATED_SCOPE_ENABLED
|
|
}
|
|
|
|
isAdmin(user: AuthUser): boolean {
|
|
return ADMIN_ROLES.has(user.role)
|
|
}
|
|
|
|
isExternalIntakeScopeUser(user: AuthUser): boolean {
|
|
return !this.isAdmin(user) && isExternalIntakeUser(user)
|
|
}
|
|
|
|
isSupplierScopeUser(user: AuthUser): boolean {
|
|
return !this.isAdmin(user) && Boolean(user.businessRoles?.includes('SUPPLIER'))
|
|
}
|
|
|
|
getSupplierCode(user: AuthUser): string {
|
|
return normalizeSupplierCode(user.supplierCode)
|
|
}
|
|
|
|
buildSupplierRecordScopeWhere(user: AuthUser): Prisma.IntakeNonIntegratedProductWhereInput {
|
|
const supplierCode = this.getSupplierCode(user)
|
|
if (!supplierCode) {
|
|
return { id: '__unbound_supplier__' }
|
|
}
|
|
|
|
return buildSupplierRecordScopeWhere(supplierCode)
|
|
}
|
|
|
|
getScopedDepartmentId(user: AuthUser): string | undefined {
|
|
return user.managedDepartmentId ?? user.departmentId
|
|
}
|
|
|
|
getScopedTeamId(user: AuthUser): string | undefined {
|
|
return user.managedTeamId ?? user.teamId
|
|
}
|
|
|
|
buildReadScopeWhere(user: AuthUser): Prisma.IntakeNonIntegratedProductWhereInput | undefined {
|
|
if (this.isSupplierScopeUser(user)) {
|
|
return this.buildSupplierRecordScopeWhere(user)
|
|
}
|
|
|
|
if (this.isExternalIntakeScopeUser(user)) {
|
|
return { createdBy: user.id }
|
|
}
|
|
|
|
if (!this.isScopeEnabled()) {
|
|
return undefined
|
|
}
|
|
|
|
if (this.isAdmin(user)) {
|
|
return undefined
|
|
}
|
|
|
|
const orConditions: Prisma.IntakeNonIntegratedProductWhereInput[] = [
|
|
{ createdBy: user.id },
|
|
{ visibility: 'PUBLIC' },
|
|
]
|
|
|
|
const scopedDepartmentId = this.getScopedDepartmentId(user)
|
|
if (scopedDepartmentId) {
|
|
orConditions.push({
|
|
visibility: 'DEPARTMENT',
|
|
departmentId: scopedDepartmentId,
|
|
})
|
|
}
|
|
|
|
const scopedTeamId = this.getScopedTeamId(user)
|
|
if (scopedTeamId) {
|
|
orConditions.push({
|
|
visibility: 'TEAM',
|
|
teamId: scopedTeamId,
|
|
})
|
|
}
|
|
|
|
return {
|
|
OR: orConditions,
|
|
}
|
|
}
|
|
|
|
canReadRecord(record: IntakeNonIntegratedPermissionTarget, user: AuthUser): boolean {
|
|
if (this.isSupplierScopeUser(user)) {
|
|
const supplierCode = this.getSupplierCode(user)
|
|
if (!supplierCode) return false
|
|
|
|
if (
|
|
record.skuItems?.some(
|
|
(item) => normalizeSupplierCode(item.manufacturerCode) === supplierCode,
|
|
)
|
|
) {
|
|
return true
|
|
}
|
|
|
|
return readPayloadFactoryRows(record.payload).some(
|
|
(row) => normalizeSupplierCode(row.supplierCode) === supplierCode,
|
|
)
|
|
}
|
|
|
|
if (this.isExternalIntakeScopeUser(user)) {
|
|
return isExternalIntakeRecordOwner(record, user)
|
|
}
|
|
|
|
if (!this.isScopeEnabled()) return true
|
|
if (this.isAdmin(user)) return true
|
|
if (record.createdBy === user.id) return true
|
|
if (record.visibility === 'PUBLIC') return true
|
|
|
|
const scopedDepartmentId = this.getScopedDepartmentId(user)
|
|
if (
|
|
record.visibility === 'DEPARTMENT' &&
|
|
scopedDepartmentId &&
|
|
record.departmentId === scopedDepartmentId
|
|
) {
|
|
return true
|
|
}
|
|
|
|
const scopedTeamId = this.getScopedTeamId(user)
|
|
if (record.visibility === 'TEAM' && scopedTeamId && record.teamId === scopedTeamId) {
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
canCreateRecord(user: AuthUser): boolean {
|
|
return canCreateNonIntegratedProductFromIntake(user)
|
|
}
|
|
|
|
private buildPermissionProduct(
|
|
record: IntakeNonIntegratedPermissionTarget,
|
|
skuItemIds?: readonly string[],
|
|
): IntakeNonIntegratedPermissionProduct {
|
|
const profile = record.profile ?? null
|
|
const readProfile = (key: keyof IntakeNonIntegratedProductProfile) => {
|
|
const value = profile?.[key]
|
|
return readText(value)
|
|
}
|
|
const payloadDesigner = readPayloadField(record.payload, 'designer')
|
|
const openingDesigner = readOpeningReferenceField(record.payload, 'designer')
|
|
const persistedDesigner = readText(record.designer)
|
|
const productPlanning = readProfile('productPlanning')
|
|
const targetSkuItemIds = new Set(
|
|
(skuItemIds ?? []).map(readText).filter(Boolean),
|
|
)
|
|
const fallbackPurchaseOwner =
|
|
readProfile('purchaseOwner') || readText(record.purchaser)
|
|
const skuPurchaseOwners = (record.skuItems ?? [])
|
|
.filter(
|
|
(item) =>
|
|
targetSkuItemIds.size === 0 ||
|
|
targetSkuItemIds.has(readText(item.skuItemId)),
|
|
)
|
|
.map((item) => {
|
|
const skuPurchaseOwner = readText(item.purchaseOwner)
|
|
return skuPurchaseOwner ||
|
|
(targetSkuItemIds.size === 0 ? fallbackPurchaseOwner : '')
|
|
})
|
|
.filter(Boolean)
|
|
const nextOpeningReference = {
|
|
productName: readProfile('productName'),
|
|
productImage: readProfile('productImageUrl'),
|
|
productCategory: readProfile('productCategory'),
|
|
salesCategory: readProfile('salesCategory'),
|
|
currentStage: readProfile('productStatus'),
|
|
year: readProfile('productYear'),
|
|
designer: openingDesigner || payloadDesigner || persistedDesigner,
|
|
structureDesigner:
|
|
readProfile('structureDesigner') ||
|
|
readOpeningReferenceField(record.payload, 'structureDesigner'),
|
|
planningOwner: productPlanning,
|
|
}
|
|
|
|
return {
|
|
createdBy: record.createdBy,
|
|
designer: persistedDesigner || payloadDesigner || openingDesigner,
|
|
purchaser: readText(record.purchaser),
|
|
purchaseOwner: readProfile('purchaseOwner'),
|
|
brandPlanning: readProfile('brandPlanning'),
|
|
spuOwner: readProfile('spuOwner'),
|
|
operator: readText(record.operator),
|
|
operationsOwner: readProfile('operationsOwner'),
|
|
productPlanning,
|
|
visualPlanning: readProfile('visualPlanning'),
|
|
visualDirector: readProfile('visualDirector'),
|
|
skuPurchaseOwners,
|
|
openingReference: nextOpeningReference,
|
|
}
|
|
}
|
|
|
|
canWriteRecord(record: IntakeNonIntegratedPermissionTarget, user: AuthUser): boolean {
|
|
return canEditAnyNonIntegratedWorkspace(this.buildPermissionProduct(record), user)
|
|
}
|
|
|
|
canUploadDetachedAsset(record: IntakeNonIntegratedPermissionTarget, user: AuthUser): boolean {
|
|
return canUploadNonIntegratedDetachedAsset(this.buildPermissionProduct(record), user)
|
|
}
|
|
|
|
canPushProductPricing(record: IntakeNonIntegratedPermissionTarget, user: AuthUser): boolean {
|
|
return canPushNonIntegratedProductPricingToJushuitan(
|
|
this.buildPermissionProduct(record),
|
|
user,
|
|
)
|
|
}
|
|
|
|
canWriteWorkspaceRecord(
|
|
record: IntakeNonIntegratedPermissionTarget,
|
|
user: AuthUser,
|
|
category?: string | null,
|
|
targetSkuItemIds?: readonly string[],
|
|
): boolean {
|
|
if (!category) {
|
|
return this.canWriteRecord(record, user)
|
|
}
|
|
|
|
const normalizedTargetSkuItemIds = Array.from(
|
|
new Set((targetSkuItemIds ?? []).map(readText).filter(Boolean)),
|
|
)
|
|
if (
|
|
category === PRODUCT_FACTORY_WORKSPACE_CATEGORY &&
|
|
normalizedTargetSkuItemIds.length > 0
|
|
) {
|
|
const skuItemsById = new Map(
|
|
(record.skuItems ?? []).map((item) => [readText(item.skuItemId), item]),
|
|
)
|
|
if (
|
|
normalizedTargetSkuItemIds.some((skuItemId) => !skuItemsById.has(skuItemId))
|
|
) {
|
|
return false
|
|
}
|
|
|
|
return normalizedTargetSkuItemIds.every((skuItemId) =>
|
|
canEditNonIntegratedWorkspace(
|
|
this.buildPermissionProduct(record, [skuItemId]),
|
|
user,
|
|
category,
|
|
),
|
|
)
|
|
}
|
|
|
|
if (category === PRODUCT_FACTORY_WORKSPACE_CATEGORY) {
|
|
const fallbackPurchaseOwner =
|
|
readText(record.profile?.purchaseOwner) || readText(record.purchaser)
|
|
const skuPurchaseOwners = new Set(
|
|
(record.skuItems ?? [])
|
|
.map((item) => readText(item.purchaseOwner) || fallbackPurchaseOwner)
|
|
.filter(Boolean),
|
|
)
|
|
if (skuPurchaseOwners.size > 1) return false
|
|
}
|
|
|
|
return canEditNonIntegratedWorkspace(this.buildPermissionProduct(record), user, category)
|
|
}
|
|
|
|
canDownloadWorkspaceRecord(
|
|
record: IntakeNonIntegratedPermissionTarget,
|
|
user: AuthUser,
|
|
category?: string | null,
|
|
): boolean {
|
|
if (!category) {
|
|
return false
|
|
}
|
|
|
|
return canDownloadNonIntegratedWorkspace(
|
|
this.buildPermissionProduct(record),
|
|
user,
|
|
category,
|
|
)
|
|
}
|
|
|
|
async canWriteWorkspaceRecordWithSettings(
|
|
record: IntakeNonIntegratedPermissionTarget,
|
|
user: AuthUser,
|
|
category?: string | null,
|
|
targetSkuItemIds?: readonly string[],
|
|
): Promise<boolean> {
|
|
if (isIntakeNonIntegratedFullEditor(user)) {
|
|
return true
|
|
}
|
|
|
|
const targetKey = getIntakeNodePermissionTargetKeyByWorkspaceCategory(category)
|
|
if (!targetKey) {
|
|
return false
|
|
}
|
|
|
|
const { overrides } = await intakeNodePermissionSettingsService.getSettings()
|
|
const subjectKeys = resolveIntakeNodePermissionSubjectKeys(user)
|
|
const nodePermissionAccess = resolveIntakeNodePermissionAccess(
|
|
overrides,
|
|
targetKey,
|
|
subjectKeys,
|
|
)
|
|
|
|
if (!nodePermissionAccess.edit) {
|
|
return false
|
|
}
|
|
|
|
if (nodePermissionAccess.override.edit) {
|
|
return true
|
|
}
|
|
|
|
return this.canWriteWorkspaceRecord(record, user, category, targetSkuItemIds)
|
|
}
|
|
|
|
async canDownloadWorkspaceRecordWithSettings(
|
|
record: IntakeNonIntegratedPermissionTarget,
|
|
user: AuthUser,
|
|
category?: string | null,
|
|
): Promise<boolean> {
|
|
if (isIntakeNonIntegratedFullEditor(user)) {
|
|
return true
|
|
}
|
|
|
|
const targetKey = getIntakeNodePermissionTargetKeyByWorkspaceCategory(category)
|
|
if (!targetKey) {
|
|
return false
|
|
}
|
|
|
|
const { overrides } = await intakeNodePermissionSettingsService.getSettings()
|
|
const subjectKeys = resolveIntakeNodePermissionSubjectKeys(user)
|
|
const nodePermissionAccess = resolveIntakeNodePermissionAccess(
|
|
overrides,
|
|
targetKey,
|
|
subjectKeys,
|
|
)
|
|
|
|
if (!nodePermissionAccess.download) {
|
|
return false
|
|
}
|
|
|
|
if (nodePermissionAccess.override.download) {
|
|
return true
|
|
}
|
|
|
|
return this.canDownloadWorkspaceRecord(record, user, category)
|
|
}
|
|
|
|
async canPreviewWorkspaceRecordWithSettings(
|
|
record: IntakeNonIntegratedPermissionTarget,
|
|
user: AuthUser,
|
|
category?: string | null,
|
|
): Promise<boolean> {
|
|
if (isIntakeNonIntegratedFullEditor(user)) {
|
|
return true
|
|
}
|
|
|
|
const targetKey = getIntakeNodePermissionTargetKeyByWorkspaceCategory(category)
|
|
if (!targetKey) {
|
|
return false
|
|
}
|
|
|
|
const { overrides } = await intakeNodePermissionSettingsService.getSettings()
|
|
const subjectKeys = resolveIntakeNodePermissionSubjectKeys(user)
|
|
const nodePermissionAccess = resolveIntakeNodePermissionAccess(
|
|
overrides,
|
|
targetKey,
|
|
subjectKeys,
|
|
)
|
|
|
|
if (!nodePermissionAccess.view) {
|
|
return false
|
|
}
|
|
|
|
if (nodePermissionAccess.override.view) {
|
|
return true
|
|
}
|
|
|
|
return this.canReadRecord(record, user)
|
|
}
|
|
|
|
async assertCanWriteRecord(
|
|
record: IntakeNonIntegratedPermissionTarget,
|
|
user: AuthUser,
|
|
category?: string | null,
|
|
targetSkuItemIds?: readonly string[],
|
|
): Promise<void> {
|
|
if (
|
|
await this.canWriteWorkspaceRecordWithSettings(
|
|
record,
|
|
user,
|
|
category,
|
|
targetSkuItemIds,
|
|
)
|
|
) {
|
|
return
|
|
}
|
|
|
|
throw new AppError(ErrorCode.AUTH_PERMISSION_DENIED, '没有权限编辑这条非集成产品记录')
|
|
}
|
|
|
|
assertCanUploadDetachedAsset(
|
|
record: IntakeNonIntegratedPermissionTarget,
|
|
user: AuthUser,
|
|
): void {
|
|
if (this.canUploadDetachedAsset(record, user)) {
|
|
return
|
|
}
|
|
|
|
throw new AppError(ErrorCode.AUTH_PERMISSION_DENIED, '没有权限编辑这条非集成产品记录')
|
|
}
|
|
|
|
async assertCanDownloadWorkspaceRecord(
|
|
record: IntakeNonIntegratedPermissionTarget,
|
|
user: AuthUser,
|
|
category?: string | null,
|
|
): Promise<void> {
|
|
if (await this.canDownloadWorkspaceRecordWithSettings(record, user, category)) {
|
|
return
|
|
}
|
|
|
|
throw new AppError(ErrorCode.AUTH_PERMISSION_DENIED, '没有权限下载这条非集成产品资料')
|
|
}
|
|
|
|
async assertCanPreviewWorkspaceRecord(
|
|
record: IntakeNonIntegratedPermissionTarget,
|
|
user: AuthUser,
|
|
category?: string | null,
|
|
): Promise<void> {
|
|
if (await this.canPreviewWorkspaceRecordWithSettings(record, user, category)) {
|
|
return
|
|
}
|
|
|
|
throw new AppError(ErrorCode.AUTH_PERMISSION_DENIED, '没有权限查看这条非集成产品资料')
|
|
}
|
|
|
|
assertCanPushProductPricing(
|
|
record: IntakeNonIntegratedPermissionTarget,
|
|
user: AuthUser,
|
|
): void {
|
|
if (this.canPushProductPricing(record, user)) {
|
|
return
|
|
}
|
|
|
|
throw new AppError(ErrorCode.AUTH_PERMISSION_DENIED, '没有权限推送这条非集成产品核价')
|
|
}
|
|
|
|
assertCanDeleteRecord(record: IntakeNonIntegratedPermissionTarget, user: AuthUser): void {
|
|
if (canDeleteNonIntegratedProductByIntakePermission(user)) {
|
|
return
|
|
}
|
|
|
|
throw new AppError(ErrorCode.AUTH_PERMISSION_DENIED, '没有权限删除这条非集成产品记录')
|
|
}
|
|
}
|
|
|
|
export const intakeNonIntegratedScopeService = new IntakeNonIntegratedScopeService()
|
|
export { IntakeNonIntegratedScopeService }
|