690329:1621 Fixing superadmin by GPT-5.3 #01
CI / CD Pipeline / build (push) Successful in 14m25s
CI / CD Pipeline / deploy (push) Successful in 4m41s

This commit is contained in:
2026-03-29 16:21:57 +07:00
parent 2074654c18
commit 65aaae9d90
22 changed files with 3145 additions and 15 deletions
@@ -0,0 +1,65 @@
14540db954a05918780678361189125c
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var _a, _b, _c, _d;
Object.defineProperty(exports, "__esModule", { value: true });
exports.Tag = void 0;
const typeorm_1 = require("typeorm");
const project_entity_1 = require("../../project/entities/project.entity");
let Tag = class Tag {
};
exports.Tag = Tag;
__decorate([
(0, typeorm_1.PrimaryGeneratedColumn)(),
__metadata("design:type", Number)
], Tag.prototype, "id", void 0);
__decorate([
(0, typeorm_1.Column)({ name: 'project_id', type: 'int', nullable: true }),
__metadata("design:type", Object)
], Tag.prototype, "projectId", void 0);
__decorate([
(0, typeorm_1.Column)({ name: 'tag_name', length: 100 }),
__metadata("design:type", String)
], Tag.prototype, "tagName", void 0);
__decorate([
(0, typeorm_1.Column)({ name: 'color_code', length: 30, default: 'default' }),
__metadata("design:type", String)
], Tag.prototype, "colorCode", void 0);
__decorate([
(0, typeorm_1.Column)({ type: 'text', nullable: true }),
__metadata("design:type", Object)
], Tag.prototype, "description", void 0);
__decorate([
(0, typeorm_1.ManyToOne)(() => project_entity_1.Project),
(0, typeorm_1.JoinColumn)({ name: 'project_id' }),
__metadata("design:type", typeof (_a = typeof project_entity_1.Project !== "undefined" && project_entity_1.Project) === "function" ? _a : Object)
], Tag.prototype, "project", void 0);
__decorate([
(0, typeorm_1.CreateDateColumn)({ name: 'created_at' }),
__metadata("design:type", typeof (_b = typeof Date !== "undefined" && Date) === "function" ? _b : Object)
], Tag.prototype, "createdAt", void 0);
__decorate([
(0, typeorm_1.UpdateDateColumn)({ name: 'updated_at' }),
__metadata("design:type", typeof (_c = typeof Date !== "undefined" && Date) === "function" ? _c : Object)
], Tag.prototype, "updatedAt", void 0);
__decorate([
(0, typeorm_1.Column)({ name: 'created_by', type: 'int', nullable: true }),
__metadata("design:type", Object)
], Tag.prototype, "createdBy", void 0);
__decorate([
(0, typeorm_1.DeleteDateColumn)({ name: 'deleted_at' }),
__metadata("design:type", Object)
], Tag.prototype, "deletedAt", void 0);
exports.Tag = Tag = __decorate([
(0, typeorm_1.Entity)('tags'),
(0, typeorm_1.Unique)('ux_tag_project', ['projectId', 'tagName'])
], Tag);
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJmaWxlIjoiRTpcXG5wLWRtc1xcbGNicDNcXGJhY2tlbmRcXHNyY1xcbW9kdWxlc1xcbWFzdGVyXFxlbnRpdGllc1xcdGFnLmVudGl0eS50cyIsIm1hcHBpbmdzIjoiOzs7Ozs7Ozs7Ozs7O0FBQUEscUNBVWlCO0FBQ2pCLDBFQUFnRTtBQUl6RCxJQUFNLEdBQUcsR0FBVCxNQUFNLEdBQUc7Q0FnQ2YsQ0FBQTtBQWhDWSxrQkFBRztBQUVkO0lBREMsSUFBQSxnQ0FBc0IsR0FBRTs7K0JBQ2I7QUFHWjtJQURDLElBQUEsZ0JBQU0sRUFBQyxFQUFFLElBQUksRUFBRSxZQUFZLEVBQUUsSUFBSSxFQUFFLEtBQUssRUFBRSxRQUFRLEVBQUUsSUFBSSxFQUFFLENBQUM7O3NDQUNsQztBQUcxQjtJQURDLElBQUEsZ0JBQU0sRUFBQyxFQUFFLElBQUksRUFBRSxVQUFVLEVBQUUsTUFBTSxFQUFFLEdBQUcsRUFBRSxDQUFDOztvQ0FDekI7QUFHakI7SUFEQyxJQUFBLGdCQUFNLEVBQUMsRUFBRSxJQUFJLEVBQUUsWUFBWSxFQUFFLE1BQU0sRUFBRSxFQUFFLEVBQUUsT0FBTyxFQUFFLFNBQVMsRUFBRSxDQUFDOztzQ0FDNUM7QUFHbkI7SUFEQyxJQUFBLGdCQUFNLEVBQUMsRUFBRSxJQUFJLEVBQUUsTUFBTSxFQUFFLFFBQVEsRUFBRSxJQUFJLEVBQUUsQ0FBQzs7d0NBQ2I7QUFLNUI7SUFGQyxJQUFBLG1CQUFTLEVBQUMsR0FBRyxFQUFFLENBQUMsd0JBQU8sQ0FBQztJQUN4QixJQUFBLG9CQUFVLEVBQUMsRUFBRSxJQUFJLEVBQUUsWUFBWSxFQUFFLENBQUM7a0RBQ3pCLHdCQUFPLG9CQUFQLHdCQUFPO29DQUFDO0FBR2xCO0lBREMsSUFBQSwwQkFBZ0IsRUFBQyxFQUFFLElBQUksRUFBRSxZQUFZLEVBQUUsQ0FBQztrREFDN0IsSUFBSSxvQkFBSixJQUFJO3NDQUFDO0FBR2pCO0lBREMsSUFBQSwwQkFBZ0IsRUFBQyxFQUFFLElBQUksRUFBRSxZQUFZLEVBQUUsQ0FBQztrREFDN0IsSUFBSSxvQkFBSixJQUFJO3NDQUFDO0FBR2pCO0lBREMsSUFBQSxnQkFBTSxFQUFDLEVBQUUsSUFBSSxFQUFFLFlBQVksRUFBRSxJQUFJLEVBQUUsS0FBSyxFQUFFLFFBQVEsRUFBRSxJQUFJLEVBQUUsQ0FBQzs7c0NBQ2xDO0FBRzFCO0lBREMsSUFBQSwwQkFBZ0IsRUFBQyxFQUFFLElBQUksRUFBRSxZQUFZLEVBQUUsQ0FBQzs7c0NBQ2pCO2NBL0JiLEdBQUc7SUFGZixJQUFBLGdCQUFNLEVBQUMsTUFBTSxDQUFDO0lBQ2QsSUFBQSxnQkFBTSxFQUFDLGdCQUFnQixFQUFFLENBQUMsV0FBVyxFQUFFLFNBQVMsQ0FBQyxDQUFDO0dBQ3RDLEdBQUcsQ0FnQ2YiLCJuYW1lcyI6W10sInNvdXJjZXMiOlsiRTpcXG5wLWRtc1xcbGNicDNcXGJhY2tlbmRcXHNyY1xcbW9kdWxlc1xcbWFzdGVyXFxlbnRpdGllc1xcdGFnLmVudGl0eS50cyJdLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQge1xyXG4gIEVudGl0eSxcclxuICBDb2x1bW4sXHJcbiAgUHJpbWFyeUdlbmVyYXRlZENvbHVtbixcclxuICBDcmVhdGVEYXRlQ29sdW1uLFxyXG4gIFVwZGF0ZURhdGVDb2x1bW4sXHJcbiAgRGVsZXRlRGF0ZUNvbHVtbixcclxuICBVbmlxdWUsXHJcbiAgTWFueVRvT25lLFxyXG4gIEpvaW5Db2x1bW4sXHJcbn0gZnJvbSAndHlwZW9ybSc7XHJcbmltcG9ydCB7IFByb2plY3QgfSBmcm9tICcuLi8uLi9wcm9qZWN0L2VudGl0aWVzL3Byb2plY3QuZW50aXR5JztcclxuXHJcbkBFbnRpdHkoJ3RhZ3MnKVxyXG5AVW5pcXVlKCd1eF90YWdfcHJvamVjdCcsIFsncHJvamVjdElkJywgJ3RhZ05hbWUnXSlcclxuZXhwb3J0IGNsYXNzIFRhZyB7XHJcbiAgQFByaW1hcnlHZW5lcmF0ZWRDb2x1bW4oKVxyXG4gIGlkITogbnVtYmVyOyAvLyDguYDguJ7guLTguYjguKEgIVxyXG5cclxuICBAQ29sdW1uKHsgbmFtZTogJ3Byb2plY3RfaWQnLCB0eXBlOiAnaW50JywgbnVsbGFibGU6IHRydWUgfSlcclxuICBwcm9qZWN0SWQhOiBudW1iZXIgfCBudWxsOyAvLyDguYDguJ7guLTguYjguKEgIVxyXG5cclxuICBAQ29sdW1uKHsgbmFtZTogJ3RhZ19uYW1lJywgbGVuZ3RoOiAxMDAgfSlcclxuICB0YWdOYW1lITogc3RyaW5nOyAvLyDguYDguJ7guLTguYjguKEgIVxyXG5cclxuICBAQ29sdW1uKHsgbmFtZTogJ2NvbG9yX2NvZGUnLCBsZW5ndGg6IDMwLCBkZWZhdWx0OiAnZGVmYXVsdCcgfSlcclxuICBjb2xvckNvZGUhOiBzdHJpbmc7IC8vIOC5gOC4nuC4tOC5iOC4oSAhXHJcblxyXG4gIEBDb2x1bW4oeyB0eXBlOiAndGV4dCcsIG51bGxhYmxlOiB0cnVlIH0pXHJcbiAgZGVzY3JpcHRpb24hOiBzdHJpbmcgfCBudWxsOyAvLyDguYDguJ7guLTguYjguKEgIVxyXG5cclxuICAvLyBSZWxhdGlvbnNcclxuICBATWFueVRvT25lKCgpID0+IFByb2plY3QpXHJcbiAgQEpvaW5Db2x1bW4oeyBuYW1lOiAncHJvamVjdF9pZCcgfSlcclxuICBwcm9qZWN0PzogUHJvamVjdDtcclxuXHJcbiAgQENyZWF0ZURhdGVDb2x1bW4oeyBuYW1lOiAnY3JlYXRlZF9hdCcgfSlcclxuICBjcmVhdGVkQXQhOiBEYXRlOyAvLyDguYDguJ7guLTguYjguKEgIVxyXG5cclxuICBAVXBkYXRlRGF0ZUNvbHVtbih7IG5hbWU6ICd1cGRhdGVkX2F0JyB9KVxyXG4gIHVwZGF0ZWRBdCE6IERhdGU7IC8vIOC5gOC4nuC4tOC5iOC4oSAhXHJcblxyXG4gIEBDb2x1bW4oeyBuYW1lOiAnY3JlYXRlZF9ieScsIHR5cGU6ICdpbnQnLCBudWxsYWJsZTogdHJ1ZSB9KVxyXG4gIGNyZWF0ZWRCeSE6IG51bWJlciB8IG51bGw7IC8vIOC5gOC4nuC4tOC5iOC4oSAhXHJcblxyXG4gIEBEZWxldGVEYXRlQ29sdW1uKHsgbmFtZTogJ2RlbGV0ZWRfYXQnIH0pXHJcbiAgZGVsZXRlZEF0ITogRGF0ZSB8IG51bGw7IC8vIOC5gOC4nuC4tOC5iOC4oSAhXHJcbn1cclxuIl0sInZlcnNpb24iOjN9
@@ -0,0 +1 @@
{"file":"E:\\np-dms\\lcbp3\\backend\\src\\modules\\master\\entities\\tag.entity.ts","mappings":";;;;;;;;;;;;;AAAA,qCAUiB;AACjB,0EAAgE;AAIzD,IAAM,GAAG,GAAT,MAAM,GAAG;CAgCf,CAAA;AAhCY,kBAAG;AAEd;IADC,IAAA,gCAAsB,GAAE;;+BACb;AAGZ;IADC,IAAA,gBAAM,EAAC,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;;sCAClC;AAG1B;IADC,IAAA,gBAAM,EAAC,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC;;oCACzB;AAGjB;IADC,IAAA,gBAAM,EAAC,EAAE,IAAI,EAAE,YAAY,EAAE,MAAM,EAAE,EAAE,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC;;sCAC5C;AAGnB;IADC,IAAA,gBAAM,EAAC,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;;wCACb;AAK5B;IAFC,IAAA,mBAAS,EAAC,GAAG,EAAE,CAAC,wBAAO,CAAC;IACxB,IAAA,oBAAU,EAAC,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC;kDACzB,wBAAO,oBAAP,wBAAO;oCAAC;AAGlB;IADC,IAAA,0BAAgB,EAAC,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC;kDAC7B,IAAI,oBAAJ,IAAI;sCAAC;AAGjB;IADC,IAAA,0BAAgB,EAAC,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC;kDAC7B,IAAI,oBAAJ,IAAI;sCAAC;AAGjB;IADC,IAAA,gBAAM,EAAC,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;;sCAClC;AAG1B;IADC,IAAA,0BAAgB,EAAC,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC;;sCACjB;cA/Bb,GAAG;IAFf,IAAA,gBAAM,EAAC,MAAM,CAAC;IACd,IAAA,gBAAM,EAAC,gBAAgB,EAAE,CAAC,WAAW,EAAE,SAAS,CAAC,CAAC;GACtC,GAAG,CAgCf","names":[],"sources":["E:\\np-dms\\lcbp3\\backend\\src\\modules\\master\\entities\\tag.entity.ts"],"sourcesContent":["import {\r\n Entity,\r\n Column,\r\n PrimaryGeneratedColumn,\r\n CreateDateColumn,\r\n UpdateDateColumn,\r\n DeleteDateColumn,\r\n Unique,\r\n ManyToOne,\r\n JoinColumn,\r\n} from 'typeorm';\r\nimport { Project } from '../../project/entities/project.entity';\r\n\r\n@Entity('tags')\r\n@Unique('ux_tag_project', ['projectId', 'tagName'])\r\nexport class Tag {\r\n @PrimaryGeneratedColumn()\r\n id!: number; // เพิ่ม !\r\n\r\n @Column({ name: 'project_id', type: 'int', nullable: true })\r\n projectId!: number | null; // เพิ่ม !\r\n\r\n @Column({ name: 'tag_name', length: 100 })\r\n tagName!: string; // เพิ่ม !\r\n\r\n @Column({ name: 'color_code', length: 30, default: 'default' })\r\n colorCode!: string; // เพิ่ม !\r\n\r\n @Column({ type: 'text', nullable: true })\r\n description!: string | null; // เพิ่ม !\r\n\r\n // Relations\r\n @ManyToOne(() => Project)\r\n @JoinColumn({ name: 'project_id' })\r\n project?: Project;\r\n\r\n @CreateDateColumn({ name: 'created_at' })\r\n createdAt!: Date; // เพิ่ม !\r\n\r\n @UpdateDateColumn({ name: 'updated_at' })\r\n updatedAt!: Date; // เพิ่ม !\r\n\r\n @Column({ name: 'created_by', type: 'int', nullable: true })\r\n createdBy!: number | null; // เพิ่ม !\r\n\r\n @DeleteDateColumn({ name: 'deleted_at' })\r\n deletedAt!: Date | null; // เพิ่ม !\r\n}\r\n"],"version":3}
@@ -1 +1 @@
{"E:\\np-dms\\lcbp3\\backend\\src\\modules\\correspondence\\due-date-reminder.service.spec.ts":[1,1723],"E:\\np-dms\\lcbp3\\backend\\src\\modules\\correspondence\\correspondence.service.spec.ts":[1,11114],"E:\\np-dms\\lcbp3\\backend\\src\\common\\auth\\auth.service.spec.ts":[1,1538],"E:\\np-dms\\lcbp3\\backend\\src\\modules\\document-numbering\\document-numbering.service.spec.ts":[1,2037],"E:\\np-dms\\lcbp3\\backend\\src\\common\\auth\\casl\\ability.factory.spec.ts":[1,1294],"E:\\np-dms\\lcbp3\\backend\\src\\common\\services\\uuid-resolver.service.spec.ts":[1,5491],"E:\\np-dms\\lcbp3\\backend\\src\\modules\\workflow-engine\\workflow-engine.service.spec.ts":[1,5639],"E:\\np-dms\\lcbp3\\backend\\src\\modules\\workflow-engine\\dsl\\parser.service.spec.ts":[1,5818],"E:\\np-dms\\lcbp3\\backend\\src\\common\\pipes\\parse-uuid.pipe.spec.ts":[1,355],"E:\\np-dms\\lcbp3\\backend\\src\\modules\\user\\user.service.spec.ts":[1,1270],"E:\\np-dms\\lcbp3\\backend\\src\\common\\file-storage\\file-storage.service.spec.ts":[1,1187],"E:\\np-dms\\lcbp3\\backend\\src\\modules\\correspondence\\correspondence.controller.spec.ts":[1,12100],"E:\\np-dms\\lcbp3\\backend\\src\\modules\\migration\\migration.service.spec.ts":[1,1296],"E:\\np-dms\\lcbp3\\backend\\src\\common\\entities\\uuid-base.entity.spec.ts":[1,430],"E:\\np-dms\\lcbp3\\backend\\src\\modules\\project\\project.service.spec.ts":[1,1109],"E:\\np-dms\\lcbp3\\backend\\src\\common\\auth\\auth.controller.spec.ts":[1,2191],"E:\\np-dms\\lcbp3\\backend\\src\\modules\\document-numbering\\services\\manual-override.service.spec.ts":[1,5256],"E:\\np-dms\\lcbp3\\backend\\src\\modules\\project\\project.controller.spec.ts":[1,1827],"E:\\np-dms\\lcbp3\\backend\\src\\modules\\migration\\migration.controller.spec.ts":[1,6380],"E:\\np-dms\\lcbp3\\backend\\src\\common\\file-storage\\file-storage.controller.spec.ts":[1,1482],"E:\\np-dms\\lcbp3\\backend\\src\\app.controller.spec.ts":[1,587],"E:\\np-dms\\lcbp3\\backend\\src\\modules\\json-schema\\json-schema.controller.spec.ts":[1,5062]} {"E:\\np-dms\\lcbp3\\backend\\src\\modules\\correspondence\\due-date-reminder.service.spec.ts":[1,1723],"E:\\np-dms\\lcbp3\\backend\\src\\modules\\correspondence\\correspondence.service.spec.ts":[1,7901],"E:\\np-dms\\lcbp3\\backend\\src\\common\\auth\\auth.service.spec.ts":[1,1538],"E:\\np-dms\\lcbp3\\backend\\src\\modules\\document-numbering\\document-numbering.service.spec.ts":[1,2037],"E:\\np-dms\\lcbp3\\backend\\src\\common\\auth\\casl\\ability.factory.spec.ts":[1,1294],"E:\\np-dms\\lcbp3\\backend\\src\\common\\services\\uuid-resolver.service.spec.ts":[1,5491],"E:\\np-dms\\lcbp3\\backend\\src\\modules\\workflow-engine\\workflow-engine.service.spec.ts":[1,5639],"E:\\np-dms\\lcbp3\\backend\\src\\modules\\workflow-engine\\dsl\\parser.service.spec.ts":[1,5818],"E:\\np-dms\\lcbp3\\backend\\src\\common\\pipes\\parse-uuid.pipe.spec.ts":[1,355],"E:\\np-dms\\lcbp3\\backend\\src\\modules\\user\\user.service.spec.ts":[1,1270],"E:\\np-dms\\lcbp3\\backend\\src\\common\\file-storage\\file-storage.service.spec.ts":[1,1187],"E:\\np-dms\\lcbp3\\backend\\src\\modules\\correspondence\\correspondence.controller.spec.ts":[1,8495],"E:\\np-dms\\lcbp3\\backend\\src\\modules\\migration\\migration.service.spec.ts":[1,1296],"E:\\np-dms\\lcbp3\\backend\\src\\common\\entities\\uuid-base.entity.spec.ts":[1,430],"E:\\np-dms\\lcbp3\\backend\\src\\modules\\project\\project.service.spec.ts":[1,1109],"E:\\np-dms\\lcbp3\\backend\\src\\common\\auth\\auth.controller.spec.ts":[1,2191],"E:\\np-dms\\lcbp3\\backend\\src\\modules\\document-numbering\\services\\manual-override.service.spec.ts":[1,5256],"E:\\np-dms\\lcbp3\\backend\\src\\modules\\project\\project.controller.spec.ts":[1,1827],"E:\\np-dms\\lcbp3\\backend\\src\\modules\\migration\\migration.controller.spec.ts":[1,6380],"E:\\np-dms\\lcbp3\\backend\\src\\common\\file-storage\\file-storage.controller.spec.ts":[1,1482],"E:\\np-dms\\lcbp3\\backend\\src\\app.controller.spec.ts":[1,587],"E:\\np-dms\\lcbp3\\backend\\src\\modules\\json-schema\\json-schema.controller.spec.ts":[1,5062]}
@@ -15,9 +15,15 @@ import { UpdateCirculationRoutingDto } from './dto/update-circulation-routing.dt
import { SearchCirculationDto } from './dto/search-circulation.dto'; import { SearchCirculationDto } from './dto/search-circulation.dto';
import { DocumentNumberingService } from '../document-numbering/services/document-numbering.service'; import { DocumentNumberingService } from '../document-numbering/services/document-numbering.service';
import { UuidResolverService } from '../../common/services/uuid-resolver.service'; import { UuidResolverService } from '../../common/services/uuid-resolver.service';
import { UserService } from '../user/user.service';
@Injectable() @Injectable()
export class CirculationService { export class CirculationService {
private async hasSystemManageAllPermission(userId: number): Promise<boolean> {
const permissions = await this.userService.getUserPermissions(userId);
return permissions.includes('system.manage_all');
}
constructor( constructor(
@InjectRepository(Circulation) @InjectRepository(Circulation)
private circulationRepo: Repository<Circulation>, private circulationRepo: Repository<Circulation>,
@@ -25,11 +31,36 @@ export class CirculationService {
private routingRepo: Repository<CirculationRouting>, private routingRepo: Repository<CirculationRouting>,
private numberingService: DocumentNumberingService, private numberingService: DocumentNumberingService,
private dataSource: DataSource, private dataSource: DataSource,
private uuidResolver: UuidResolverService private uuidResolver: UuidResolverService,
private userService: UserService
) {} ) {}
async create(createDto: CreateCirculationDto, user: User) { async create(createDto: CreateCirculationDto, user: User) {
if (!user.primaryOrganizationId) { let userOrgId = user.primaryOrganizationId;
if (!userOrgId) {
const fullUser = await this.userService.findOne(user.user_id);
if (fullUser) {
userOrgId = fullUser.primaryOrganizationId;
}
}
const resolvedOriginatorId = createDto.originatorId
? await this.uuidResolver.resolveOrganizationId(createDto.originatorId)
: undefined;
if (resolvedOriginatorId && resolvedOriginatorId !== userOrgId) {
const canManageAll = await this.hasSystemManageAllPermission(
user.user_id
);
if (!canManageAll) {
throw new ForbiddenException(
'You do not have permission to create documents on behalf of other organizations.'
);
}
userOrgId = resolvedOriginatorId;
}
if (!userOrgId) {
throw new BadRequestException('User must belong to an organization'); throw new BadRequestException('User must belong to an organization');
} }
@@ -52,7 +83,7 @@ export class CirculationService {
// Generate No. using DocumentNumberingService (Type 900 - Circulation) // Generate No. using DocumentNumberingService (Type 900 - Circulation)
const result = await this.numberingService.generateNextNumber({ const result = await this.numberingService.generateNextNumber({
projectId: resolvedProjectId, projectId: resolvedProjectId,
originatorOrganizationId: user.primaryOrganizationId, originatorOrganizationId: userOrgId,
typeId: 900, // Fixed Type ID for Circulation typeId: 900, // Fixed Type ID for Circulation
year: new Date().getFullYear(), year: new Date().getFullYear(),
customTokens: { customTokens: {
@@ -62,7 +93,7 @@ export class CirculationService {
}); });
const circulation = queryRunner.manager.create(Circulation, { const circulation = queryRunner.manager.create(Circulation, {
organizationId: user.primaryOrganizationId, organizationId: userOrgId,
correspondenceId: resolvedCorrId, correspondenceId: resolvedCorrId,
circulationNo: result.number, circulationNo: result.number,
subject: createDto.subject, subject: createDto.subject,
@@ -76,7 +107,7 @@ export class CirculationService {
queryRunner.manager.create(CirculationRouting, { queryRunner.manager.create(CirculationRouting, {
circulationId: savedCirculation.id, circulationId: savedCirculation.id,
stepNumber: index + 1, stepNumber: index + 1,
organizationId: user.primaryOrganizationId, organizationId: userOrgId,
assignedTo: assigneeId, assignedTo: assigneeId,
status: 'PENDING', status: 'PENDING',
}) })
@@ -13,6 +13,9 @@ export class CreateCirculationDto {
@IsOptional() @IsOptional()
projectId?: number | string; // Project ID or UUID for Numbering projectId?: number | string; // Project ID or UUID for Numbering
@IsOptional()
originatorId?: number | string; // ระบุองค์กรเจ้าของเอกสาร (ต้องใช้ร่วมกับสิทธิ system.manage_all)
@IsString() @IsString()
@IsNotEmpty() @IsNotEmpty()
subject!: string; // หัวข้อเรื่อง (Subject) subject!: string; // หัวข้อเรื่อง (Subject)
@@ -19,6 +19,7 @@ import { FileStorageService } from '../../common/file-storage/file-storage.servi
import { UuidResolverService } from '../../common/services/uuid-resolver.service'; import { UuidResolverService } from '../../common/services/uuid-resolver.service';
import { NotificationService } from '../notification/notification.service'; import { NotificationService } from '../notification/notification.service';
import { UpdateCorrespondenceDto } from './dto/update-correspondence.dto'; import { UpdateCorrespondenceDto } from './dto/update-correspondence.dto';
import { CreateCorrespondenceDto } from './dto/create-correspondence.dto';
import { User } from '../user/entities/user.entity'; import { User } from '../user/entities/user.entity';
describe('CorrespondenceService', () => { describe('CorrespondenceService', () => {
@@ -336,4 +337,95 @@ describe('CorrespondenceService', () => {
).toHaveBeenCalled(); ).toHaveBeenCalled();
}); });
}); });
describe('create', () => {
it('should allow system.manage_all user without primaryOrganizationId when originatorId is provided', async () => {
const mockUser = {
user_id: 1,
primaryOrganizationId: null,
} as unknown as User;
const createDto: CreateCorrespondenceDto = {
projectId: 'project-uuid',
typeId: 1,
subject: 'Test Subject',
originatorId: 'originator-uuid',
recipients: [{ organizationId: 'recipient-uuid', type: 'TO' }],
};
const userService = testingModule.get<UserService>(UserService);
const typeRepo = testingModule.get<Repository<CorrespondenceType>>(
getRepositoryToken(CorrespondenceType)
);
const statusRepo = testingModule.get<Repository<CorrespondenceStatus>>(
getRepositoryToken(CorrespondenceStatus)
);
const uuidResolver =
testingModule.get<UuidResolverService>(UuidResolverService);
(userService.findOne as jest.Mock).mockResolvedValue({
user_id: 1,
primaryOrganizationId: null,
});
(userService.getUserPermissions as jest.Mock).mockResolvedValue([
'system.manage_all',
]);
(uuidResolver.resolveProjectId as jest.Mock).mockResolvedValue(100);
(uuidResolver.resolveOrganizationId as jest.Mock).mockImplementation(
(value: number | string) => {
if (value === 'originator-uuid') return 10;
if (value === 'recipient-uuid') return 20;
return 0;
}
);
(typeRepo.findOne as jest.Mock).mockResolvedValue({
id: 1,
typeCode: 'LTR',
});
(statusRepo.findOne as jest.Mock).mockResolvedValue({
id: 1,
statusCode: 'DRAFT',
});
(numberingService.generateNextNumber as jest.Mock).mockResolvedValue({
number: 'DOC-001',
});
mockDataSource.manager.findOne
.mockResolvedValueOnce({ id: 10, organizationCode: 'ORG' })
.mockResolvedValueOnce({ id: 20, organizationCode: 'REC' });
const queryRunner = {
connect: jest.fn(),
startTransaction: jest.fn(),
commitTransaction: jest.fn(),
rollbackTransaction: jest.fn(),
release: jest.fn(),
manager: {
create: jest.fn(
(_entity: unknown, payload: Record<string, unknown>) => payload
),
save: jest
.fn()
.mockResolvedValueOnce({ id: 999, publicId: 'corr-uuid' })
.mockResolvedValueOnce({ id: 1000 })
.mockResolvedValueOnce([]),
findOne: jest.fn(),
},
};
(mockDataSource.createQueryRunner as jest.Mock).mockReturnValue(
queryRunner
);
await service.create(createDto, mockUser);
expect(queryRunner.manager.create).toHaveBeenCalledWith(
Correspondence,
expect.objectContaining({ originatorId: 10 })
);
});
});
}); });
@@ -50,6 +50,11 @@ interface ResolvedRecipient {
export class CorrespondenceService { export class CorrespondenceService {
private readonly logger = new Logger(CorrespondenceService.name); private readonly logger = new Logger(CorrespondenceService.name);
private async hasSystemManageAllPermission(userId: number): Promise<boolean> {
const permissions = await this.userService.getUserPermissions(userId);
return permissions.includes('system.manage_all');
}
constructor( constructor(
@InjectRepository(Correspondence) @InjectRepository(Correspondence)
private correspondenceRepo: Repository<Correspondence>, private correspondenceRepo: Repository<Correspondence>,
@@ -92,9 +97,22 @@ export class CorrespondenceService {
} }
if (!userOrgId) { if (!userOrgId) {
throw new BadRequestException( if (createDto.originatorId) {
'User must belong to an organization to create documents' const canManageAll = await this.hasSystemManageAllPermission(
); user.user_id
);
if (canManageAll) {
userOrgId = await this.uuidResolver.resolveOrganizationId(
createDto.originatorId
);
}
}
if (!userOrgId) {
throw new BadRequestException(
'User must belong to an organization to create documents'
);
}
} }
// For impersonation, use the specified originator // For impersonation, use the specified originator
@@ -187,10 +205,10 @@ export class CorrespondenceService {
// Impersonation Logic // Impersonation Logic
if (resolvedOriginatorId && resolvedOriginatorId !== userOrgId) { if (resolvedOriginatorId && resolvedOriginatorId !== userOrgId) {
const permissions = await this.userService.getUserPermissions( const canManageAll = await this.hasSystemManageAllPermission(
user.user_id user.user_id
); );
if (!permissions.includes('system.manage_all')) { if (!canManageAll) {
throw new ForbiddenException( throw new ForbiddenException(
'You do not have permission to create documents on behalf of other organizations.' 'You do not have permission to create documents on behalf of other organizations.'
); );
@@ -25,6 +25,14 @@ export class CreateRfaDto {
@IsNotEmpty() @IsNotEmpty()
toOrganizationId!: number | string; toOrganizationId!: number | string;
@ApiProperty({
description:
'Originator Organization ID or UUID (for users with system.manage_all)',
required: false,
})
@IsOptional()
originatorId?: number | string;
@ApiProperty({ description: 'ID ของประเภท RFA', example: 1 }) @ApiProperty({ description: 'ID ของประเภท RFA', example: 1 })
@IsInt() @IsInt()
@IsNotEmpty() @IsNotEmpty()
+22
View File
@@ -63,6 +63,11 @@ import { UuidResolverService } from '../../common/services/uuid-resolver.service
export class RfaService { export class RfaService {
private readonly logger = new Logger(RfaService.name); private readonly logger = new Logger(RfaService.name);
private async hasSystemManageAllPermission(userId: number): Promise<boolean> {
const permissions = await this.userService.getUserPermissions(userId);
return permissions.includes('system.manage_all');
}
constructor( constructor(
@InjectRepository(Rfa) @InjectRepository(Rfa)
private rfaRepo: Repository<Rfa>, private rfaRepo: Repository<Rfa>,
@@ -226,12 +231,29 @@ export class RfaService {
); );
} }
const resolvedOriginatorId = createDto.originatorId
? await this.uuidResolver.resolveOrganizationId(createDto.originatorId)
: undefined;
// Determine User Organization // Determine User Organization
let userOrgId = user.primaryOrganizationId; let userOrgId = user.primaryOrganizationId;
if (!userOrgId) { if (!userOrgId) {
const fullUser = await this.userService.findOne(user.user_id); const fullUser = await this.userService.findOne(user.user_id);
if (fullUser) userOrgId = fullUser.primaryOrganizationId; if (fullUser) userOrgId = fullUser.primaryOrganizationId;
} }
if (resolvedOriginatorId && resolvedOriginatorId !== userOrgId) {
const canManageAll = await this.hasSystemManageAllPermission(
user.user_id
);
if (!canManageAll) {
throw new ForbiddenException(
'You do not have permission to create documents on behalf of other organizations.'
);
}
userOrgId = resolvedOriginatorId;
}
if (!userOrgId) { if (!userOrgId) {
throw new BadRequestException('User must belong to an organization'); throw new BadRequestException('User must belong to an organization');
} }
@@ -54,6 +54,14 @@ export class CreateTransmittalDto {
@IsNotEmpty() @IsNotEmpty()
recipientOrganizationId!: number | string; recipientOrganizationId!: number | string;
@ApiProperty({
description:
'ผู้ส่ง Organization ID หรือ UUID (สำหรับผู้ใช้ที่มีสิทธิ system.manage_all)',
required: false,
})
@IsOptional()
originatorId?: number | string;
@ApiProperty({ @ApiProperty({
description: 'Correspondence ID หรือ UUID (ADR-019)', description: 'Correspondence ID หรือ UUID (ADR-019)',
required: false, required: false,
@@ -4,6 +4,7 @@ import {
NotFoundException, NotFoundException,
InternalServerErrorException, InternalServerErrorException,
BadRequestException, BadRequestException,
ForbiddenException,
} from '@nestjs/common'; } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { Repository, DataSource } from 'typeorm'; import { Repository, DataSource } from 'typeorm';
@@ -22,11 +23,17 @@ import { CorrespondenceType } from '../correspondence/entities/correspondence-ty
import { CorrespondenceStatus } from '../correspondence/entities/correspondence-status.entity'; import { CorrespondenceStatus } from '../correspondence/entities/correspondence-status.entity';
import { UuidResolverService } from '../../common/services/uuid-resolver.service'; import { UuidResolverService } from '../../common/services/uuid-resolver.service';
import { CorrespondenceRecipient } from '../correspondence/entities/correspondence-recipient.entity'; import { CorrespondenceRecipient } from '../correspondence/entities/correspondence-recipient.entity';
import { UserService } from '../user/user.service';
@Injectable() @Injectable()
export class TransmittalService { export class TransmittalService {
private readonly logger = new Logger(TransmittalService.name); private readonly logger = new Logger(TransmittalService.name);
private async hasSystemManageAllPermission(userId: number): Promise<boolean> {
const permissions = await this.userService.getUserPermissions(userId);
return permissions.includes('system.manage_all');
}
constructor( constructor(
@InjectRepository(Transmittal) @InjectRepository(Transmittal)
private transmittalRepo: Repository<Transmittal>, private transmittalRepo: Repository<Transmittal>,
@@ -38,7 +45,8 @@ export class TransmittalService {
private statusRepo: Repository<CorrespondenceStatus>, private statusRepo: Repository<CorrespondenceStatus>,
private numberingService: DocumentNumberingService, private numberingService: DocumentNumberingService,
private dataSource: DataSource, private dataSource: DataSource,
private uuidResolver: UuidResolverService private uuidResolver: UuidResolverService,
private userService: UserService
) {} ) {}
async create( async create(
@@ -61,7 +69,31 @@ export class TransmittalService {
await queryRunner.connect(); await queryRunner.connect();
await queryRunner.startTransaction(); await queryRunner.startTransaction();
if (!user.primaryOrganizationId) { let userOrgId = user.primaryOrganizationId;
if (!userOrgId) {
const fullUser = await this.userService.findOne(user.user_id);
if (fullUser) {
userOrgId = fullUser.primaryOrganizationId;
}
}
const resolvedOriginatorId = createDto.originatorId
? await this.uuidResolver.resolveOrganizationId(createDto.originatorId)
: undefined;
if (resolvedOriginatorId && resolvedOriginatorId !== userOrgId) {
const canManageAll = await this.hasSystemManageAllPermission(
user.user_id
);
if (!canManageAll) {
throw new ForbiddenException(
'You do not have permission to create documents on behalf of other organizations.'
);
}
userOrgId = resolvedOriginatorId;
}
if (!userOrgId) {
throw new BadRequestException( throw new BadRequestException(
'User must belong to an organization to create a transmittal' 'User must belong to an organization to create a transmittal'
); );
@@ -76,7 +108,7 @@ export class TransmittalService {
// 2. Generate Number // 2. Generate Number
const docNumber = await this.numberingService.generateNextNumber({ const docNumber = await this.numberingService.generateNextNumber({
projectId: internalProjectId, projectId: internalProjectId,
originatorOrganizationId: user.primaryOrganizationId, originatorOrganizationId: userOrgId,
typeId: type.id, typeId: type.id,
year: new Date().getFullYear(), year: new Date().getFullYear(),
customTokens: { customTokens: {
@@ -90,7 +122,7 @@ export class TransmittalService {
correspondenceNumber: docNumber.number, correspondenceNumber: docNumber.number,
correspondenceTypeId: type.id, correspondenceTypeId: type.id,
projectId: internalProjectId, projectId: internalProjectId,
originatorId: user.primaryOrganizationId, originatorId: userOrgId,
isInternal: false, isInternal: false,
createdBy: user.user_id, createdBy: user.user_id,
}); });