@things-factory/worklist 10.1.221 → 10.1.223
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/controllers/activity-approval/approve.js +3 -0
- package/dist-server/controllers/activity-approval/approve.js.map +1 -1
- package/dist-server/controllers/activity-approval/create-approval.js +8 -2
- package/dist-server/controllers/activity-approval/create-approval.js.map +1 -1
- package/dist-server/controllers/activity-approval/delegate.js +4 -1
- package/dist-server/controllers/activity-approval/delegate.js.map +1 -1
- package/dist-server/controllers/activity-approval/reject.js +3 -0
- package/dist-server/controllers/activity-approval/reject.js.map +1 -1
- package/dist-server/service/approval-notice/approval-notice-rules.d.ts +67 -0
- package/dist-server/service/approval-notice/approval-notice-rules.js +67 -0
- package/dist-server/service/approval-notice/approval-notice-rules.js.map +1 -0
- package/dist-server/service/approval-notice/approval-notice.d.ts +11 -0
- package/dist-server/service/approval-notice/approval-notice.js +76 -0
- package/dist-server/service/approval-notice/approval-notice.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +10 -9
- package/tests/approval-notice-db.test.ts +180 -0
- package/translations/en.json +15 -1
- package/translations/es.json +15 -1
- package/translations/ja.json +15 -1
- package/translations/ko.json +15 -1
- package/translations/ms.json +15 -1
- package/translations/zh.json +15 -1
|
@@ -9,6 +9,7 @@ const check_authority_js_1 = require("./check-authority.js");
|
|
|
9
9
|
const approval_line_js_1 = require("./approval-line.js");
|
|
10
10
|
const create_approval_js_1 = require("./create-approval.js");
|
|
11
11
|
const claim_js_1 = require("./claim.js");
|
|
12
|
+
const approval_notice_js_1 = require("../../service/approval-notice/approval-notice.js");
|
|
12
13
|
async function approve({ id, comment }, context) {
|
|
13
14
|
const { domain, user, tx } = context.state;
|
|
14
15
|
var repository = tx.getRepository(activity_approval_js_1.ActivityApproval);
|
|
@@ -132,6 +133,8 @@ async function approve({ id, comment }, context) {
|
|
|
132
133
|
terminator: user,
|
|
133
134
|
updater: user
|
|
134
135
|
});
|
|
136
|
+
/* The last approval ended it - the requester is told (ADR-0164). The next approver's turn is told where its row is made. */
|
|
137
|
+
await (0, approval_notice_js_1.noticeApproval)(context, { kind: 'approved', approvalId: result.id });
|
|
135
138
|
}
|
|
136
139
|
await (0, common_js_1.updateActivityInstanceState)(activityThread.activityInstanceId, { causedBy: [activityThread] }, context);
|
|
137
140
|
return result;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"approve.js","sourceRoot":"","sources":["../../../server/controllers/activity-approval/approve.ts"],"names":[],"mappings":"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","sourcesContent":["import { ApprovalLineItem } from '@things-factory/organization'\nimport { stampedComment } from './opinion.js'\nimport { ActivityThread, ActivityThreadStatus } from '../../service/activity-thread/activity-thread.js'\nimport { ActivityApproval, ActivityApprovalJudgment } from '../../service/activity-approval/activity-approval.js'\nimport { updateActivityInstanceState } from '../common.js'\nimport { checkAuthority } from './check-authority.js'\nimport { isReferenceOrder, nextApproval, referencesToNotify } from './approval-line.js'\nimport { createApproval, createReferences } from './create-approval.js'\nimport { claimPendingApproval } from './claim.js'\n\nexport async function approve(\n { id, comment }: { id: string; comment: string },\n context: ResolverContext\n): Promise<ActivityApproval> {\n const { domain, user, tx } = context.state\n\n var repository = tx.getRepository(ActivityApproval)\n\n var activityApproval = await repository.findOne({\n where: { domain: { id: domain.id }, id }\n })\n\n if (!activityApproval) {\n throw new Error(\n context.t('error.activity-approval not found', {\n activityApproval: id\n })\n )\n }\n\n await checkAuthority(activityApproval, context)\n\n /* \n Prerequisite of Approval\n - Previous judgment should be 'pending'\n */\n if (activityApproval.judgment !== ActivityApprovalJudgment.Pending) {\n throw new Error(\n context.t(`error.activity-approval is already terminated`, {\n activityApproval: id,\n actual: activityApproval.judgment\n })\n )\n }\n\n var activityThread = await tx.getRepository(ActivityThread).findOne({\n where: { domain: { id: domain.id }, id: activityApproval.activityThreadId },\n relations: ['activityInstance']\n })\n\n /*\n * 여기까지 오면 스레드가 있어야 한다. 없는 채로 아래를 읽으면 `TypeError` 가 나고, 그 오류로는\n * 무엇이 없는지 알 수 없다.\n */\n if (!activityThread) {\n throw new Error(context.t('error.activity-thread not found', { activityThread: activityApproval.activityThreadId }))\n }\n\n if (!activityThread.activityInstance) {\n throw new Error(context.t('error.activity-instance not found', { activityThread: activityThread.id }))\n }\n\n const approvalLine: ApprovalLineItem[] = activityThread.activityInstance.approvalLine\n const order = activityApproval.order\n\n /*\n * A reference does not judge.\n *\n * The three checks above all pass for a reference row: it exists, checkAuthority says it belongs\n * to this user, and its judgment is still Pending. Without this refusal the approval is recorded\n * as real and nextApproval() then picks the approver sitting after that seat, moving the approval\n * a step forward; with no approver after it, one reference ends the thread.\n *\n * This stands before claimPendingApproval, the first write. Throwing after the claim would leave\n * the row already stamped with a judgment.\n *\n * The message says what this person can do instead of reporting a missing permission. A reference\n * seat is a different seat, not a blocked one — comments are theirs to leave.\n */\n if (isReferenceOrder(approvalLine, order)) {\n throw new Error(context.t('error.activity-approval is reference only'))\n }\n\n /* 다음 결재자가 누구인지는 한 자리가 정한다 — `approval-line.ts`. */\n const next = nextApproval(approvalLine, order)\n\n /*\n * **여기서 잡는다.** 위의 검사는 읽은 시점의 것이라 그 사이에 남이 처리할 수 있다. 잡기가 첫 쓰기여야\n * 진 쪽이 다음 결재자를 만들기 전에 물러난다.\n */\n if (!(await claimPendingApproval(tx, activityApproval, ActivityApprovalJudgment.Approved))) {\n /* 「이미 끝났다」보다 **그 사람이 할 일**을 말한다 — 목록을 새로 읽으면 지금 상태가 보인다. */\n throw new Error(context.t('error.activity-approval taken by someone else', { activityApproval: id }))\n }\n\n /*\n * **승인한 사람은 승인한 것이다** (2026-08-31, 아키텍트 확인).\n *\n * 앞서는 다음 결재자가 있으면 `Escalated` 로 적었다. 그러면 「누가 승인했나」를 물을 때 중간 결재자가\n * 전부 「상신함」으로 나오고, **승인한 것과 그냥 넘긴 것이 구별되지 않는다.** 규제 산업에서는 결재\n * 이력이 곧 근거라 이것이 결함이다.\n *\n * 다음으로 넘어간 사실은 `transaction` 이 그대로 들고 있으므로 잃는 것이 없다.\n */\n const result = await tx.getRepository(ActivityApproval).save({\n ...activityApproval,\n judgment: ActivityApprovalJudgment.Approved,\n ...stampedComment(comment),\n transaction: next ? 'escalate' : 'approve',\n /*\n * **누가 처리했는지 남긴다.**\n *\n * 역할로 걸린 결재는 `approver` 가 비어 있고 `approverRole` 만 있다. 처리해도 그 자리가 비어 있으면\n * 두 가지가 함께 무너진다 — 그 역할을 가진 사람들의 **대기 목록에 영원히 남고**(`approver IS NULL`\n * 로 걸리므로), 정작 처리한 사람의 **완료 목록에는 안 나온다**(`approver = 나` 로 거르므로).\n *\n * 그리고 결재 이력에서 「누가 승인했나」는 역할이 아니라 사람이어야 한다.\n */\n approver: activityApproval.approver ?? user,\n updater: user,\n terminatedAt: new Date(),\n terminator: user\n })\n\n /*\n * **이번 결재가 끝나서 알릴 참조들.** `notifyAfter` 가 이 차례를 가리키는 참조가 여기 온다.\n *\n * 다음 결재자를 세우는 것과 **무관하게** 만든다 — 참조는 막지 않으므로, 이것이 실패해도 결재는 간다.\n */\n const refs = await createReferences(\n { references: referencesToNotify(approvalLine, order), activityThread },\n context\n )\n for (const f of refs.failed) {\n console.warn(`[worklist] 참조자를 세우지 못했다 (결재선 ${f.order}번째) — ${f.reason}`)\n }\n\n if (next) {\n /* 다음 결재자에게 넘긴다 — 행을 만드는 자리는 한 곳이다(§`create-approval.ts`). */\n await createApproval(\n { item: next.item, order: next.order, participation: 'approval', activityThread, transaction: 'escalate' },\n context\n )\n\n activityThread = await tx.getRepository(ActivityThread).save({\n ...activityThread,\n transaction: 'escalate',\n reason: comment,\n state: ActivityThreadStatus.Escalated,\n updater: user\n })\n } else {\n activityThread = await tx.getRepository(ActivityThread).save({\n ...activityThread,\n transaction: 'approve',\n reason: comment,\n state: ActivityThreadStatus.Ended,\n terminatedAt: new Date(),\n terminator: user,\n updater: user\n })\n }\n\n await updateActivityInstanceState(activityThread.activityInstanceId, { causedBy: [activityThread] }, context)\n\n return result\n}\n"]}
|
|
1
|
+
{"version":3,"file":"approve.js","sourceRoot":"","sources":["../../../server/controllers/activity-approval/approve.ts"],"names":[],"mappings":"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","sourcesContent":["import { ApprovalLineItem } from '@things-factory/organization'\nimport { stampedComment } from './opinion.js'\nimport { ActivityThread, ActivityThreadStatus } from '../../service/activity-thread/activity-thread.js'\nimport { ActivityApproval, ActivityApprovalJudgment } from '../../service/activity-approval/activity-approval.js'\nimport { updateActivityInstanceState } from '../common.js'\nimport { checkAuthority } from './check-authority.js'\nimport { isReferenceOrder, nextApproval, referencesToNotify } from './approval-line.js'\nimport { createApproval, createReferences } from './create-approval.js'\nimport { claimPendingApproval } from './claim.js'\nimport { noticeApproval } from '../../service/approval-notice/approval-notice.js'\n\nexport async function approve(\n { id, comment }: { id: string; comment: string },\n context: ResolverContext\n): Promise<ActivityApproval> {\n const { domain, user, tx } = context.state\n\n var repository = tx.getRepository(ActivityApproval)\n\n var activityApproval = await repository.findOne({\n where: { domain: { id: domain.id }, id }\n })\n\n if (!activityApproval) {\n throw new Error(\n context.t('error.activity-approval not found', {\n activityApproval: id\n })\n )\n }\n\n await checkAuthority(activityApproval, context)\n\n /* \n Prerequisite of Approval\n - Previous judgment should be 'pending'\n */\n if (activityApproval.judgment !== ActivityApprovalJudgment.Pending) {\n throw new Error(\n context.t(`error.activity-approval is already terminated`, {\n activityApproval: id,\n actual: activityApproval.judgment\n })\n )\n }\n\n var activityThread = await tx.getRepository(ActivityThread).findOne({\n where: { domain: { id: domain.id }, id: activityApproval.activityThreadId },\n relations: ['activityInstance']\n })\n\n /*\n * 여기까지 오면 스레드가 있어야 한다. 없는 채로 아래를 읽으면 `TypeError` 가 나고, 그 오류로는\n * 무엇이 없는지 알 수 없다.\n */\n if (!activityThread) {\n throw new Error(context.t('error.activity-thread not found', { activityThread: activityApproval.activityThreadId }))\n }\n\n if (!activityThread.activityInstance) {\n throw new Error(context.t('error.activity-instance not found', { activityThread: activityThread.id }))\n }\n\n const approvalLine: ApprovalLineItem[] = activityThread.activityInstance.approvalLine\n const order = activityApproval.order\n\n /*\n * A reference does not judge.\n *\n * The three checks above all pass for a reference row: it exists, checkAuthority says it belongs\n * to this user, and its judgment is still Pending. Without this refusal the approval is recorded\n * as real and nextApproval() then picks the approver sitting after that seat, moving the approval\n * a step forward; with no approver after it, one reference ends the thread.\n *\n * This stands before claimPendingApproval, the first write. Throwing after the claim would leave\n * the row already stamped with a judgment.\n *\n * The message says what this person can do instead of reporting a missing permission. A reference\n * seat is a different seat, not a blocked one — comments are theirs to leave.\n */\n if (isReferenceOrder(approvalLine, order)) {\n throw new Error(context.t('error.activity-approval is reference only'))\n }\n\n /* 다음 결재자가 누구인지는 한 자리가 정한다 — `approval-line.ts`. */\n const next = nextApproval(approvalLine, order)\n\n /*\n * **여기서 잡는다.** 위의 검사는 읽은 시점의 것이라 그 사이에 남이 처리할 수 있다. 잡기가 첫 쓰기여야\n * 진 쪽이 다음 결재자를 만들기 전에 물러난다.\n */\n if (!(await claimPendingApproval(tx, activityApproval, ActivityApprovalJudgment.Approved))) {\n /* 「이미 끝났다」보다 **그 사람이 할 일**을 말한다 — 목록을 새로 읽으면 지금 상태가 보인다. */\n throw new Error(context.t('error.activity-approval taken by someone else', { activityApproval: id }))\n }\n\n /*\n * **승인한 사람은 승인한 것이다** (2026-08-31, 아키텍트 확인).\n *\n * 앞서는 다음 결재자가 있으면 `Escalated` 로 적었다. 그러면 「누가 승인했나」를 물을 때 중간 결재자가\n * 전부 「상신함」으로 나오고, **승인한 것과 그냥 넘긴 것이 구별되지 않는다.** 규제 산업에서는 결재\n * 이력이 곧 근거라 이것이 결함이다.\n *\n * 다음으로 넘어간 사실은 `transaction` 이 그대로 들고 있으므로 잃는 것이 없다.\n */\n const result = await tx.getRepository(ActivityApproval).save({\n ...activityApproval,\n judgment: ActivityApprovalJudgment.Approved,\n ...stampedComment(comment),\n transaction: next ? 'escalate' : 'approve',\n /*\n * **누가 처리했는지 남긴다.**\n *\n * 역할로 걸린 결재는 `approver` 가 비어 있고 `approverRole` 만 있다. 처리해도 그 자리가 비어 있으면\n * 두 가지가 함께 무너진다 — 그 역할을 가진 사람들의 **대기 목록에 영원히 남고**(`approver IS NULL`\n * 로 걸리므로), 정작 처리한 사람의 **완료 목록에는 안 나온다**(`approver = 나` 로 거르므로).\n *\n * 그리고 결재 이력에서 「누가 승인했나」는 역할이 아니라 사람이어야 한다.\n */\n approver: activityApproval.approver ?? user,\n updater: user,\n terminatedAt: new Date(),\n terminator: user\n })\n\n /*\n * **이번 결재가 끝나서 알릴 참조들.** `notifyAfter` 가 이 차례를 가리키는 참조가 여기 온다.\n *\n * 다음 결재자를 세우는 것과 **무관하게** 만든다 — 참조는 막지 않으므로, 이것이 실패해도 결재는 간다.\n */\n const refs = await createReferences(\n { references: referencesToNotify(approvalLine, order), activityThread },\n context\n )\n for (const f of refs.failed) {\n console.warn(`[worklist] 참조자를 세우지 못했다 (결재선 ${f.order}번째) — ${f.reason}`)\n }\n\n if (next) {\n /* 다음 결재자에게 넘긴다 — 행을 만드는 자리는 한 곳이다(§`create-approval.ts`). */\n await createApproval(\n { item: next.item, order: next.order, participation: 'approval', activityThread, transaction: 'escalate' },\n context\n )\n\n activityThread = await tx.getRepository(ActivityThread).save({\n ...activityThread,\n transaction: 'escalate',\n reason: comment,\n state: ActivityThreadStatus.Escalated,\n updater: user\n })\n } else {\n activityThread = await tx.getRepository(ActivityThread).save({\n ...activityThread,\n transaction: 'approve',\n reason: comment,\n state: ActivityThreadStatus.Ended,\n terminatedAt: new Date(),\n terminator: user,\n updater: user\n })\n\n /* The last approval ended it - the requester is told (ADR-0164). The next approver's turn is told where its row is made. */\n await noticeApproval(context, { kind: 'approved', approvalId: result.id })\n }\n\n await updateActivityInstanceState(activityThread.activityInstanceId, { causedBy: [activityThread] }, context)\n\n return result\n}\n"]}
|
|
@@ -17,6 +17,7 @@ const organization_1 = require("@things-factory/organization");
|
|
|
17
17
|
const activity_approval_js_1 = require("../../service/activity-approval/activity-approval.js");
|
|
18
18
|
const common_js_1 = require("../common.js");
|
|
19
19
|
const approval_line_standing_js_1 = require("../../service/work-door/approval-line-standing.js");
|
|
20
|
+
const approval_notice_js_1 = require("../../service/approval-notice/approval-notice.js");
|
|
20
21
|
/**
|
|
21
22
|
* 결재선 항목 하나로 행을 만든다.
|
|
22
23
|
*
|
|
@@ -46,13 +47,18 @@ async function createApproval(args, context) {
|
|
|
46
47
|
if (!approverRole) {
|
|
47
48
|
throw new Error(context.t('error.approver role not found', { role: roleId }));
|
|
48
49
|
}
|
|
49
|
-
return await tx.getRepository(activity_approval_js_1.ActivityApproval).save({ ...base, approverRole });
|
|
50
|
+
return await told(await tx.getRepository(activity_approval_js_1.ActivityApproval).save({ ...base, approverRole }), participation, context);
|
|
50
51
|
}
|
|
51
52
|
const approver = await (0, common_js_1.getSystemUserFromOrgMemberItem)(item, context);
|
|
52
53
|
if (!approver) {
|
|
53
54
|
throw new Error(context.t('error.approver is not set on the approval line', { order }));
|
|
54
55
|
}
|
|
55
|
-
return await tx.getRepository(activity_approval_js_1.ActivityApproval).save({ ...base, approver });
|
|
56
|
+
return await told(await tx.getRepository(activity_approval_js_1.ActivityApproval).save({ ...base, approver }), participation, context);
|
|
57
|
+
}
|
|
58
|
+
/** The seat is told its turn - every row made here is one (ADR-0164). */
|
|
59
|
+
async function told(approval, participation, context) {
|
|
60
|
+
await (0, approval_notice_js_1.noticeApproval)(context, { kind: participation === 'reference' ? 'reference' : 'request', approvalId: approval.id });
|
|
61
|
+
return approval;
|
|
56
62
|
}
|
|
57
63
|
/**
|
|
58
64
|
* 이번에 알려야 할 참조들의 행을 만든다 — **하나가 실패해도 결재를 세우지 않는다.**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create-approval.js","sourceRoot":"","sources":["../../../server/controllers/activity-approval/create-approval.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"create-approval.js","sourceRoot":"","sources":["../../../server/controllers/activity-approval/create-approval.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * **결재선 한 항목을 실제 행으로 만든다** — 한 자리.\n *\n * ── 왜 뺐나 (2026-09-01) ─────────────────────────────────────────────────\n * 같은 열다섯 줄이 두 파일에 있었다 — `submit.ts`(첫 결재자)와 `approve.ts`(다음 결재자). 역할로 걸린\n * 경우와 사람으로 걸린 경우를 가르고, 못 찾으면 던지고, 행을 만든다.\n *\n * 참조까지 넣으면 **세 벌**이 된다. 그때 한쪽만 고쳐지는 날이 오고, 그러면 어떤 자격의 결재자만\n * 조용히 안 만들어진다 — 오류가 안 나고 결재가 안 걸린다.\n */\nimport { Role } from '@things-factory/auth-base'\nimport type { ApprovalLineItem } from '@things-factory/organization'\nimport { OrgMemberTargetType } from '@things-factory/organization'\n\nimport { ActivityApproval, ActivityApprovalJudgment } from '../../service/activity-approval/activity-approval.js'\nimport type { ActivityThread } from '../../service/activity-thread/activity-thread.js'\nimport { getSystemUserFromOrgMemberItem } from '../common.js'\nimport { seatRoleId } from '../../service/work-door/approval-line-standing.js'\nimport { noticeApproval } from '../../service/approval-notice/approval-notice.js'\n\n/** 이 항목이 무슨 자격으로 서나 — 행에도 적는다(목록 질의가 참조를 걸러야 한다). */\nexport type Participation = 'approval' | 'reference'\n\n/**\n * 결재선 항목 하나로 행을 만든다.\n *\n * 역할로 걸린 것은 `approverRole` 만 채워지고 `approver` 는 비어 있다 — 그 역할을 가진 사람 여럿이 같은\n * 건을 본다. 처리하는 사람이 정해지는 것은 누른 순간이다(§`approve.ts`).\n */\nexport async function createApproval(\n args: {\n item: ApprovalLineItem\n order: number\n participation: Participation\n activityThread: ActivityThread\n /** 이 행이 왜 생겼나 — `submit` · `escalate` · `notify`. */\n transaction: string\n },\n context: ResolverContext\n): Promise<ActivityApproval> {\n const { domain, user, tx } = context.state\n const { item, order, participation, activityThread, transaction } = args\n\n const base = {\n domain,\n activityThread,\n round: activityThread.round,\n order,\n judgment: ActivityApprovalJudgment.Pending,\n participation,\n transaction,\n creator: user,\n updater: user\n }\n\n if (item.type == OrgMemberTargetType.Role) {\n const roleId = seatRoleId(item)\n if (!roleId) {\n throw new Error(context.t('error.approver role is not set on the approval line', { order }))\n }\n\n const approverRole = await tx.getRepository(Role).findOneBy({ id: roleId })\n if (!approverRole) {\n throw new Error(context.t('error.approver role not found', { role: roleId }))\n }\n\n return await told(await tx.getRepository(ActivityApproval).save({ ...base, approverRole } as any), participation, context)\n }\n\n const approver = await getSystemUserFromOrgMemberItem(item, context)\n if (!approver) {\n throw new Error(context.t('error.approver is not set on the approval line', { order }))\n }\n\n return await told(await tx.getRepository(ActivityApproval).save({ ...base, approver } as any), participation, context)\n}\n\n/** The seat is told its turn - every row made here is one (ADR-0164). */\nasync function told(approval: ActivityApproval, participation: Participation, context: ResolverContext): Promise<ActivityApproval> {\n await noticeApproval(context, { kind: participation === 'reference' ? 'reference' : 'request', approvalId: approval.id })\n return approval\n}\n\n/**\n * 이번에 알려야 할 참조들의 행을 만든다 — **하나가 실패해도 결재를 세우지 않는다.**\n *\n * 참조는 막지 않는 자격이다. 그런데 참조자를 못 찾아 던지면 그 예외가 상신·승인을 통째로 물린다 —\n * 막지 않기로 한 것이 막게 된다.\n *\n * 그래서 실패를 삼키지 않되 **던지지도 않는다.** 못 만든 참조를 이유와 함께 돌려주고, 부르는 쪽이\n * 그것을 기록한다.\n */\nexport async function createReferences(\n args: {\n references: { item: ApprovalLineItem; order: number }[]\n activityThread: ActivityThread\n },\n context: ResolverContext\n): Promise<{ created: ActivityApproval[]; failed: { order: number; reason: string }[] }> {\n const created: ActivityApproval[] = []\n const failed: { order: number; reason: string }[] = []\n\n for (const { item, order } of args.references) {\n try {\n created.push(\n await createApproval(\n { item, order, participation: 'reference', activityThread: args.activityThread, transaction: 'notify' },\n context\n )\n )\n } catch (e: any) {\n failed.push({ order, reason: String(e?.message ?? e) })\n }\n }\n\n return { created, failed }\n}\n"]}
|
|
@@ -7,6 +7,7 @@ const activity_approval_js_1 = require("../../service/activity-approval/activity
|
|
|
7
7
|
const check_authority_js_1 = require("./check-authority.js");
|
|
8
8
|
const claim_js_1 = require("./claim.js");
|
|
9
9
|
const delegation_js_1 = require("./delegation.js");
|
|
10
|
+
const approval_notice_js_1 = require("../../service/approval-notice/approval-notice.js");
|
|
10
11
|
/**
|
|
11
12
|
* 결재를 다른 사람에게 넘긴다.
|
|
12
13
|
*
|
|
@@ -76,7 +77,7 @@ async function delegate({ id, to, comment }, context) {
|
|
|
76
77
|
* `id` 를 퍼뜨리지 않는다 — 퍼뜨리면 원본을 고치는 것이 되고 새 행이 안 생긴다. 차례(`order`)와
|
|
77
78
|
* 차수(`round`)는 같다. 결재선의 같은 단계이고 사람만 바뀐 것이다.
|
|
78
79
|
*/
|
|
79
|
-
await repository.save(repository.create({
|
|
80
|
+
const handedOn = await repository.save(repository.create({
|
|
80
81
|
domain: activityApproval.domain,
|
|
81
82
|
activityThread: activityApproval.activityThread,
|
|
82
83
|
originalApproval: activityApproval,
|
|
@@ -95,6 +96,8 @@ async function delegate({ id, to, comment }, context) {
|
|
|
95
96
|
creator: user,
|
|
96
97
|
updater: user
|
|
97
98
|
}));
|
|
99
|
+
/* The receiver is told the seat is theirs now (ADR-0164). */
|
|
100
|
+
await (0, approval_notice_js_1.noticeApproval)(context, { kind: 'delegated', approvalId: handedOn.id });
|
|
98
101
|
/* 넘긴 사람의 행을 끝낸다. 의견은 `comment` 에 남긴다 — `reason` 은 이 표에 없는 칸이다. */
|
|
99
102
|
const result = await repository.save({
|
|
100
103
|
...activityApproval,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"delegate.js","sourceRoot":"","sources":["../../../server/controllers/activity-approval/delegate.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"delegate.js","sourceRoot":"","sources":["../../../server/controllers/activity-approval/delegate.ts"],"names":[],"mappings":"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","sourcesContent":["import { User, UserStatus } from '@things-factory/auth-base'\nimport { stampedComment } from './opinion.js'\nimport { ObjectRef } from '@things-factory/shell'\nimport { ActivityApproval, ActivityApprovalJudgment } from '../../service/activity-approval/activity-approval.js'\nimport { checkAuthority } from './check-authority.js'\nimport { claimPendingApproval } from './claim.js'\nimport { DELEGATION_REFUSAL_KEYS, handOverRefusal } from './delegation.js'\nimport { noticeApproval } from '../../service/approval-notice/approval-notice.js'\n\n/**\n * 결재를 다른 사람에게 넘긴다.\n *\n * ── 넘긴 사람의 행은 끝내고, 받은 사람의 행을 새로 만든다 ──────────────────\n * 두 행이 필요하다. 넘긴 기록이 남아야 「누가 왜 넘겼나」에 답할 수 있고, 받은 사람에게 대기 건이\n * 서야 결재가 이어진다.\n *\n * 앞서는 두 번 저장하면서 **둘 다 같은 행을 덮었다.** `{ ...activityApproval }` 이 `id` 까지 퍼뜨려서\n * TypeORM 이 새로 넣지 않고 고쳤기 때문이다. 그래서 위임하면\n *\n * 받은 사람에게 대기 건이 안 생기고\n * 원본은 `delegated` 로 끝나고\n * 스레드는 `submitted` 인 채로 멈춘다\n *\n * **결재가 죽었다.** 오류는 안 났다.\n */\nexport async function delegate(\n { id, to, comment }: { id: string; to: ObjectRef; comment: string },\n context: ResolverContext\n): Promise<ActivityApproval> {\n const { domain, user, tx } = context.state\n\n var repository = tx.getRepository(ActivityApproval)\n\n var activityApproval = await repository.findOne({\n where: { domain: { id: domain.id }, id },\n relations: ['domain', 'activityThread']\n })\n\n if (!activityApproval) {\n throw new Error(\n context.t('error.activity-approval not found', {\n activityApproval: id\n })\n )\n }\n\n await checkAuthority(activityApproval, context)\n\n /*\n Prerequisite of Delegation\n - Previous judgment should be 'pending'\n */\n if (activityApproval.judgment !== ActivityApprovalJudgment.Pending) {\n throw new Error(\n context.t(`error.activity-approval is already terminated`, {\n activityInstance: id,\n actual: activityApproval.judgment\n })\n )\n }\n\n const approver = await tx.getRepository(User).findOne({\n where: { id: to.id, status: UserStatus.ACTIVATED },\n relations: ['domains']\n })\n\n if (!approver) {\n throw new Error(context.t(`error.user not found`))\n }\n\n /* 누구에게 넘길 수 있나 — 판단은 `delegation.ts` 가 한다(시험이 그 파일을 든다). */\n const refusal = handOverRefusal({\n fromUserId: user.id,\n toUserId: approver.id,\n toInDomain: (approver.domains || []).some(one => one.id === domain.id)\n })\n\n if (refusal) {\n throw Object.assign(new Error(context.t(DELEGATION_REFUSAL_KEYS[refusal])), { code: refusal })\n }\n\n\n /*\n * **여기서 잡는다.** 위의 검사는 읽은 시점의 것이라 그 사이에 남이 처리할 수 있다. 잡기가 첫 쓰기여야\n * 진 쪽이 다음 결재자를 만들기 전에 물러난다.\n */\n if (!(await claimPendingApproval(tx, activityApproval, ActivityApprovalJudgment.Delegated))) {\n /* 「이미 끝났다」보다 **그 사람이 할 일**을 말한다 — 목록을 새로 읽으면 지금 상태가 보인다. */\n throw new Error(context.t('error.activity-approval taken by someone else', { activityApproval: id }))\n }\n\n /*\n * 받은 사람의 대기 건을 **새로 만든다.**\n *\n * `id` 를 퍼뜨리지 않는다 — 퍼뜨리면 원본을 고치는 것이 되고 새 행이 안 생긴다. 차례(`order`)와\n * 차수(`round`)는 같다. 결재선의 같은 단계이고 사람만 바뀐 것이다.\n */\n const handedOn = await repository.save(\n repository.create({\n domain: activityApproval.domain,\n activityThread: activityApproval.activityThread,\n originalApproval: activityApproval,\n round: activityApproval.round,\n order: activityApproval.order,\n /*\n * The seat keeps what it is. Left out, an empty participation reads as approval, so a reference\n * handed on became a judging seat in the receiver's pending list that the line refused to let them\n * approve, and it left their reference list.\n */\n participation: activityApproval.participation,\n judgment: ActivityApprovalJudgment.Pending,\n transaction: 'delegate',\n ...stampedComment(comment),\n approver,\n creator: user,\n updater: user\n })\n )\n\n /* The receiver is told the seat is theirs now (ADR-0164). */\n await noticeApproval(context, { kind: 'delegated', approvalId: handedOn.id })\n\n /* 넘긴 사람의 행을 끝낸다. 의견은 `comment` 에 남긴다 — `reason` 은 이 표에 없는 칸이다. */\n const result = await repository.save({\n ...activityApproval,\n judgment: ActivityApprovalJudgment.Delegated,\n ...stampedComment(comment),\n transaction: 'delegate',\n updater: user,\n terminator: user,\n terminatedAt: new Date()\n })\n\n return result\n}\n"]}
|
|
@@ -8,6 +8,7 @@ const common_js_1 = require("../common.js");
|
|
|
8
8
|
const check_authority_js_1 = require("./check-authority.js");
|
|
9
9
|
const claim_js_1 = require("./claim.js");
|
|
10
10
|
const approval_line_js_1 = require("./approval-line.js");
|
|
11
|
+
const approval_notice_js_1 = require("../../service/approval-notice/approval-notice.js");
|
|
11
12
|
async function reject({ id, comment }, context) {
|
|
12
13
|
const { domain, user, tx } = context.state;
|
|
13
14
|
var repository = tx.getRepository(activity_approval_js_1.ActivityApproval);
|
|
@@ -84,6 +85,8 @@ async function reject({ id, comment }, context) {
|
|
|
84
85
|
state: activity_thread_js_1.ActivityThreadStatus.Rejected,
|
|
85
86
|
updater: user
|
|
86
87
|
});
|
|
88
|
+
/* The requester is told it came back (ADR-0164). */
|
|
89
|
+
await (0, approval_notice_js_1.noticeApproval)(context, { kind: 'rejected', approvalId: result.id });
|
|
87
90
|
await (0, common_js_1.updateActivityInstanceState)(activityApproval.activityThread.activityInstanceId, { causedBy: [activityThread] }, context);
|
|
88
91
|
return result;
|
|
89
92
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reject.js","sourceRoot":"","sources":["../../../server/controllers/activity-approval/reject.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"reject.js","sourceRoot":"","sources":["../../../server/controllers/activity-approval/reject.ts"],"names":[],"mappings":"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","sourcesContent":["import { ActivityThread, ActivityThreadStatus } from '../../service/activity-thread/activity-thread.js'\nimport { stampedComment } from './opinion.js'\nimport { ActivityApproval, ActivityApprovalJudgment } from '../../service/activity-approval/activity-approval.js'\nimport { updateActivityInstanceState } from '../common.js'\nimport { checkAuthority } from './check-authority.js'\nimport { claimPendingApproval } from './claim.js'\nimport { isReferenceOrder } from './approval-line.js'\nimport { noticeApproval } from '../../service/approval-notice/approval-notice.js'\n\nexport async function reject(\n { id, comment }: { id: string; comment: string },\n context: ResolverContext\n): Promise<ActivityApproval> {\n const { domain, user, tx } = context.state\n\n var repository = tx.getRepository(ActivityApproval)\n\n var activityApproval = await repository.findOne({\n where: { domain: { id: domain.id }, id },\n /*\n * The approval line comes along, because the refusal below asks the line whether this seat is a\n * reference. approve() reads the thread in a separate query to reach the same value; this\n * function already holds the thread, so it takes one more level instead.\n */\n relations: ['activityThread', 'activityThread.activityInstance']\n })\n\n if (!activityApproval) {\n throw new Error(\n context.t('error.activity-approval not found', {\n activityApproval: id\n })\n )\n }\n\n await checkAuthority(activityApproval, context)\n\n /* \n Prerequisite of Rejection\n - Previous judgment should be 'pending'\n */\n if (activityApproval.judgment !== ActivityApprovalJudgment.Pending) {\n throw new Error(\n context.t(`error.activity-approval is already terminated`, {\n activityInstance: id,\n actual: activityApproval.judgment\n })\n )\n }\n\n\n /*\n * A reference does not judge, and rejecting carries more than approving.\n *\n * This function did not look at the approval line at all. A reference pressing reject had it\n * recorded as a real rejection and the thread closed as Rejected. Activities that set\n * rejectionIsFinal — the twin actuation and the disposition approval — cannot be restarted, so\n * someone seated to comment closed the case for good.\n *\n * This stands before claimPendingApproval, the first write. Throwing after the claim would leave\n * the row already stamped as rejected.\n */\n if (isReferenceOrder(activityApproval.activityThread?.activityInstance?.approvalLine, activityApproval.order)) {\n throw new Error(context.t('error.activity-approval is reference only'))\n }\n\n /*\n * **여기서 잡는다.** 위의 검사는 읽은 시점의 것이라 그 사이에 남이 처리할 수 있다. 잡기가 첫 쓰기여야\n * 진 쪽이 다음 결재자를 만들기 전에 물러난다.\n */\n if (!(await claimPendingApproval(tx, activityApproval, ActivityApprovalJudgment.Rejected))) {\n /* 「이미 끝났다」보다 **그 사람이 할 일**을 말한다 — 목록을 새로 읽으면 지금 상태가 보인다. */\n throw new Error(context.t('error.activity-approval taken by someone else', { activityApproval: id }))\n }\n\n var result = await repository.save({\n ...activityApproval,\n judgment: ActivityApprovalJudgment.Rejected,\n ...stampedComment(comment),\n transaction: 'reject',\n /*\n * **누가 처리했는지 남긴다.**\n *\n * 역할로 걸린 결재는 `approver` 가 비어 있고 `approverRole` 만 있다. 처리해도 그 자리가 비어 있으면\n * 두 가지가 함께 무너진다 — 그 역할을 가진 사람들의 **대기 목록에 영원히 남고**(`approver IS NULL`\n * 로 걸리므로), 정작 처리한 사람의 **완료 목록에는 안 나온다**(`approver = 나` 로 거르므로).\n *\n * 그리고 결재 이력에서 「누가 승인했나」는 역할이 아니라 사람이어야 한다.\n */\n approver: activityApproval.approver ?? user,\n updater: user,\n terminatedAt: new Date(),\n terminator: user\n })\n\n var activityThread = await tx.getRepository(ActivityThread).save({\n ...activityApproval.activityThread,\n transaction: 'reject',\n reason: comment,\n state: ActivityThreadStatus.Rejected,\n updater: user\n })\n\n /* The requester is told it came back (ADR-0164). */\n await noticeApproval(context, { kind: 'rejected', approvalId: result.id })\n\n await updateActivityInstanceState(\n activityApproval.activityThread.activityInstanceId,\n { causedBy: [activityThread] },\n context\n )\n\n return result\n}\n"]}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { NoticeWords } from '@things-factory/notification';
|
|
2
|
+
import type { ApprovalLineItem } from '@things-factory/organization';
|
|
3
|
+
/**
|
|
4
|
+
* What happened.
|
|
5
|
+
*
|
|
6
|
+
* - `request` a seat's turn came - the first one on submit, the next one after an approval
|
|
7
|
+
* - `delegated` a seat was handed to someone
|
|
8
|
+
* - `reference` someone was seated to read along
|
|
9
|
+
* - `approved` the last approval ended the case
|
|
10
|
+
* - `rejected` an approver sent it back
|
|
11
|
+
*/
|
|
12
|
+
export type ApprovalNoticeKind = 'request' | 'delegated' | 'reference' | 'approved' | 'rejected';
|
|
13
|
+
export declare const APPROVAL_NOTICE_KINDS: readonly ApprovalNoticeKind[];
|
|
14
|
+
/** A notice for the person whose turn it is - the other kinds tell the requester of a judgment. */
|
|
15
|
+
export declare function isTurnNotice(kind: ApprovalNoticeKind): boolean;
|
|
16
|
+
export type NoticePerson = {
|
|
17
|
+
id: string;
|
|
18
|
+
name?: string | null;
|
|
19
|
+
email?: string | null;
|
|
20
|
+
locale?: string | null;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Who is told.
|
|
24
|
+
*
|
|
25
|
+
* A turn reaches the seat: its person, or - for a seat held by a role - everyone holding the role. A judgment
|
|
26
|
+
* reaches the requester. Whoever did the thing is not told of it, and nobody is told twice.
|
|
27
|
+
*/
|
|
28
|
+
export declare function noticeReceivers({ kind, approver, roleHolders, requester, actorId }: {
|
|
29
|
+
kind: ApprovalNoticeKind;
|
|
30
|
+
approver?: NoticePerson | null;
|
|
31
|
+
roleHolders?: NoticePerson[] | null;
|
|
32
|
+
requester?: NoticePerson | null;
|
|
33
|
+
actorId?: string | null;
|
|
34
|
+
}): NoticePerson[];
|
|
35
|
+
/**
|
|
36
|
+
* The page the notice opens, relative to the domain - the same pages the worklist lists open.
|
|
37
|
+
*
|
|
38
|
+
* A turn opens the approval. An approved case opens the thread's read-only view; a rejected one opens the thread
|
|
39
|
+
* itself, where the requester revises and raises it again.
|
|
40
|
+
*/
|
|
41
|
+
export declare function noticeAddress(kind: ApprovalNoticeKind, { approvalId, threadId }: {
|
|
42
|
+
approvalId: string;
|
|
43
|
+
threadId: string;
|
|
44
|
+
}): string;
|
|
45
|
+
/** Which approval of the line this seat is - references take a place in the line but are not counted. */
|
|
46
|
+
export declare function approvalStep(line: ApprovalLineItem[] | null | undefined, order: number): {
|
|
47
|
+
step: number;
|
|
48
|
+
steps: number;
|
|
49
|
+
} | undefined;
|
|
50
|
+
export type NoticeFacts = {
|
|
51
|
+
kind: ApprovalNoticeKind;
|
|
52
|
+
/** The case as the worklist names it. */
|
|
53
|
+
name: string;
|
|
54
|
+
requester?: string | null;
|
|
55
|
+
/** Who did the thing - the approver who judged, the person who handed a seat on. */
|
|
56
|
+
actor?: string | null;
|
|
57
|
+
step?: number;
|
|
58
|
+
steps?: number;
|
|
59
|
+
};
|
|
60
|
+
export type Translate = (key: string, options?: {
|
|
61
|
+
[name: string]: unknown;
|
|
62
|
+
}) => string;
|
|
63
|
+
/**
|
|
64
|
+
* The sentences, from the worklist translations (`approval-notice.<kind>.*`). Nothing of the request's content goes
|
|
65
|
+
* in: no input, no amount, no comment. The page behind the link shows those to whoever may see them.
|
|
66
|
+
*/
|
|
67
|
+
export declare function noticeWords(t: Translate, facts: NoticeFacts): NoticeWords;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.APPROVAL_NOTICE_KINDS = void 0;
|
|
4
|
+
exports.isTurnNotice = isTurnNotice;
|
|
5
|
+
exports.noticeReceivers = noticeReceivers;
|
|
6
|
+
exports.noticeAddress = noticeAddress;
|
|
7
|
+
exports.approvalStep = approvalStep;
|
|
8
|
+
exports.noticeWords = noticeWords;
|
|
9
|
+
const approval_line_js_1 = require("../../controllers/activity-approval/approval-line.js");
|
|
10
|
+
exports.APPROVAL_NOTICE_KINDS = ['request', 'delegated', 'reference', 'approved', 'rejected'];
|
|
11
|
+
/** A notice for the person whose turn it is - the other kinds tell the requester of a judgment. */
|
|
12
|
+
function isTurnNotice(kind) {
|
|
13
|
+
return kind === 'request' || kind === 'delegated' || kind === 'reference';
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Who is told.
|
|
17
|
+
*
|
|
18
|
+
* A turn reaches the seat: its person, or - for a seat held by a role - everyone holding the role. A judgment
|
|
19
|
+
* reaches the requester. Whoever did the thing is not told of it, and nobody is told twice.
|
|
20
|
+
*/
|
|
21
|
+
function noticeReceivers({ kind, approver, roleHolders, requester, actorId }) {
|
|
22
|
+
const candidates = isTurnNotice(kind) ? (approver ? [approver] : roleHolders || []) : requester ? [requester] : [];
|
|
23
|
+
const seen = new Set();
|
|
24
|
+
return candidates.filter(person => {
|
|
25
|
+
if (!person?.id || person.id === actorId || seen.has(person.id))
|
|
26
|
+
return false;
|
|
27
|
+
seen.add(person.id);
|
|
28
|
+
return true;
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The page the notice opens, relative to the domain - the same pages the worklist lists open.
|
|
33
|
+
*
|
|
34
|
+
* A turn opens the approval. An approved case opens the thread's read-only view; a rejected one opens the thread
|
|
35
|
+
* itself, where the requester revises and raises it again.
|
|
36
|
+
*/
|
|
37
|
+
function noticeAddress(kind, { approvalId, threadId }) {
|
|
38
|
+
if (isTurnNotice(kind))
|
|
39
|
+
return `activity-approval/${approvalId}`;
|
|
40
|
+
return kind === 'approved' ? `activity-thread-view/${threadId}` : `activity-thread/${threadId}`;
|
|
41
|
+
}
|
|
42
|
+
/** Which approval of the line this seat is - references take a place in the line but are not counted. */
|
|
43
|
+
function approvalStep(line, order) {
|
|
44
|
+
const orders = (line || []).map((item, index) => ((0, approval_line_js_1.isApprovalStep)(item) ? index + 1 : 0)).filter(Boolean);
|
|
45
|
+
const step = orders.indexOf(order) + 1;
|
|
46
|
+
return step ? { step, steps: orders.length } : undefined;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The sentences, from the worklist translations (`approval-notice.<kind>.*`). Nothing of the request's content goes
|
|
50
|
+
* in: no input, no amount, no comment. The page behind the link shows those to whoever may see them.
|
|
51
|
+
*/
|
|
52
|
+
function noticeWords(t, facts) {
|
|
53
|
+
const values = {
|
|
54
|
+
name: facts.name,
|
|
55
|
+
requester: facts.requester || '-',
|
|
56
|
+
actor: facts.actor || '-',
|
|
57
|
+
step: facts.step ?? '-',
|
|
58
|
+
steps: facts.steps ?? '-'
|
|
59
|
+
};
|
|
60
|
+
return {
|
|
61
|
+
subject: t(`approval-notice.${facts.kind}.subject`, values),
|
|
62
|
+
body: t(`approval-notice.${facts.kind}.body`, values),
|
|
63
|
+
action: t(isTurnNotice(facts.kind) ? 'approval-notice.open-approval' : 'approval-notice.open-request', values),
|
|
64
|
+
note: t(isTurnNotice(facts.kind) ? 'approval-notice.see-approval-box' : 'approval-notice.see-request-box', values)
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=approval-notice-rules.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"approval-notice-rules.js","sourceRoot":"","sources":["../../../server/service/approval-notice/approval-notice-rules.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * The rules of the approval notice (ADR-0164) - who is told and what they read. How it is sent is notification's.\n * Plain functions over plain values: no entity, no request, so a test holds every rule without a database.\n */\nimport type { NoticeWords } from '@things-factory/notification'\nimport type { ApprovalLineItem } from '@things-factory/organization'\n\nimport { isApprovalStep } from '../../controllers/activity-approval/approval-line.js'\n\n/**\n * What happened.\n *\n * - `request` a seat's turn came - the first one on submit, the next one after an approval\n * - `delegated` a seat was handed to someone\n * - `reference` someone was seated to read along\n * - `approved` the last approval ended the case\n * - `rejected` an approver sent it back\n */\nexport type ApprovalNoticeKind = 'request' | 'delegated' | 'reference' | 'approved' | 'rejected'\n\nexport const APPROVAL_NOTICE_KINDS: readonly ApprovalNoticeKind[] = ['request', 'delegated', 'reference', 'approved', 'rejected']\n\n/** A notice for the person whose turn it is - the other kinds tell the requester of a judgment. */\nexport function isTurnNotice(kind: ApprovalNoticeKind): boolean {\n return kind === 'request' || kind === 'delegated' || kind === 'reference'\n}\n\nexport type NoticePerson = { id: string; name?: string | null; email?: string | null; locale?: string | null }\n\n/**\n * Who is told.\n *\n * A turn reaches the seat: its person, or - for a seat held by a role - everyone holding the role. A judgment\n * reaches the requester. Whoever did the thing is not told of it, and nobody is told twice.\n */\nexport function noticeReceivers({\n kind,\n approver,\n roleHolders,\n requester,\n actorId\n}: {\n kind: ApprovalNoticeKind\n approver?: NoticePerson | null\n roleHolders?: NoticePerson[] | null\n requester?: NoticePerson | null\n actorId?: string | null\n}): NoticePerson[] {\n const candidates = isTurnNotice(kind) ? (approver ? [approver] : roleHolders || []) : requester ? [requester] : []\n const seen = new Set<string>()\n\n return candidates.filter(person => {\n if (!person?.id || person.id === actorId || seen.has(person.id)) return false\n seen.add(person.id)\n return true\n })\n}\n\n/**\n * The page the notice opens, relative to the domain - the same pages the worklist lists open.\n *\n * A turn opens the approval. An approved case opens the thread's read-only view; a rejected one opens the thread\n * itself, where the requester revises and raises it again.\n */\nexport function noticeAddress(kind: ApprovalNoticeKind, { approvalId, threadId }: { approvalId: string; threadId: string }): string {\n if (isTurnNotice(kind)) return `activity-approval/${approvalId}`\n return kind === 'approved' ? `activity-thread-view/${threadId}` : `activity-thread/${threadId}`\n}\n\n/** Which approval of the line this seat is - references take a place in the line but are not counted. */\nexport function approvalStep(line: ApprovalLineItem[] | null | undefined, order: number): { step: number; steps: number } | undefined {\n const orders = (line || []).map((item, index) => (isApprovalStep(item) ? index + 1 : 0)).filter(Boolean)\n const step = orders.indexOf(order) + 1\n return step ? { step, steps: orders.length } : undefined\n}\n\n\nexport type NoticeFacts = {\n kind: ApprovalNoticeKind\n /** The case as the worklist names it. */\n name: string\n requester?: string | null\n /** Who did the thing - the approver who judged, the person who handed a seat on. */\n actor?: string | null\n step?: number\n steps?: number\n}\n\nexport type Translate = (key: string, options?: { [name: string]: unknown }) => string\n\n/**\n * The sentences, from the worklist translations (`approval-notice.<kind>.*`). Nothing of the request's content goes\n * in: no input, no amount, no comment. The page behind the link shows those to whoever may see them.\n */\nexport function noticeWords(t: Translate, facts: NoticeFacts): NoticeWords {\n const values = {\n name: facts.name,\n requester: facts.requester || '-',\n actor: facts.actor || '-',\n step: facts.step ?? '-',\n steps: facts.steps ?? '-'\n }\n\n return {\n subject: t(`approval-notice.${facts.kind}.subject`, values),\n body: t(`approval-notice.${facts.kind}.body`, values),\n action: t(isTurnNotice(facts.kind) ? 'approval-notice.open-approval' : 'approval-notice.open-request', values),\n note: t(isTurnNotice(facts.kind) ? 'approval-notice.see-approval-box' : 'approval-notice.see-request-box', values)\n }\n}\n"]}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type ApprovalNoticeKind } from './approval-notice-rules.js';
|
|
2
|
+
/**
|
|
3
|
+
* Tells of what just happened to approval `approvalId`, once the transaction has committed.
|
|
4
|
+
*
|
|
5
|
+
* Who and what are settled here, inside the transaction, from rows it can see. Nothing here throws: a notice that
|
|
6
|
+
* cannot be made is reported and the approval stands.
|
|
7
|
+
*/
|
|
8
|
+
export declare function noticeApproval(context: ResolverContext, { kind, approvalId }: {
|
|
9
|
+
kind: ApprovalNoticeKind;
|
|
10
|
+
approvalId: string;
|
|
11
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.noticeApproval = noticeApproval;
|
|
4
|
+
/*
|
|
5
|
+
* The approval notice (ADR-0164) - the one place the worklist tells people of an approval's turn and judgment.
|
|
6
|
+
*
|
|
7
|
+
* The four places an approval moves call `noticeApproval`: createApproval (a turn or a reference), delegate (a
|
|
8
|
+
* handed seat), approve with no one after (approved) and reject (rejected). Applications call nothing; a case that
|
|
9
|
+
* goes through the approval line is told of. Sending is notification's (`sendNoticeWhenCommitted`).
|
|
10
|
+
*/
|
|
11
|
+
const auth_base_1 = require("@things-factory/auth-base");
|
|
12
|
+
const env_1 = require("@things-factory/env");
|
|
13
|
+
const translator_1 = require("@things-factory/i18n-base/translator");
|
|
14
|
+
const notification_1 = require("@things-factory/notification");
|
|
15
|
+
const activity_approval_js_1 = require("../activity-approval/activity-approval.js");
|
|
16
|
+
const assignee_pool_js_1 = require("../activity-instance/assignee-pool.js");
|
|
17
|
+
const approval_notice_rules_js_1 = require("./approval-notice-rules.js");
|
|
18
|
+
/**
|
|
19
|
+
* Tells of what just happened to approval `approvalId`, once the transaction has committed.
|
|
20
|
+
*
|
|
21
|
+
* Who and what are settled here, inside the transaction, from rows it can see. Nothing here throws: a notice that
|
|
22
|
+
* cannot be made is reported and the approval stands.
|
|
23
|
+
*/
|
|
24
|
+
async function noticeApproval(context, { kind, approvalId }) {
|
|
25
|
+
try {
|
|
26
|
+
const { user, tx } = context.state;
|
|
27
|
+
const approval = await tx.getRepository(activity_approval_js_1.ActivityApproval).findOne({
|
|
28
|
+
where: { id: approvalId },
|
|
29
|
+
relations: ['approver', 'approverRole', 'approverRole.users', 'activityThread', 'activityThread.assignee', 'activityThread.activityInstance']
|
|
30
|
+
});
|
|
31
|
+
const thread = approval?.activityThread;
|
|
32
|
+
const instance = thread?.activityInstance;
|
|
33
|
+
if (!approval || !thread || !instance)
|
|
34
|
+
return;
|
|
35
|
+
const receivers = (0, approval_notice_rules_js_1.noticeReceivers)({
|
|
36
|
+
kind,
|
|
37
|
+
approver: active(approval.approver),
|
|
38
|
+
roleHolders: (0, assignee_pool_js_1.poolMembers)(approval.approverRole ? [approval.approverRole] : []).map(active).filter(Boolean),
|
|
39
|
+
requester: active(thread.assignee),
|
|
40
|
+
actorId: user?.id
|
|
41
|
+
});
|
|
42
|
+
if (!receivers.length)
|
|
43
|
+
return;
|
|
44
|
+
const facts = {
|
|
45
|
+
kind,
|
|
46
|
+
name: instance.name || '',
|
|
47
|
+
requester: thread.assignee?.name,
|
|
48
|
+
actor: user?.name,
|
|
49
|
+
...((0, approval_notice_rules_js_1.isTurnNotice)(kind) ? (0, approval_notice_rules_js_1.approvalStep)(instance.approvalLine, approval.order) : {})
|
|
50
|
+
};
|
|
51
|
+
const address = (0, approval_notice_rules_js_1.noticeAddress)(kind, { approvalId: approval.id, threadId: thread.id });
|
|
52
|
+
(0, notification_1.sendNoticeWhenCommitted)(context, {
|
|
53
|
+
type: 'approval',
|
|
54
|
+
level: kind === 'rejected' ? 'warn' : 'info',
|
|
55
|
+
receivers,
|
|
56
|
+
words: lng => (0, approval_notice_rules_js_1.noticeWords)(translatorOf(lng), facts),
|
|
57
|
+
address,
|
|
58
|
+
link: (0, notification_1.noticeLinkOf)(context, address),
|
|
59
|
+
label: `approval notice ${kind} of ${approval.id}`
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
env_1.logger.error(`[worklist] approval notice ${kind} of ${approvalId} could not be made`, error);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/** Only a person who can sign in is told. */
|
|
67
|
+
function active(person) {
|
|
68
|
+
return person && person.status === auth_base_1.UserStatus.ACTIVATED ? { id: person.id, name: person.name, email: person.email, locale: person.locale } : null;
|
|
69
|
+
}
|
|
70
|
+
/** A sentence missing in the person's language is said in English, never as its key. */
|
|
71
|
+
function translatorOf(lng) {
|
|
72
|
+
const own = (0, translator_1.translatorFor)(lng);
|
|
73
|
+
const english = (0, translator_1.translatorFor)('en');
|
|
74
|
+
return (key, options = {}) => own(key, { ...options, defaultValue: english(key, options) });
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=approval-notice.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"approval-notice.js","sourceRoot":"","sources":["../../../server/service/approval-notice/approval-notice.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * The approval notice (ADR-0164) - the one place the worklist tells people of an approval's turn and judgment.\n *\n * The four places an approval moves call `noticeApproval`: createApproval (a turn or a reference), delegate (a\n * handed seat), approve with no one after (approved) and reject (rejected). Applications call nothing; a case that\n * goes through the approval line is told of. Sending is notification's (`sendNoticeWhenCommitted`).\n */\nimport { User, UserStatus } from '@things-factory/auth-base'\nimport { logger } from '@things-factory/env'\nimport { translatorFor } from '@things-factory/i18n-base/translator'\nimport { noticeLinkOf, sendNoticeWhenCommitted } from '@things-factory/notification'\n\nimport { ActivityApproval } from '../activity-approval/activity-approval.js'\nimport { poolMembers } from '../activity-instance/assignee-pool.js'\nimport {\n approvalStep,\n isTurnNotice,\n noticeAddress,\n noticeReceivers,\n noticeWords,\n type ApprovalNoticeKind,\n type NoticeFacts,\n type NoticePerson,\n type Translate\n} from './approval-notice-rules.js'\n\n/**\n * Tells of what just happened to approval `approvalId`, once the transaction has committed.\n *\n * Who and what are settled here, inside the transaction, from rows it can see. Nothing here throws: a notice that\n * cannot be made is reported and the approval stands.\n */\nexport async function noticeApproval(context: ResolverContext, { kind, approvalId }: { kind: ApprovalNoticeKind; approvalId: string }): Promise<void> {\n try {\n const { user, tx } = context.state\n\n const approval = await tx.getRepository(ActivityApproval).findOne({\n where: { id: approvalId },\n relations: ['approver', 'approverRole', 'approverRole.users', 'activityThread', 'activityThread.assignee', 'activityThread.activityInstance']\n })\n const thread = approval?.activityThread\n const instance = thread?.activityInstance\n if (!approval || !thread || !instance) return\n\n const receivers = noticeReceivers({\n kind,\n approver: active(approval.approver),\n roleHolders: poolMembers(approval.approverRole ? [approval.approverRole] : []).map(active).filter(Boolean),\n requester: active(thread.assignee),\n actorId: user?.id\n })\n if (!receivers.length) return\n\n const facts: NoticeFacts = {\n kind,\n name: instance.name || '',\n requester: thread.assignee?.name,\n actor: user?.name,\n ...(isTurnNotice(kind) ? approvalStep(instance.approvalLine, approval.order) : {})\n }\n const address = noticeAddress(kind, { approvalId: approval.id, threadId: thread.id })\n\n sendNoticeWhenCommitted(context, {\n type: 'approval',\n level: kind === 'rejected' ? 'warn' : 'info',\n receivers,\n words: lng => noticeWords(translatorOf(lng), facts),\n address,\n link: noticeLinkOf(context, address),\n label: `approval notice ${kind} of ${approval.id}`\n })\n } catch (error) {\n logger.error(`[worklist] approval notice ${kind} of ${approvalId} could not be made`, error)\n }\n}\n\n/** Only a person who can sign in is told. */\nfunction active(person?: User | null): NoticePerson | null {\n return person && person.status === UserStatus.ACTIVATED ? { id: person.id, name: person.name, email: person.email, locale: person.locale } : null\n}\n\n/** A sentence missing in the person's language is said in English, never as its key. */\nfunction translatorOf(lng: string): Translate {\n const own = translatorFor(lng)\n const english = translatorFor('en')\n return (key, options = {}) => own(key, { ...options, defaultValue: english(key, options) })\n}\n"]}
|