forked from votrongdao/FlowX
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathManagement.cs
More file actions
470 lines (400 loc) · 17.5 KB
/
Copy pathManagement.cs
File metadata and controls
470 lines (400 loc) · 17.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
using FlowX;
namespace Crm;
// -------------------------------------------------------------------------------- the vocabulary
/// <summary>What a person sees by default, which the tenant sets.</summary>
/// <remarks>
/// <strong>A scope, not a permission.</strong> All three may read; what differs is whose plans are
/// in the total. Without the distinction the only answers available are "mine" and "the whole
/// tenant", and a company with nine sales managers gets neither of the ones it wanted.
/// </remarks>
public enum OrgRole
{
/// <summary>Sees their own plans.</summary>
Representative,
/// <summary>Sees their own and everyone below them in the line.</summary>
Manager,
/// <summary>Sees the tenant's.</summary>
Director,
}
/// <summary>What an account objective is measured in.</summary>
/// <remarks>
/// Closed, because an objective measured in whatever somebody typed is one no two account plans
/// can be compared on — and comparing them is what a quarterly review is.
/// </remarks>
public enum ObjectiveMeasure
{
/// <summary>Money.</summary>
Revenue,
/// <summary>A count of deals.</summary>
Deals,
/// <summary>A count of meetings held.</summary>
Meetings,
/// <summary>How many of the range they have bought.</summary>
Products,
/// <summary>Introductions they have made.</summary>
Referrals,
}
/// <summary>Where an objective has got to.</summary>
public enum ObjectiveStatus
{
/// <summary>Nothing has happened yet.</summary>
NotStarted,
/// <summary>Something has.</summary>
InProgress,
/// <summary>Done.</summary>
Achieved,
/// <summary>Given up on, deliberately. Not the same as forgotten.</summary>
Abandoned,
}
/// <summary>What a person is to a deal.</summary>
public enum StakeholderRole
{
/// <summary>Can spend the money without asking anybody.</summary>
EconomicBuyer,
/// <summary>Wants this to happen and can act.</summary>
Champion,
/// <summary>Shapes the decision without making it.</summary>
Influencer,
/// <summary>Will live with what is bought.</summary>
User,
/// <summary>Does not want this to happen.</summary>
Blocker,
/// <summary>Reviews the contract.</summary>
Legal,
/// <summary>Runs the buying process.</summary>
Procurement,
/// <summary>Decides whether it works.</summary>
Technical,
}
/// <summary>How a person feels about it.</summary>
public enum Sentiment
{
/// <summary>Argues for it when nobody from the vendor is in the room.</summary>
Advocate,
/// <summary>In favour.</summary>
Supportive,
/// <summary>Has not decided, or does not care.</summary>
Neutral,
/// <summary>Has doubts.</summary>
Sceptical,
/// <summary>Against.</summary>
Opposed,
}
/// <summary>How bad a risk would be.</summary>
public enum RiskSeverity
{
/// <summary>Annoying.</summary>
Low,
/// <summary>Would cost time.</summary>
Medium,
/// <summary>Would cost the deal's date.</summary>
High,
/// <summary>Would cost the deal.</summary>
Critical,
}
/// <summary>What a KPI is computed from.</summary>
/// <remarks>
/// A closed list this build knows how to compute, for the same reason the report sources are
/// closed: the alternative is an expression language, and what a leadership team reviews is five
/// or six numbers that never change, not an arbitrary formula.
/// </remarks>
public enum KpiSource
{
/// <summary>The sum of opportunities that have not been decided.</summary>
OpenPipeline,
/// <summary>The sum of those that were won inside the period.</summary>
WonRevenue,
/// <summary>How many leads arrived inside the period.</summary>
LeadsCaptured,
/// <summary>How many activities are still open.</summary>
OpenTasks,
/// <summary>How many agreed steps are past their date and not done.</summary>
OverduePlanSteps,
}
/// <summary>Which way is good.</summary>
/// <remarks>
/// Without it, a target of five overdue steps and one of five million in pipeline would be scored
/// the same way, and one of the two answers would be exactly wrong.
/// </remarks>
public enum KpiDirection
{
/// <summary>More is better.</summary>
HigherIsBetter,
/// <summary>Less is better.</summary>
LowerIsBetter,
}
// -------------------------------------------------------------------------------- what is asked
/// <summary>Places a person in the organisation.</summary>
/// <param name="UserId">Their subject, as their token carries it.</param>
/// <param name="DisplayName">What to show.</param>
/// <param name="Role">What they see by default.</param>
/// <param name="ReportsTo">Their manager's subject, or null at the top.</param>
public sealed record SetOrgMember(
string UserId,
string DisplayName,
OrgRole Role,
string? ReportsTo);
/// <summary>The person was placed.</summary>
/// <param name="UserId">Whose.</param>
/// <param name="Reports">How many people are below them in the line, at any depth.</param>
public sealed record OrgMemberSet(string UserId, int Reports);
/// <summary>Asks for the reporting line, whole.</summary>
/// <remarks>
/// <para>
/// <strong>Every read in this sample is scoped by this line, and nothing could show it.</strong> A
/// manager sees their reports' rows, a director sees everybody's, and which is which is decided
/// here — so a person placed under the wrong manager silently sees the wrong pipeline, and the one
/// screen that would reveal it did not exist.
/// </para>
/// <para>
/// <strong>Unscoped on purpose, unlike the reads it governs.</strong> The chart is who reports to
/// whom, not anybody's numbers; a manager who could only see their own branch could not tell a
/// missing person from somebody placed elsewhere, which is the mistake this read exists to make
/// visible.
/// </para>
/// </remarks>
public sealed record ReadOrgChart();
/// <summary>One person in the line.</summary>
/// <param name="UserId">Their subject, as their token carries it.</param>
/// <param name="DisplayName">What to show.</param>
/// <param name="Role">What they see by default.</param>
/// <param name="ReportsTo">Their manager's subject, or null at the top.</param>
/// <param name="Reports">
/// How many report to them directly. Direct rather than at any depth, because the number sits
/// beside a row whose children are drawn underneath it, and a total that counted grandchildren
/// would not match what the reader can see.
/// </param>
public sealed record OrgChartMember(
string UserId,
string DisplayName,
OrgRole Role,
string? ReportsTo,
int Reports);
/// <summary>The reporting line.</summary>
/// <param name="Members">
/// Everybody, managers before their reports.
/// <para>
/// <strong>There is no "unplaced" count beside this, and the schema is why.</strong> A quota
/// carries a foreign key into <c>org_member</c>, so somebody holding a number is placed by
/// construction — a count of people who are not would be a column that is always zero. A caller
/// whose own subject is absent from this list is the case that matters, and the caller is the one
/// who knows their subject.
/// </para>
/// </param>
public sealed record OrgChart(IReadOnlyList<OrgChartMember> Members);
/// <summary>Writes an objective of an account plan.</summary>
/// <param name="Plan">Which plan.</param>
/// <param name="Ordinal">Where it sits.</param>
/// <param name="Description">What is to be achieved.</param>
/// <param name="Measure">In what.</param>
/// <param name="Target">How much.</param>
/// <param name="Status">Where it has got to.</param>
public sealed record SetObjective(
string Plan,
int Ordinal,
string Description,
ObjectiveMeasure Measure,
decimal Target,
ObjectiveStatus Status);
/// <summary>The objective was written.</summary>
/// <param name="Ordinal">Which one.</param>
/// <param name="Outstanding">How many of this plan's objectives are not yet achieved.</param>
public sealed record ObjectiveSet(int Ordinal, int Outstanding);
/// <summary>Puts a person on the relationship map.</summary>
/// <param name="Plan">Which plan.</param>
/// <param name="Contact">Who.</param>
/// <param name="Role">What they are to the deal.</param>
/// <param name="Sentiment">How they feel about it.</param>
/// <param name="Influence">How much they matter, one to five.</param>
public sealed record SetStakeholder(
string Plan,
Guid Contact,
StakeholderRole Role,
Sentiment Sentiment,
int Influence);
/// <summary>The map was written.</summary>
/// <param name="Mapped">How many people are on it.</param>
/// <param name="Opposed">
/// How many are <see cref="Sentiment.Sceptical"/> or <see cref="Sentiment.Opposed"/> — the number
/// a coverage review actually asks for.
/// </param>
public sealed record StakeholderSet(int Mapped, int Opposed);
/// <summary>Raises a risk, or closes one.</summary>
/// <param name="Plan">Which plan.</param>
/// <param name="Ordinal">Where it sits.</param>
/// <param name="Description">What could go wrong.</param>
/// <param name="Severity">How bad it would be.</param>
/// <param name="Mitigation">What is being done about it.</param>
/// <param name="IsOpen">Whether it is still a risk.</param>
public sealed record SetRisk(
string Plan,
int Ordinal,
string Description,
RiskSeverity Severity,
string Mitigation,
bool IsOpen);
/// <summary>The risk was written.</summary>
/// <param name="Ordinal">Which one.</param>
/// <param name="Open">How many of this plan's risks are still open.</param>
public sealed record RiskSet(int Ordinal, int Open);
/// <summary>Declares a number the leadership team reviews.</summary>
/// <param name="Name">What to ask for it by.</param>
/// <param name="Label">What to show.</param>
/// <param name="Source">What it is computed from.</param>
/// <param name="Target">What good looks like.</param>
/// <param name="Direction">Which way is good.</param>
public sealed record DefineKpi(
string Name,
string Label,
KpiSource Source,
decimal Target,
KpiDirection Direction);
/// <summary>The KPI was declared.</summary>
/// <param name="KpiId">Its id.</param>
public sealed record KpiDefined(Guid KpiId);
/// <summary>Records what was said about a number, when it was said.</summary>
/// <param name="Kpi">Which KPI.</param>
/// <param name="Period">For which period.</param>
/// <param name="Commentary">What was said.</param>
public sealed record ReviewKpi(string Kpi, string Period, string Commentary);
/// <summary>The review was recorded.</summary>
/// <param name="Kpi">Which KPI.</param>
/// <param name="Actual">
/// What the number was when the commentary was written. <strong>Stored, unlike a plan's
/// actual</strong> — a review is a minute of a meeting, and a minute that silently updated itself
/// would not be one.
/// </param>
/// <param name="Status">Whether it met its target, by its direction.</param>
public sealed record KpiReviewed(string Kpi, decimal Actual, string Status);
/// <summary>Who the caller is, read from their claims and never from the body.</summary>
/// <remarks>
/// <strong>The subject comes off the principal.</strong> A request that carried its own user id
/// would let anybody read anybody's roll-up by typing a different name into it — which is the
/// whole reason the tenant is a claim and not a header.
/// </remarks>
public static class Caller
{
/// <summary>The caller's subject, or the empty string when they have none.</summary>
/// <param name="principal">Who is calling.</param>
/// <returns>Their subject.</returns>
public static string Subject(System.Security.Claims.ClaimsPrincipal? principal) =>
principal?.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value
?? string.Empty;
}
/// <summary>What a roll-up is given, once the flow has read the caller.</summary>
/// <param name="Period">Which period.</param>
/// <param name="UserId">Whose view it is.</param>
public sealed record ForViewer(string Period, string UserId);
/// <summary>What the review capability is given, once the flow has read the caller.</summary>
/// <param name="Request">What was asked for.</param>
/// <param name="UserId">Who is minuting it.</param>
public sealed record ForReview(ReviewKpi Request, string UserId);
/// <summary>Asks for every KPI, computed for a period.</summary>
/// <param name="Period">Which period.</param>
public sealed record ReadScorecard(string Period);
// ------------------------------------------------------------------------------- what comes back
/// <summary>One KPI, its target and what it actually is.</summary>
/// <param name="Name">Which KPI.</param>
/// <param name="Label">What to show.</param>
/// <param name="Source">What it was computed from.</param>
/// <param name="Target">What good looks like.</param>
/// <param name="Actual">What it is, read live.</param>
/// <param name="Direction">Which way is good.</param>
/// <param name="Status"><c>OnTrack</c> or <c>OffTrack</c>, decided by the direction.</param>
/// <param name="LastCommentary">What was said about it last time, or null.</param>
public sealed record KpiResult(
string Name,
string Label,
string Source,
decimal Target,
decimal Actual,
string Direction,
string Status,
string? LastCommentary);
/// <summary>Every KPI for a period.</summary>
/// <param name="Period">Which period.</param>
/// <param name="Kpis">The numbers, off-track first — which is the order a review walks them in.</param>
public sealed record Scorecard(string Period, IReadOnlyList<KpiResult> Kpis);
/// <summary>Who a caller is, and therefore whose plans they see.</summary>
/// <param name="UserId">Their subject.</param>
/// <param name="Role">What the tenant set them to.</param>
/// <param name="Scope">
/// The subjects whose plans they see. One for a representative, their line for a manager, and
/// empty for a director — where empty means "no restriction" rather than "nothing".
/// </param>
public sealed record ViewerScope(string UserId, OrgRole Role, IReadOnlyList<string> Scope);
// ------------------------------------------------------------------------------- what can go wrong
/// <summary>Refusals the management surface can produce.</summary>
public static class ManagementErrors
{
/// <summary>The manager named is not somebody this tenant has.</summary>
/// <param name="userId">Who was named.</param>
/// <returns>The refusal.</returns>
public static Error MemberNotFound(string userId) =>
new(
"crm.org_member_not_found",
$"'{userId}' has not been placed in this organisation.",
ErrorCategory.NotFound);
/// <summary>The line would loop back on itself.</summary>
/// <param name="userId">Who was being placed.</param>
/// <returns>The refusal.</returns>
/// <remarks>
/// Refused at the write, because a cycle in a reporting line makes every roll-up below it
/// either wrong or non-terminating, and a settings screen is where the two-person loop gets
/// created by accident.
/// </remarks>
public static Error ReportingLineWouldLoop(string userId) =>
new(
"crm.org_line_would_loop",
$"Placing '{userId}' there would make the reporting line loop back on itself.",
ErrorCategory.Conflict);
/// <summary>The setting is not one this plan's kind has.</summary>
/// <param name="kind">Which kind the plan is.</param>
/// <param name="what">What was being written.</param>
/// <returns>The refusal.</returns>
public static Error NotOfThisPlanKind(string kind, string what) =>
new(
"crm.plan_kind_has_no",
$"A {kind} plan has no {what}.",
ErrorCategory.Validation);
/// <summary>The KPI is not one this tenant has.</summary>
/// <param name="name">What was asked for.</param>
/// <returns>The refusal.</returns>
public static Error KpiNotFound(string name) =>
new(
"crm.kpi_not_found",
$"No KPI named '{name}' has been declared for this tenant.",
ErrorCategory.NotFound);
/// <summary>The influence was outside one to five.</summary>
/// <param name="influence">What was sent.</param>
/// <returns>The refusal.</returns>
public static Error InfluenceIsOutOfRange(int influence) =>
new(
"crm.stakeholder_influence_range",
$"Influence is one to five, and {influence} was sent.",
ErrorCategory.Validation);
/// <summary>The caller has not been placed in the organisation.</summary>
/// <returns>The refusal.</returns>
/// <remarks>
/// Refused rather than defaulted to <see cref="OrgRole.Representative"/>. A default would make
/// a director whose row was never written see one plan and conclude their organisation had
/// stopped selling.
/// </remarks>
public static Error CallerIsNotInTheOrganisation() =>
new(
"crm.caller_not_in_organisation",
"This caller has not been placed in the organisation, so there is no way to say " +
"whose plans they see.",
ErrorCategory.Forbidden);
}
/// <summary>What the management surface accepts.</summary>
public static class ManagementLimits
{
/// <summary>How deep a reporting line is followed.</summary>
/// <remarks>
/// <strong>A cycle in the line cannot be prevented by a constraint</strong> — a three-person
/// loop is three individually legal rows — so the recursive read is bounded instead. Deeper
/// than this and an organisation has a data problem, not a hierarchy.
/// </remarks>
public const int MaxDepth = 20;
}