gdc-common-utils-ts 2.2.2 → 2.3.0

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.
@@ -28,7 +28,7 @@ export class ConditionEntryEditor extends ClinicalResourceEntryEditor {
28
28
  getOnsetDateTime() { return this.getScalarClaim(ConditionClaim.OnsetDateTime); }
29
29
  setRecorder(reference) { return this.setScalarClaim(ConditionClaim.Recorder, reference); }
30
30
  getRecorder() { return this.getScalarClaim(ConditionClaim.Recorder); }
31
- setContainedDocumentIdentifierList(identifiers) { return this.setCsvClaimList(ConditionClaim.ContainedDocuments, identifiers); }
32
- getContainedDocumentIdentifierList() { return this.getCsvClaimList(ConditionClaim.ContainedDocuments); }
31
+ setContainedDocumentIdentifierList(identifiers) { return this.setCsvClaimList(ConditionClaim.ContainedReferenceList, identifiers); }
32
+ getContainedDocumentIdentifierList() { return this.getCsvClaimList(ConditionClaim.ContainedReferenceList); }
33
33
  }
34
34
  registerBundleEntryEditor(BundleEditableResourceTypes.condition, ConditionEntryEditor);
@@ -205,7 +205,7 @@ function normalizeConsentRoleForRuleKey(value) {
205
205
  return `org.hl7.terminology.codesystem.v3-rolecode.${code}`;
206
206
  }
207
207
  if (system === 'v3-personalrelationshiproletype') {
208
- return `org.hl7.v3.personalrelationship.${code}`;
208
+ return `org.hl7.terminology.codesystem.v3-rolecode.${code}`;
209
209
  }
210
210
  return claim.toLowerCase();
211
211
  }
@@ -8,9 +8,11 @@ import { buildLicenseSearchEntry, type LicenseClaims, type LicenseSearchInput, t
8
8
  export type LicenseListSearchState = Readonly<{
9
9
  serialNumbers?: readonly string[];
10
10
  email?: string;
11
+ telephone?: string;
11
12
  role?: string;
12
13
  status?: LicenseStatus;
13
14
  subjectId?: string;
15
+ ownerOrganizationId?: string;
14
16
  userClass?: string;
15
17
  type?: string;
16
18
  active?: boolean;
@@ -28,6 +30,7 @@ export type LicenseListRecord = Readonly<{
28
30
  id?: string;
29
31
  status?: string;
30
32
  subjectId?: string;
33
+ ownerOrganizationId?: string;
31
34
  email?: string;
32
35
  role?: string;
33
36
  category?: string;
@@ -66,6 +69,8 @@ export declare class LicenseListSearchEditor {
66
69
  setRole(value: string): this;
67
70
  setStatus(value: LicenseStatus): this;
68
71
  setSubjectId(value: string): this;
72
+ /** Limits results to the organization that owns the seat pool. */
73
+ setOwnerOrganizationId(value: string): this;
69
74
  setUserClass(value: string): this;
70
75
  setAppType(value: string): this;
71
76
  setActive(value: boolean): this;
@@ -15,6 +15,7 @@ function cloneDraft(state) {
15
15
  role: normalizeText(state?.role),
16
16
  status: state?.status,
17
17
  subjectId: normalizeText(state?.subjectId),
18
+ ownerOrganizationId: normalizeText(state?.ownerOrganizationId),
18
19
  userClass: normalizeText(state?.userClass),
19
20
  type: normalizeText(state?.type),
20
21
  active: typeof state?.active === 'boolean' ? state.active : undefined,
@@ -71,6 +72,11 @@ export class LicenseListSearchEditor {
71
72
  this.draft = cloneDraft({ ...this.draft, subjectId: value });
72
73
  return this;
73
74
  }
75
+ /** Limits results to the organization that owns the seat pool. */
76
+ setOwnerOrganizationId(value) {
77
+ this.draft = cloneDraft({ ...this.draft, ownerOrganizationId: value });
78
+ return this;
79
+ }
74
80
  setUserClass(value) {
75
81
  this.draft = cloneDraft({ ...this.draft, userClass: value });
76
82
  return this;
@@ -117,6 +123,7 @@ export class LicenseListSearchEditor {
117
123
  ...(this.draft.role ? { role: this.draft.role } : {}),
118
124
  ...(resolveStatus(this.draft) ? { status: resolveStatus(this.draft) } : {}),
119
125
  ...(this.draft.subjectId ? { subjectId: this.draft.subjectId } : {}),
126
+ ...(this.draft.ownerOrganizationId ? { ownerOrganizationId: this.draft.ownerOrganizationId } : {}),
120
127
  additionalClaims: cloneClaims(this.draft.additionalClaims),
121
128
  };
122
129
  }
@@ -151,7 +158,9 @@ export function readLicenseListRecords(body) {
151
158
  || entry.id),
152
159
  status: normalizeText(meta.status),
153
160
  subjectId: normalizeText(meta.subjectId),
161
+ ownerOrganizationId: normalizeText(meta.ownerOrganizationId || claims['License.ownerOrganizationId']),
154
162
  email: normalizeText(claims[ClaimsPersonSchemaorg.email]),
163
+ telephone: normalizeText(claims[ClaimsPersonSchemaorg.telephone]),
155
164
  role: normalizeText(claims[ClaimsPersonSchemaorg.hasOccupationalRoleValue]),
156
165
  category: normalizeText(claims[ClaimsIndividualProductSchemaorg.category]),
157
166
  appType: normalizeText(claims[ClaimsIndividualProductSchemaorg.additionalType]),
@@ -54,11 +54,44 @@ export declare const LicenseStatuses: Readonly<{
54
54
  readonly Inactive: "inactive";
55
55
  }>;
56
56
  export type LicenseStatus = typeof LicenseStatuses[keyof typeof LicenseStatuses];
57
+ /**
58
+ * Role families that decide which organization owns the actor's license.
59
+ *
60
+ * This classification is deliberately independent from Consent. A person may
61
+ * receive permissions in either case, but only a FHIR v3 relationship role is
62
+ * a member of the individual's organization and consumes its seat pool.
63
+ */
64
+ export declare const LicenseRoleKinds: Readonly<{
65
+ readonly IndividualMember: "individual-member";
66
+ readonly Professional: "professional";
67
+ readonly Unsupported: "unsupported";
68
+ }>;
69
+ export type LicenseRoleKind = typeof LicenseRoleKinds[keyof typeof LicenseRoleKinds];
70
+ /**
71
+ * Classifies one canonical or compatibility role token for license ownership.
72
+ *
73
+ * Rules:
74
+ * - FHIR/HL7 v3 `RoleCode` actors are non-employee members of the individual
75
+ * organization and consume one of its licenses.
76
+ * - ISCO/ISCO-08 actors are professionals. Their employer/organization owns
77
+ * their professional license, so a patient's individual pool is untouched.
78
+ * - Unknown role systems are not silently charged to either pool.
79
+ */
80
+ export declare function classifyLicenseRole(role: string): LicenseRoleKind;
57
81
  export type LicenseIssueInput = Readonly<{
58
- email: string;
82
+ email?: string;
83
+ telephone?: string;
59
84
  role: string;
60
85
  userClass?: DeviceUserClass;
61
86
  type?: DeviceAppType;
87
+ /** Individual/legal organization that owns the seat pool. */
88
+ ownerOrganizationId?: string;
89
+ /** Card/individual DID granted by the invitation after acceptance. */
90
+ subjectDid?: string;
91
+ /** Existing RelatedPerson/contact selected before the invitation. */
92
+ relatedPersonId?: string;
93
+ /** Stable invitation workflow identifier; not the license id or code. */
94
+ invitationId?: string;
62
95
  additionalClaims?: LicenseClaims;
63
96
  }>;
64
97
  export type LicensePurchaseInput = Readonly<{
@@ -66,6 +99,10 @@ export type LicensePurchaseInput = Readonly<{
66
99
  userClass?: DeviceUserClass;
67
100
  type?: DeviceAppType;
68
101
  serialNumbers?: readonly string[];
102
+ price?: number;
103
+ priceCurrency?: string;
104
+ /** Organization whose pool receives the newly materialized seats. */
105
+ ownerOrganizationId?: string;
69
106
  additionalClaims?: LicenseClaims;
70
107
  }>;
71
108
  export type LicenseSearchInput = Readonly<{
@@ -76,6 +113,7 @@ export type LicenseSearchInput = Readonly<{
76
113
  role?: string;
77
114
  status?: LicenseStatus;
78
115
  subjectId?: string;
116
+ ownerOrganizationId?: string;
79
117
  additionalClaims?: LicenseClaims;
80
118
  }>;
81
119
  /**
@@ -106,6 +144,10 @@ export declare function buildLicenseIssueEntry(input: LicenseIssueInput): {
106
144
  };
107
145
  meta: {
108
146
  claims: LicenseClaims;
147
+ ownerOrganizationId?: string;
148
+ subjectDid?: string;
149
+ relatedPersonId?: string;
150
+ invitationId?: string;
109
151
  };
110
152
  };
111
153
  /**
@@ -130,6 +172,7 @@ export declare function buildLicensePurchaseEntry(input: LicensePurchaseInput):
130
172
  };
131
173
  meta: {
132
174
  claims: LicenseClaims;
175
+ ownerOrganizationId?: string;
133
176
  };
134
177
  };
135
178
  /**
@@ -151,5 +194,6 @@ export declare function buildLicenseSearchEntry(input: LicenseSearchInput): {
151
194
  claims: LicenseClaims;
152
195
  status?: LicenseStatus;
153
196
  subjectId?: string;
197
+ ownerOrganizationId?: string;
154
198
  };
155
199
  };
@@ -52,6 +52,47 @@ export const LicenseStatuses = Object.freeze({
52
52
  Active: 'active',
53
53
  Inactive: 'inactive',
54
54
  });
55
+ /**
56
+ * Role families that decide which organization owns the actor's license.
57
+ *
58
+ * This classification is deliberately independent from Consent. A person may
59
+ * receive permissions in either case, but only a FHIR v3 relationship role is
60
+ * a member of the individual's organization and consumes its seat pool.
61
+ */
62
+ export const LicenseRoleKinds = Object.freeze({
63
+ IndividualMember: 'individual-member',
64
+ Professional: 'professional',
65
+ Unsupported: 'unsupported',
66
+ });
67
+ /**
68
+ * Classifies one canonical or compatibility role token for license ownership.
69
+ *
70
+ * Rules:
71
+ * - FHIR/HL7 v3 `RoleCode` actors are non-employee members of the individual
72
+ * organization and consume one of its licenses.
73
+ * - ISCO/ISCO-08 actors are professionals. Their employer/organization owns
74
+ * their professional license, so a patient's individual pool is untouched.
75
+ * - Unknown role systems are not silently charged to either pool.
76
+ */
77
+ export function classifyLicenseRole(role) {
78
+ const normalized = String(role || '').trim().toLowerCase();
79
+ if (!normalized)
80
+ return LicenseRoleKinds.Unsupported;
81
+ const system = normalized.includes('|') ? normalized.split('|', 1)[0] : normalized;
82
+ if (system === 'isco'
83
+ || system === 'isco-08'
84
+ || system === 'org.ilo.isco'
85
+ || system === 'org.ilo.isco-08'
86
+ || system.endsWith('/isco-08')) {
87
+ return LicenseRoleKinds.Professional;
88
+ }
89
+ if (system === 'v3-rolecode'
90
+ || system === 'org.hl7.v3.rolecode'
91
+ || system.endsWith('/v3-rolecode')) {
92
+ return LicenseRoleKinds.IndividualMember;
93
+ }
94
+ return LicenseRoleKinds.Unsupported;
95
+ }
55
96
  function cloneClaims(claims) {
56
97
  return { ...(claims || {}) };
57
98
  }
@@ -82,12 +123,31 @@ export function serializeLicenseSerialNumbers(serialNumbers) {
82
123
  * - seat/app semantics come from `IndividualProduct.*`
83
124
  */
84
125
  export function buildLicenseIssueClaims(input) {
126
+ const email = String(input.email || '').trim();
127
+ const telephone = String(input.telephone || '').trim();
128
+ if (!email && !telephone) {
129
+ throw new Error('License issue requires an email or telephone recipient.');
130
+ }
131
+ if (telephone && !/^\+[1-9]\d{6,14}$/.test(telephone)) {
132
+ throw new Error('License issue telephone must use an international ITU format beginning with +.');
133
+ }
134
+ const userClass = input.userClass || DeviceUserClasses.Employee;
135
+ if (userClass === DeviceUserClasses.Individual) {
136
+ const roleKind = classifyLicenseRole(input.role);
137
+ if (roleKind === LicenseRoleKinds.Professional) {
138
+ throw new Error('ISCO professional roles do not consume individual-member licenses.');
139
+ }
140
+ if (roleKind !== LicenseRoleKinds.IndividualMember) {
141
+ throw new Error('Individual-member licenses require a FHIR v3-RoleCode role.');
142
+ }
143
+ }
85
144
  const claims = {
86
145
  '@context': LicenseClaimContext.SchemaOrg,
87
146
  ...cloneClaims(input.additionalClaims),
88
- [ClaimsPersonSchemaorg.email]: input.email.trim(),
147
+ ...(email ? { [ClaimsPersonSchemaorg.email]: email } : {}),
148
+ ...(telephone ? { [ClaimsPersonSchemaorg.telephone]: telephone } : {}),
89
149
  [ClaimsPersonSchemaorg.hasOccupationalRoleValue]: input.role.trim(),
90
- [ClaimsIndividualProductSchemaorg.category]: mapLicenseCategoryFromUserClass(input.userClass || DeviceUserClasses.Employee),
150
+ [ClaimsIndividualProductSchemaorg.category]: mapLicenseCategoryFromUserClass(userClass),
91
151
  [ClaimsIndividualProductSchemaorg.additionalType]: input.type || DeviceAppTypes.Mobile,
92
152
  };
93
153
  return claims;
@@ -104,6 +164,18 @@ export function buildLicenseIssueEntry(input) {
104
164
  ...buildLicenseIssueClaims(input),
105
165
  '@type': LicenseEntryOperations.Issue,
106
166
  },
167
+ ...(String(input.ownerOrganizationId || '').trim()
168
+ ? { ownerOrganizationId: String(input.ownerOrganizationId).trim() }
169
+ : {}),
170
+ ...(String(input.subjectDid || '').trim()
171
+ ? { subjectDid: String(input.subjectDid).trim() }
172
+ : {}),
173
+ ...(String(input.relatedPersonId || '').trim()
174
+ ? { relatedPersonId: String(input.relatedPersonId).trim() }
175
+ : {}),
176
+ ...(String(input.invitationId || '').trim()
177
+ ? { invitationId: String(input.invitationId).trim() }
178
+ : {}),
107
179
  },
108
180
  };
109
181
  }
@@ -129,6 +201,12 @@ export function buildLicensePurchaseClaims(input) {
129
201
  if (serializedSerialNumbers) {
130
202
  claims[ClaimsOfferSchemaorg.serialNumber] = serializedSerialNumbers;
131
203
  }
204
+ if (typeof input.price === 'number' && Number.isFinite(input.price) && input.price >= 0) {
205
+ claims[ClaimsOfferSchemaorg.price] = input.price;
206
+ }
207
+ if (typeof input.priceCurrency === 'string' && input.priceCurrency.trim()) {
208
+ claims[ClaimsOfferSchemaorg.priceCurrency] = input.priceCurrency.trim().toUpperCase();
209
+ }
132
210
  return claims;
133
211
  }
134
212
  /**
@@ -144,6 +222,9 @@ export function buildLicensePurchaseEntry(input) {
144
222
  ...buildLicensePurchaseClaims(input),
145
223
  '@type': LicenseEntryOperations.Purchase,
146
224
  },
225
+ ...(String(input.ownerOrganizationId || '').trim()
226
+ ? { ownerOrganizationId: String(input.ownerOrganizationId).trim() }
227
+ : {}),
147
228
  },
148
229
  };
149
230
  }
@@ -189,6 +270,9 @@ export function buildLicenseSearchEntry(input) {
189
270
  ...(typeof input.subjectId === 'string' && input.subjectId.trim()
190
271
  ? { subjectId: input.subjectId.trim() }
191
272
  : {}),
273
+ ...(typeof input.ownerOrganizationId === 'string' && input.ownerOrganizationId.trim()
274
+ ? { ownerOrganizationId: input.ownerOrganizationId.trim() }
275
+ : {}),
192
276
  },
193
277
  };
194
278
  }
@@ -2,11 +2,15 @@ export type RelatedPersonListRecord = Readonly<{
2
2
  identifier?: string;
3
3
  patient?: string;
4
4
  relationship?: string;
5
+ /** Functional `RelatedPerson.role` extension values, separate from kinship. */
6
+ roles: readonly string[];
5
7
  name?: string;
6
8
  telecom?: string;
7
9
  active?: string;
8
10
  status?: string;
9
11
  resourceId?: string;
12
+ relatedEntityType?: string;
13
+ actorIdentifiers: readonly string[];
10
14
  claims: Record<string, unknown>;
11
15
  }>;
12
16
  export type RelatedPersonListSelection = Readonly<{
@@ -12,6 +12,17 @@ function extractClaims(entry) {
12
12
  const resourceClaims = resourceMeta.claims && typeof resourceMeta.claims === 'object' ? resourceMeta.claims : undefined;
13
13
  return { ...(resourceClaims || {}), ...(metaClaims || {}) };
14
14
  }
15
+ function readClaim(claims, claimKey) {
16
+ if (claims[claimKey] !== undefined)
17
+ return claims[claimKey];
18
+ const context = normalizeText(claims['@context']);
19
+ if (context && claims[`${context.replace(/\.$/, '')}.${claimKey}`] !== undefined) {
20
+ return claims[`${context.replace(/\.$/, '')}.${claimKey}`];
21
+ }
22
+ const suffix = `.${claimKey}`;
23
+ const matchedKey = Object.keys(claims).find((key) => key.endsWith(suffix));
24
+ return matchedKey ? claims[matchedKey] : undefined;
25
+ }
15
26
  /**
16
27
  * Reads subject-side relationship records from one current GW-style result
17
28
  * body into one neutral list shape for frontend screens.
@@ -29,14 +40,23 @@ export function readRelatedPersonListRecords(body) {
29
40
  const meta = entry.meta && typeof entry.meta === 'object' ? entry.meta : {};
30
41
  const resource = entry.resource && typeof entry.resource === 'object' ? entry.resource : {};
31
42
  return {
32
- identifier: normalizeText(claims[RelatedPersonClaim.IdentifierValue] ?? claims[RelatedPersonClaim.Identifier]),
33
- patient: normalizeText(claims[RelatedPersonClaim.Patient]),
34
- relationship: normalizeText(claims[RelatedPersonClaim.Relationship]),
35
- name: normalizeText(claims[RelatedPersonClaim.Name]),
36
- telecom: normalizeText(claims[RelatedPersonClaim.Telecom]),
37
- active: normalizeText(claims[RelatedPersonClaim.Active]),
43
+ identifier: normalizeText(readClaim(claims, RelatedPersonClaim.IdentifierValue) ?? readClaim(claims, RelatedPersonClaim.Identifier)),
44
+ patient: normalizeText(readClaim(claims, RelatedPersonClaim.Patient)),
45
+ relationship: normalizeText(readClaim(claims, RelatedPersonClaim.Relationship)),
46
+ roles: String(readClaim(claims, RelatedPersonClaim.Role) ?? '')
47
+ .split(',')
48
+ .map((value) => value.trim().toUpperCase())
49
+ .filter(Boolean),
50
+ name: normalizeText(readClaim(claims, RelatedPersonClaim.Name)),
51
+ telecom: normalizeText(readClaim(claims, RelatedPersonClaim.Telecom)),
52
+ active: normalizeText(readClaim(claims, RelatedPersonClaim.Active)),
38
53
  status: normalizeText(meta.status),
39
54
  resourceId: normalizeText(resource.id || entry.id),
55
+ relatedEntityType: normalizeText(readClaim(claims, RelatedPersonClaim.RelatedEntityType)),
56
+ actorIdentifiers: String(readClaim(claims, RelatedPersonClaim.ActorIdentifier) ?? '')
57
+ .split(',')
58
+ .map((value) => value.trim())
59
+ .filter(Boolean),
40
60
  claims,
41
61
  };
42
62
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gdc-common-utils-ts",
3
- "version": "2.2.2",
3
+ "version": "2.3.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },