vet-data-utils-ts 0.5.32 → 0.5.37

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/README.md CHANGED
@@ -1,4 +1,13 @@
1
1
  # vet-data-utils-ts
2
+ ## Blocking local-first SDK release
3
+
4
+ The canonical contract is [`SDK_LAYERING.md`](https://github.com/Fundacion-UNID/sos-data-utils-ts/blob/main/docs/SDK_LAYERING.md). Its
5
+ indivisible order is **local packages -> full local test matrix -> merge every
6
+ validated branch to `main` -> publish bottom-up -> install exact registry
7
+ versions and commit lockfiles on `main` -> staging**. Never publish to discover
8
+ deterministic failures and never commit a local package reference. Staging
9
+ begins only after exact registry packages are installed and verified.
10
+
2
11
 
3
12
  ## Lost-animal access and behavior
4
13
 
@@ -189,6 +198,16 @@ for immunologicals); ordinary human ATC is not substituted for it. Veterinary
189
198
  allergy product coding may also use ATCvet, while manifestations use an allowed
190
199
  SNOMED source.
191
200
 
201
+ Terminology-assisted entry uses the neutral coding-review contract from
202
+ `fhir-data-utils-ts/coding-review-flat-claims`. Spoken, typed or imported text
203
+ is `Immunization.vaccine-code-text`; an unconfirmed candidate stays in
204
+ `meta.codingProposals[]` beside the Immunization's `meta.claims`, targets
205
+ `Immunization.vaccine-code`, and is never copied to `ResearchSubject.meta`.
206
+ Confirmation adds the selected `system|code` and official `-display` without
207
+ overwriting local `-text`. Vet policy supplies ATCvet/species constraints; it
208
+ does not redefine the shared shape. Consumers reuse exported fixtures and
209
+ builders rather than repeating governed literals.
210
+
192
211
  `vet-data-utils-ts/veterinary-biologic-product` keeps three independent axes:
193
212
 
194
213
  - WHO ATCvet QI is the international medicine classification;
@@ -280,7 +299,8 @@ value used by UHC UNID, VetChain and SOSChain adapters. The host supplies the
280
299
  allowed sector list and translated labels. The value contains one official
281
300
  identifier (`TAX`, `EIN` or `BN`), ISO country, optional matching ISO
282
301
  subdivision, optional official licence, legal representative, technical
283
- controller and six independent index participation roles. It rejects a second
302
+ controller and independent index, personal-backup and Digital Twin
303
+ participation roles. It rejects a second
284
304
  `taxId` and every PIN field: profile protection happens immediately after
285
305
  login, before any application screen.
286
306
 
@@ -1,4 +1,5 @@
1
1
  import { normalizeClaimsFromFhirResource } from 'gdc-common-utils-ts/utils/interoperable-resource-operation';
2
+ import { normalizeFhirApiClaimKey } from 'gdc-common-utils-ts/utils/fhir-api-claim-helpers';
2
3
  const PRIMARY_CODE_PROJECTIONS = Object.freeze({
3
4
  Condition: { field: 'Condition.code', code: 'Condition.code', text: 'Condition.code-text', display: 'Condition.code-display' },
4
5
  Procedure: { field: 'Procedure.code', code: 'Procedure.code', text: 'Procedure.code-text', display: 'Procedure.code-display' },
@@ -42,7 +43,7 @@ async function projectResource(resource, options) {
42
43
  const existingMeta = asRecord(resource.meta);
43
44
  const existingClaims = asRecord(existingMeta.claims);
44
45
  const claims = Object.keys(existingClaims).length
45
- ? { ...existingClaims }
46
+ ? presentationClaims(existingClaims)
46
47
  : normalizeClaimsFromFhirResource(resource, {});
47
48
  const projection = PRIMARY_CODE_PROJECTIONS[resourceType];
48
49
  if (projection) {
@@ -69,6 +70,24 @@ async function projectResource(resource, options) {
69
70
  meta: { ...existingMeta, claims },
70
71
  };
71
72
  }
73
+ /**
74
+ * Keeps structural FHIR metadata on the resource while preventing derived
75
+ * paths such as `Immunization.meta.versionId` from becoming editable/search
76
+ * claims in the presentation copy.
77
+ */
78
+ function presentationClaims(source) {
79
+ return Object.fromEntries(Object.entries(source).filter(([claim]) => {
80
+ if (claim.startsWith('@'))
81
+ return true;
82
+ try {
83
+ normalizeFhirApiClaimKey(claim);
84
+ return true;
85
+ }
86
+ catch {
87
+ return false;
88
+ }
89
+ }));
90
+ }
72
91
  function parseCodingToken(value) {
73
92
  const separator = value.lastIndexOf('|');
74
93
  if (separator <= 0 || separator === value.length - 1)
@@ -1,8 +1,9 @@
1
+ /** @deprecated Import `ClinicalTerminologySystems` from sos-data-utils-ts. */
1
2
  export declare const VeterinaryClinicalTerminologySystems: Readonly<{
2
3
  readonly Loinc: "http://loinc.org";
3
4
  readonly SnomedCt: "http://snomed.info/sct";
4
5
  readonly Icd10: "http://hl7.org/fhir/sid/icd-10";
5
- /** WHO Collaborating Centre ATCvet classification, distinct from human ATC. */
6
+ readonly Atc: "http://www.whocc.no/atc";
6
7
  readonly AtcVet: "http://www.whocc.no/atcvet";
7
8
  }>;
8
9
  /**
@@ -11,15 +12,19 @@ export declare const VeterinaryClinicalTerminologySystems: Readonly<{
11
12
  * canonical SNOMED system URI.
12
13
  * https://www.snomed.org/gps
13
14
  */
15
+ /** @deprecated Import `ClinicalTerminologySourceScopes` from sos-data-utils-ts. */
14
16
  export declare const VeterinaryTerminologySourceScopes: Readonly<{
15
17
  readonly Loinc: "LNC";
16
18
  readonly SnomedGps: "SNOMED_GPS";
19
+ readonly SnomedIps: "SNOMED_IPS";
17
20
  readonly SnomedCt: "SNOMED_CT";
18
21
  readonly SnomedVeterinary: "SNOMEDCT_VET";
19
22
  readonly Icd10: "ICD10";
23
+ readonly Atc: "ATC";
20
24
  readonly AtcVet: "ATCVET";
21
25
  }>;
22
26
  /** Deterministic standards-based data for the isolated local terminology upstream. */
27
+ /** @deprecated Import `CLINICAL_TERMINOLOGY_TEST_DATA` from sos-data-utils-ts. */
23
28
  export declare const VET_TERMINOLOGY_TEST_DATA: Readonly<{
24
29
  readonly localApiKey: "local-playwright-umls-key";
25
30
  readonly bodyTemperature: Readonly<{
@@ -35,8 +40,31 @@ export declare const VET_TERMINOLOGY_TEST_DATA: Readonly<{
35
40
  code: "3135009";
36
41
  display: "otitis externa";
37
42
  }>;
43
+ readonly canineRabiesVaccine: Readonly<{
44
+ scope: "ATCVET";
45
+ system: "http://www.whocc.no/atcvet";
46
+ code: "QI07AA02";
47
+ text: "virus de la rabia";
48
+ display: "rabies virus";
49
+ ncbiTaxonomyId: "9615";
50
+ }>;
51
+ readonly humanInactivatedInfluenzaVaccine: Readonly<{
52
+ scope: "ATC";
53
+ system: "http://www.whocc.no/atc";
54
+ code: "J07BB02";
55
+ text: "vacuna contra la gripe inactivada de virus fraccionados o ant\u00EDgeno de superficie";
56
+ display: "influenza, inactivated, split virus or surface antigen";
57
+ ncbiTaxonomyId: "9606";
58
+ }>;
59
+ readonly rabiesDisease: Readonly<{
60
+ scope: "SNOMED_GPS";
61
+ umlsSource: "SNOMEDCT_US";
62
+ system: "http://snomed.info/sct";
63
+ code: "14168008";
64
+ display: "Rabies";
65
+ }>;
38
66
  }>;
39
- export type VeterinaryTerminologySourceScope = typeof VeterinaryTerminologySourceScopes[keyof typeof VeterinaryTerminologySourceScopes];
67
+ export type VeterinaryTerminologySourceScope = Exclude<typeof VeterinaryTerminologySourceScopes[keyof typeof VeterinaryTerminologySourceScopes], 'ATC' | 'SNOMED_IPS'>;
40
68
  export type VeterinaryTerminologyCapability = Readonly<{
41
69
  type: 'terminology-capability';
42
70
  id: string;
@@ -1,41 +1,17 @@
1
- export const VeterinaryClinicalTerminologySystems = Object.freeze({
2
- Loinc: 'http://loinc.org',
3
- SnomedCt: 'http://snomed.info/sct',
4
- Icd10: 'http://hl7.org/fhir/sid/icd-10',
5
- /** WHO Collaborating Centre ATCvet classification, distinct from human ATC. */
6
- AtcVet: 'http://www.whocc.no/atcvet',
7
- });
1
+ import { CLINICAL_TERMINOLOGY_TEST_DATA, ClinicalTerminologySourceScopes, ClinicalTerminologySystems, } from 'sos-data-utils-ts/clinical-terminology';
2
+ /** @deprecated Import `ClinicalTerminologySystems` from sos-data-utils-ts. */
3
+ export const VeterinaryClinicalTerminologySystems = ClinicalTerminologySystems;
8
4
  /**
9
5
  * Source scopes are authorization/content boundaries, not Coding.system URIs.
10
6
  * GPS, full SNOMED CT and the veterinary extension intentionally share the
11
7
  * canonical SNOMED system URI.
12
8
  * https://www.snomed.org/gps
13
9
  */
14
- export const VeterinaryTerminologySourceScopes = Object.freeze({
15
- Loinc: 'LNC',
16
- SnomedGps: 'SNOMED_GPS',
17
- SnomedCt: 'SNOMED_CT',
18
- SnomedVeterinary: 'SNOMEDCT_VET',
19
- Icd10: 'ICD10',
20
- AtcVet: 'ATCVET',
21
- });
10
+ /** @deprecated Import `ClinicalTerminologySourceScopes` from sos-data-utils-ts. */
11
+ export const VeterinaryTerminologySourceScopes = ClinicalTerminologySourceScopes;
22
12
  /** Deterministic standards-based data for the isolated local terminology upstream. */
23
- export const VET_TERMINOLOGY_TEST_DATA = Object.freeze({
24
- localApiKey: 'local-playwright-umls-key',
25
- bodyTemperature: Object.freeze({
26
- source: VeterinaryTerminologySourceScopes.Loinc,
27
- system: VeterinaryClinicalTerminologySystems.Loinc,
28
- code: '8310-5',
29
- display: 'Body temperature:Temp:Pt:^Patient:Qn',
30
- }),
31
- otitisExternaSpanish: Object.freeze({
32
- scope: VeterinaryTerminologySourceScopes.SnomedCt,
33
- umlsSource: 'SCTSPA',
34
- system: VeterinaryClinicalTerminologySystems.SnomedCt,
35
- code: '3135009',
36
- display: 'otitis externa',
37
- }),
38
- });
13
+ /** @deprecated Import `CLINICAL_TERMINOLOGY_TEST_DATA` from sos-data-utils-ts. */
14
+ export const VET_TERMINOLOGY_TEST_DATA = CLINICAL_TERMINOLOGY_TEST_DATA;
39
15
  /**
40
16
  * ISO 3166-1 alpha-2 territories enumerated by the official SNOMED
41
17
  * International member page on 2026-09-05.
@@ -0,0 +1,51 @@
1
+ import { type FlatClaimCodingCandidate, type FlatClaimCodingReviewResource } from 'fhir-data-utils-ts/coding-review-flat-claims';
2
+ /** Governed claim coordinates reused by Vet gateways, portals and assistants. */
3
+ export declare const VeterinaryImmunizationCodingReviewClaims: Readonly<{
4
+ code: "Immunization.vaccine-code";
5
+ display: string;
6
+ localText: string;
7
+ }>;
8
+ /**
9
+ * Builds an unconfirmed veterinary Immunization coding review from text spoken,
10
+ * typed or imported by a channel. The neutral helper owns proposal placement;
11
+ * this wrapper contributes veterinary resource coordinates and ATCvet policy.
12
+ */
13
+ export declare function buildVeterinaryImmunizationCodingReview(input: Readonly<{
14
+ entryId: string;
15
+ animalReference: string;
16
+ administrationDate: string;
17
+ localText: string;
18
+ language: string;
19
+ proposalId: string;
20
+ candidates: readonly FlatClaimCodingCandidate[];
21
+ }>): FlatClaimCodingReviewResource;
22
+ /** Package-owned synthetic example for assistants, portals and downstream tests. */
23
+ export declare const VETERINARY_IMMUNIZATION_CODING_REVIEW_EXAMPLE: Readonly<{
24
+ input: Readonly<{
25
+ entryId: "00000000-0000-4000-8000-000000000201";
26
+ animalReference: "Patient/00000000-0000-4000-8000-000000000202";
27
+ administrationDate: "2024-05-23";
28
+ localText: "vacuna contra la rabia";
29
+ language: "es-ES";
30
+ proposalId: "veterinary-immunization-proposal-example";
31
+ candidates: readonly Readonly<{
32
+ id: "veterinary-immunization-candidate-example";
33
+ system: "http://www.whocc.no/atcvet";
34
+ code: "QI07AA02";
35
+ display: "Inactivated rabies virus vaccines";
36
+ }>[];
37
+ }>;
38
+ claims: Readonly<{
39
+ code: "Immunization.vaccine-code";
40
+ display: string;
41
+ localText: string;
42
+ }>;
43
+ expected: Readonly<{
44
+ resourceType: string;
45
+ id: string;
46
+ meta: Readonly<{
47
+ claims: Readonly<Record<string, string>>;
48
+ codingProposals?: readonly import("fhir-data-utils-ts/coding-review-flat-claims").FlatClaimCodingProposal[];
49
+ }>;
50
+ }>;
51
+ }>;
@@ -0,0 +1,59 @@
1
+ import { buildFlatClaimCodingReviewResource, deriveCodingDisplayClaim, deriveCodingLocalTextClaim, } from 'fhir-data-utils-ts/coding-review-flat-claims';
2
+ import { VeterinaryClinicalTerminologySystems } from './clinical-terminology.js';
3
+ const IMMUNIZATION_RESOURCE_TYPE = 'Immunization';
4
+ const IMMUNIZATION_VACCINE_CODE_CLAIM = `${IMMUNIZATION_RESOURCE_TYPE}.vaccine-code`;
5
+ /** Governed claim coordinates reused by Vet gateways, portals and assistants. */
6
+ export const VeterinaryImmunizationCodingReviewClaims = Object.freeze({
7
+ code: IMMUNIZATION_VACCINE_CODE_CLAIM,
8
+ display: deriveCodingDisplayClaim(IMMUNIZATION_VACCINE_CODE_CLAIM),
9
+ localText: deriveCodingLocalTextClaim(IMMUNIZATION_VACCINE_CODE_CLAIM),
10
+ });
11
+ /**
12
+ * Builds an unconfirmed veterinary Immunization coding review from text spoken,
13
+ * typed or imported by a channel. The neutral helper owns proposal placement;
14
+ * this wrapper contributes veterinary resource coordinates and ATCvet policy.
15
+ */
16
+ export function buildVeterinaryImmunizationCodingReview(input) {
17
+ const candidates = input.candidates.map((candidate) => {
18
+ if (candidate.system !== VeterinaryClinicalTerminologySystems.AtcVet) {
19
+ throw new TypeError('veterinary_immunization_candidate_system_invalid');
20
+ }
21
+ return candidate;
22
+ });
23
+ return buildFlatClaimCodingReviewResource({
24
+ resourceType: IMMUNIZATION_RESOURCE_TYPE,
25
+ resourceId: input.entryId,
26
+ targetClaim: VeterinaryImmunizationCodingReviewClaims.code,
27
+ localText: input.localText,
28
+ language: input.language,
29
+ baseClaims: {
30
+ 'Immunization.status': 'completed',
31
+ 'Immunization.patient': input.animalReference,
32
+ 'Immunization.date': input.administrationDate,
33
+ },
34
+ proposalId: input.proposalId,
35
+ candidates,
36
+ });
37
+ }
38
+ const EXAMPLE_ENTRY_ID = '00000000-0000-4000-8000-000000000201';
39
+ const EXAMPLE_ANIMAL_ID = '00000000-0000-4000-8000-000000000202';
40
+ const EXAMPLE_INPUT = Object.freeze({
41
+ entryId: EXAMPLE_ENTRY_ID,
42
+ animalReference: `Patient/${EXAMPLE_ANIMAL_ID}`,
43
+ administrationDate: '2024-05-23',
44
+ localText: 'vacuna contra la rabia',
45
+ language: 'es-ES',
46
+ proposalId: 'veterinary-immunization-proposal-example',
47
+ candidates: Object.freeze([Object.freeze({
48
+ id: 'veterinary-immunization-candidate-example',
49
+ system: VeterinaryClinicalTerminologySystems.AtcVet,
50
+ code: 'QI07AA02',
51
+ display: 'Inactivated rabies virus vaccines',
52
+ })]),
53
+ });
54
+ /** Package-owned synthetic example for assistants, portals and downstream tests. */
55
+ export const VETERINARY_IMMUNIZATION_CODING_REVIEW_EXAMPLE = Object.freeze({
56
+ input: EXAMPLE_INPUT,
57
+ claims: VeterinaryImmunizationCodingReviewClaims,
58
+ expected: buildVeterinaryImmunizationCodingReview(EXAMPLE_INPUT),
59
+ });
@@ -39,5 +39,5 @@ export declare function confirmVeterinaryImmunizationDraft(draft: VeterinaryImmu
39
39
  export declare function materializeVeterinaryImmunizationDraft(draft: VeterinaryImmunizationDraft, context: Readonly<{
40
40
  entryId: string;
41
41
  animalReference: string;
42
- performerReference: string;
42
+ performerReference?: string;
43
43
  }>): readonly FlatClaimResourceEntry[];
@@ -46,14 +46,19 @@ export function materializeVeterinaryImmunizationDraft(draft, context) {
46
46
  return buildVeterinaryImmunizationFlatGraph({
47
47
  entryId: required(context.entryId, 'veterinary_immunization_entry_id_required'),
48
48
  animalReference: required(context.animalReference, 'veterinary_immunization_animal_reference_required'),
49
- performerReference: required(context.performerReference, 'veterinary_immunization_performer_required'),
49
+ ...(context.performerReference ? {
50
+ performerReference: required(context.performerReference, 'veterinary_immunization_performer_invalid'),
51
+ } : {}),
50
52
  administrationDate: draft.administrationDate,
51
53
  vaccineCode: draft.vaccineCode,
52
54
  targetDiseases: draft.targetDiseases,
53
55
  ...(draft.routeCode ? { route: { system: 'http://terminology.hl7.org/CodeSystem/v3-RouteOfAdministration', code: draft.routeCode } } : {}),
54
56
  ...(draft.siteCode ? { site: { system: 'http://terminology.hl7.org/CodeSystem/v3-ActSite', code: draft.siteCode } } : {}),
55
57
  ...(draft.doseValue ? { dose: { value: draft.doseValue, ...(draft.doseUnit ? { unit: draft.doseUnit } : {}) } } : {}),
56
- ...(draft.nextDoseDate ? { nextDoseDate: draft.nextDoseDate } : {}),
58
+ ...(draft.nextDoseDate ? {
59
+ nextDoseDate: draft.nextDoseDate,
60
+ recommendationDate: required(draft.confirmedAt, 'veterinary_immunization_confirmation_time_required'),
61
+ } : {}),
57
62
  ...(hasMedication ? { medication: {
58
63
  entryId: `${context.entryId}-medication`,
59
64
  productName: draft.productName || draft.vaccineCode.display || `${draft.vaccineCode.system}|${draft.vaccineCode.code}`,
@@ -75,6 +80,7 @@ function coding(value, error) {
75
80
  return Object.freeze({
76
81
  system: required(candidate.system, error), code: required(candidate.code, error),
77
82
  ...(text(candidate.display) ? { display: text(candidate.display) } : {}),
83
+ ...(text(candidate.text) ? { text: text(candidate.text) } : {}),
78
84
  });
79
85
  }
80
86
  function date(value, error) {
@@ -1,8 +1,15 @@
1
1
  import { type FlatClaimResourceEntry } from 'sos-data-utils-ts/flat-claim-resource-graph';
2
+ import { type ImmunizationReminderChannel } from 'sos-data-utils-ts/immunization-reminder-plan';
2
3
  export type VeterinaryImmunizationCoding = Readonly<{
3
4
  system: string;
4
5
  code: string;
5
6
  display?: string;
7
+ /**
8
+ * Canonical requested-language term entered or selected by the channel.
9
+ * A neutral coding proposal targets `Immunization.vaccine-code`; accepting
10
+ * it must preserve this value as `Immunization.vaccine-code-text`.
11
+ */
12
+ text?: string;
6
13
  }>;
7
14
  export type VeterinaryImmunizationFlatInput = Readonly<{
8
15
  entryId: string;
@@ -10,7 +17,8 @@ export type VeterinaryImmunizationFlatInput = Readonly<{
10
17
  animalReference: string;
11
18
  administrationDate: string;
12
19
  vaccineCode: VeterinaryImmunizationCoding;
13
- performerReference: string;
20
+ /** Present only when the administering professional is known. */
21
+ performerReference?: string;
14
22
  targetDiseases?: readonly VeterinaryImmunizationCoding[];
15
23
  route?: VeterinaryImmunizationCoding;
16
24
  site?: VeterinaryImmunizationCoding;
@@ -19,6 +27,10 @@ export type VeterinaryImmunizationFlatInput = Readonly<{
19
27
  unit?: string;
20
28
  }>;
21
29
  nextDoseDate?: string;
30
+ /** FHIR dateTime when the recommendation was created; required with nextDoseDate. */
31
+ recommendationDate?: string;
32
+ /** Ordered candidates only; dispatch rechecks current preference and Consent. */
33
+ reminderChannels?: readonly ImmunizationReminderChannel[];
22
34
  medication?: Readonly<{
23
35
  entryId: string;
24
36
  productName: string;
@@ -36,7 +48,10 @@ export type VeterinaryImmunizationFlatInput = Readonly<{
36
48
  attachmentEntryIds?: readonly string[];
37
49
  }>;
38
50
  /**
39
- * Creates a version-neutral flat graph. SearchParameter names are preferred
51
+ * Creates a version-neutral flat graph after coding review is confirmed.
52
+ * Unconfirmed `meta.codingProposals[]` use the neutral `fhir-data-utils-ts`
53
+ * contract and remain on the same resource, outside `meta.claims`.
54
+ * SearchParameter names are preferred
40
55
  * (`Immunization.date`, `Medication.expiration-date`) and product evidence is
41
56
  * kept in Medication instead of overloading credential validity.
42
57
  */
@@ -1,6 +1,10 @@
1
1
  import { buildDocumentReferenceFlatEntry, buildFlatClaimResourceEntry, linkFlatClaimResourceEntries, } from 'sos-data-utils-ts/flat-claim-resource-graph';
2
+ import { ImmunizationReminderChannels, buildImmunizationReminderTasks, } from 'sos-data-utils-ts/immunization-reminder-plan';
2
3
  /**
3
- * Creates a version-neutral flat graph. SearchParameter names are preferred
4
+ * Creates a version-neutral flat graph after coding review is confirmed.
5
+ * Unconfirmed `meta.codingProposals[]` use the neutral `fhir-data-utils-ts`
6
+ * contract and remain on the same resource, outside `meta.claims`.
7
+ * SearchParameter names are preferred
4
8
  * (`Immunization.date`, `Medication.expiration-date`) and product evidence is
5
9
  * kept in Medication instead of overloading credential validity.
6
10
  */
@@ -11,14 +15,24 @@ export function buildVeterinaryImmunizationFlatGraph(input) {
11
15
  'Immunization.patient': required(input.animalReference, 'veterinary_immunization_animal_reference_required'),
12
16
  'Immunization.date': isoDate(input.administrationDate, 'veterinary_immunization_date_invalid'),
13
17
  'Immunization.vaccine-code': vaccineCode,
14
- 'Immunization.performer': required(input.performerReference, 'veterinary_immunization_performer_required'),
15
18
  };
16
19
  if (input.identifier)
17
20
  claims['Immunization.identifier'] = required(input.identifier, 'veterinary_immunization_identifier_invalid');
18
21
  if (input.vaccineCode.display)
19
22
  claims['Immunization.vaccine-code-display'] = input.vaccineCode.display.trim();
20
- if (input.targetDiseases?.length)
23
+ if (input.vaccineCode.text)
24
+ claims['Immunization.vaccine-code-text'] = input.vaccineCode.text.trim();
25
+ if (input.performerReference)
26
+ claims['Immunization.performer'] = required(input.performerReference, 'veterinary_immunization_performer_invalid');
27
+ if (input.targetDiseases?.length) {
21
28
  claims['Immunization.target-disease'] = input.targetDiseases.map(codingToken).join(',');
29
+ if (input.targetDiseases.some(({ display }) => display?.trim())) {
30
+ claims['Immunization.target-disease-display'] = joinCsv(input.targetDiseases.map(({ display }) => display?.trim() ?? ''));
31
+ }
32
+ if (input.targetDiseases.some(({ text }) => text?.trim())) {
33
+ claims['Immunization.target-disease-text'] = joinCsv(input.targetDiseases.map(({ text }) => text?.trim() ?? ''));
34
+ }
35
+ }
22
36
  if (input.route)
23
37
  claims['Immunization.route'] = codingToken(input.route);
24
38
  if (input.site)
@@ -30,18 +44,34 @@ export function buildVeterinaryImmunizationFlatGraph(input) {
30
44
  }
31
45
  const immunization = buildFlatClaimResourceEntry({ entryId: input.entryId, resourceType: 'Immunization', claims });
32
46
  const children = [];
47
+ let recommendationId;
48
+ let reminderTasks = [];
33
49
  if (input.medication)
34
50
  children.push(buildMedicationEntry(input.medication));
35
51
  if (input.nextDoseDate) {
52
+ recommendationId = `${input.entryId}-recommendation`;
36
53
  children.push(buildFlatClaimResourceEntry({
37
- entryId: `${input.entryId}-recommendation`,
54
+ entryId: recommendationId,
38
55
  resourceType: 'ImmunizationRecommendation',
39
56
  claims: {
40
57
  'ImmunizationRecommendation.patient': input.animalReference,
41
- 'ImmunizationRecommendation.date': isoDate(input.nextDoseDate, 'veterinary_immunization_next_dose_date_invalid'),
58
+ // FHIR date is when the forecast was created, not when coverage ends.
59
+ 'ImmunizationRecommendation.date': isoDate(required(input.recommendationDate, 'veterinary_immunization_recommendation_date_required'), 'veterinary_immunization_recommendation_date_invalid'),
42
60
  'ImmunizationRecommendation.vaccine-code': vaccineCode,
61
+ 'ImmunizationRecommendation.forecast-status': 'http://terminology.hl7.org/CodeSystem/immunization-recommendation-status|due',
62
+ 'ImmunizationRecommendation.date-criterion-code': 'http://loinc.org|30980-7',
63
+ 'ImmunizationRecommendation.date-criterion-value': isoDate(input.nextDoseDate, 'veterinary_immunization_next_dose_date_invalid'),
64
+ 'ImmunizationRecommendation.supporting-immunization': `Immunization/${input.entryId}`,
43
65
  },
44
66
  }));
67
+ reminderTasks = buildImmunizationReminderTasks({
68
+ entryIdPrefix: recommendationId,
69
+ recommendationReference: `ImmunizationRecommendation/${recommendationId}`,
70
+ subjectReference: input.animalReference,
71
+ coverageEndDate: input.nextDoseDate,
72
+ channelCandidates: input.reminderChannels ?? ImmunizationReminderChannels,
73
+ });
74
+ children.push(...reminderTasks);
45
75
  }
46
76
  for (const attachment of input.attachments ?? []) {
47
77
  if (attachment.resourceType !== 'DocumentReference')
@@ -52,11 +82,18 @@ export function buildVeterinaryImmunizationFlatGraph(input) {
52
82
  const childEntryIds = [
53
83
  ...(input.medication ? [input.medication.entryId] : []),
54
84
  ...(input.nextDoseDate ? [`${input.entryId}-recommendation`] : []),
85
+ ...(input.attachments ?? []).map(({ entryId }) => entryId),
55
86
  ...(input.attachmentEntryIds ?? []),
56
87
  ];
57
- return childEntryIds.length
88
+ const linkedImmunization = childEntryIds.length
58
89
  ? linkFlatClaimResourceEntries(graph, { parentEntryId: input.entryId, childEntryIds })
59
90
  : graph;
91
+ return recommendationId && reminderTasks.length
92
+ ? linkFlatClaimResourceEntries(linkedImmunization, {
93
+ parentEntryId: recommendationId,
94
+ childEntryIds: reminderTasks.map(({ entryId }) => entryId),
95
+ })
96
+ : linkedImmunization;
60
97
  }
61
98
  function buildMedicationEntry(input) {
62
99
  const claims = {
@@ -91,23 +128,29 @@ export function exportVeterinaryImmunizationFlatGraph(graph, release) {
91
128
  function exportEntry(entry, release, byReference) {
92
129
  const claims = entry.claims;
93
130
  if (entry.resourceType === 'Immunization') {
94
- const vaccineCode = codeableConcept(claims['Immunization.vaccine-code'], claims['Immunization.vaccine-code-display']);
131
+ const vaccineCode = codeableConcept(claims['Immunization.vaccine-code'], claims['Immunization.vaccine-code-display'], claims['Immunization.vaccine-code-text']);
95
132
  const children = splitCsv(claims['Immunization.contained-reference-list']).map((reference) => byReference.get(reference)).filter(Boolean);
96
133
  const medication = children.find((child) => child?.resourceType === 'Medication');
97
134
  const resource = {
98
135
  resourceType: 'Immunization', id: entry.entryId, status: claims['Immunization.status'],
99
136
  vaccineCode, patient: { reference: claims['Immunization.patient'] },
100
137
  occurrenceDateTime: claims['Immunization.date'],
101
- performer: [{ actor: { reference: claims['Immunization.performer'] } }],
102
138
  };
139
+ if (claims['Immunization.performer'])
140
+ resource.performer = [{ actor: { reference: claims['Immunization.performer'] } }];
103
141
  if (claims['Immunization.identifier'])
104
142
  resource.identifier = [{ value: claims['Immunization.identifier'] }];
105
143
  if (claims['Immunization.route'])
106
144
  resource.route = codeableConcept(claims['Immunization.route']);
107
145
  if (claims['Immunization.site'])
108
146
  resource.site = codeableConcept(claims['Immunization.site']);
109
- if (claims['Immunization.target-disease'])
110
- resource.protocolApplied = [{ targetDisease: splitCsv(claims['Immunization.target-disease']).map((code) => codeableConcept(code)) }];
147
+ if (claims['Immunization.target-disease']) {
148
+ const displays = splitCsvKeepingEmpty(claims['Immunization.target-disease-display']);
149
+ const texts = splitCsvKeepingEmpty(claims['Immunization.target-disease-text']);
150
+ resource.protocolApplied = [{
151
+ targetDisease: splitCsv(claims['Immunization.target-disease']).map((code, index) => (codeableConcept(code, displays[index], texts[index]))),
152
+ }];
153
+ }
111
154
  if (medication && release !== 'R4')
112
155
  resource.administeredProduct = { reference: { reference: medication.reference } };
113
156
  return resource;
@@ -117,7 +160,7 @@ function exportEntry(entry, release, byReference) {
117
160
  const expirationDate = claims['Medication.expiration-date'];
118
161
  const resource = {
119
162
  resourceType: 'Medication', id: entry.entryId,
120
- code: codeableConcept(claims['Medication.code'], claims['Medication.code-text']),
163
+ code: codeableConcept(claims['Medication.code'], undefined, claims['Medication.code-text']),
121
164
  };
122
165
  if (claims['Medication.identifier'])
123
166
  resource.identifier = splitCsv(claims['Medication.identifier']).map(identifierFromToken);
@@ -132,7 +175,15 @@ function exportEntry(entry, release, byReference) {
132
175
  resourceType: 'ImmunizationRecommendation', id: entry.entryId,
133
176
  patient: { reference: claims['ImmunizationRecommendation.patient'] },
134
177
  date: claims['ImmunizationRecommendation.date'],
135
- recommendation: [{ vaccineCode: [codeableConcept(claims['ImmunizationRecommendation.vaccine-code'])] }],
178
+ recommendation: [{
179
+ vaccineCode: [codeableConcept(claims['ImmunizationRecommendation.vaccine-code'])],
180
+ forecastStatus: codeableConcept(claims['ImmunizationRecommendation.forecast-status']),
181
+ dateCriterion: [{
182
+ code: codeableConcept(claims['ImmunizationRecommendation.date-criterion-code']),
183
+ value: claims['ImmunizationRecommendation.date-criterion-value'],
184
+ }],
185
+ supportingImmunization: [{ reference: claims['ImmunizationRecommendation.supporting-immunization'] }],
186
+ }],
136
187
  };
137
188
  if (entry.resourceType === 'DocumentReference')
138
189
  return {
@@ -146,12 +197,15 @@ function exportEntry(entry, release, byReference) {
146
197
  };
147
198
  return { resourceType: entry.resourceType, id: entry.entryId, meta: { claims: { ...claims } } };
148
199
  }
149
- function codeableConcept(token, display) {
200
+ function codeableConcept(token, display, text) {
150
201
  const value = required(token, 'veterinary_coding_required');
151
202
  const separator = value.lastIndexOf('|');
152
203
  if (separator <= 0 || separator === value.length - 1)
153
- return { text: display || value };
154
- return { coding: [{ system: value.slice(0, separator), code: value.slice(separator + 1), ...(display ? { display } : {}) }] };
204
+ return { text: text || display || value };
205
+ return {
206
+ coding: [{ system: value.slice(0, separator), code: value.slice(separator + 1), ...(display ? { display } : {}) }],
207
+ ...(text ? { text } : {}),
208
+ };
155
209
  }
156
210
  function identifierFromToken(token) {
157
211
  const separator = token.lastIndexOf('|');
@@ -172,7 +226,36 @@ function required(value, error) {
172
226
  return value.trim();
173
227
  }
174
228
  function splitCsv(value) {
175
- return value?.split(',').map((item) => item.trim()).filter(Boolean) ?? [];
229
+ return splitCsvKeepingEmpty(value).filter(Boolean);
230
+ }
231
+ function splitCsvKeepingEmpty(value) {
232
+ if (value === undefined)
233
+ return [];
234
+ const values = [];
235
+ let current = '';
236
+ let quoted = false;
237
+ for (let index = 0; index < value.length; index += 1) {
238
+ const character = value[index];
239
+ if (character === '"' && quoted && value[index + 1] === '"') {
240
+ current += '"';
241
+ index += 1;
242
+ }
243
+ else if (character === '"')
244
+ quoted = !quoted;
245
+ else if (character === ',' && !quoted) {
246
+ values.push(current.trim());
247
+ current = '';
248
+ }
249
+ else
250
+ current += character;
251
+ }
252
+ values.push(current.trim());
253
+ return values;
254
+ }
255
+ function joinCsv(values) {
256
+ return values.map((value) => value.includes(',') || value.includes('"')
257
+ ? `"${value.replaceAll('"', '""')}"`
258
+ : value).join(',');
176
259
  }
177
260
  function uniqueEntries(entries) {
178
261
  const byId = new Map();
@@ -194,9 +277,13 @@ export const NALA_VACCINATION_FLAT_GRAPH_TEST_DATA = uniqueEntries([
194
277
  ...buildVeterinaryImmunizationFlatGraph({
195
278
  entryId: 'nala-rabies', animalReference: 'Patient/nala', administrationDate: '2024-05-23',
196
279
  performerReference: 'PractitionerRole/certificate-veterinarian',
197
- vaccineCode: { system: atcVet, code: 'QI07AA02', display: 'Inactivated rabies virus vaccines' },
198
- targetDiseases: [{ system: 'http://snomed.info/sct', code: '14168008', display: 'Rabies' }],
280
+ vaccineCode: {
281
+ system: atcVet, code: 'QI07AA02', display: 'Inactivated rabies virus vaccines',
282
+ text: 'Vacuna contra el virus de la rabia',
283
+ },
284
+ targetDiseases: [{ system: 'http://snomed.info/sct', code: '14168008', display: 'Rabies', text: 'Rabia' }],
199
285
  nextDoseDate: '2027-05-23',
286
+ recommendationDate: '2026-09-20T12:00:00.000Z',
200
287
  medication: {
201
288
  entryId: 'nala-rabies-product', productName: 'Imrab 3 TF',
202
289
  productCode: { system: atcVet, code: 'QI07AA02' }, manufacturer: 'Boehringer Ingelheim',
@@ -213,12 +300,14 @@ export const NALA_VACCINATION_FLAT_GRAPH_TEST_DATA = uniqueEntries([
213
300
  entryId: 'nala-da2plp', animalReference: 'Patient/nala', administrationDate: '2024-05-23',
214
301
  performerReference: 'PractitionerRole/certificate-veterinarian',
215
302
  vaccineCode: { system: atcVet, code: 'QI07AI02', display: 'Canine viral and bacterial combination vaccines' },
216
- nextDoseDate: '2025-05-23', attachments: [certificate], attachmentEntryIds: [certificate.entryId],
303
+ nextDoseDate: '2025-05-23', recommendationDate: '2026-09-20T12:00:00.000Z',
304
+ attachments: [certificate], attachmentEntryIds: [certificate.entryId],
217
305
  }),
218
306
  ...buildVeterinaryImmunizationFlatGraph({
219
307
  entryId: 'nala-lyme', animalReference: 'Patient/nala', administrationDate: '2024-05-23',
220
308
  performerReference: 'PractitionerRole/certificate-veterinarian',
221
309
  vaccineCode: { system: atcVet, code: 'QI07AB04', display: 'Borrelia vaccines' },
222
- nextDoseDate: '2025-05-23', attachments: [certificate], attachmentEntryIds: [certificate.entryId],
310
+ nextDoseDate: '2025-05-23', recommendationDate: '2026-09-20T12:00:00.000Z',
311
+ attachments: [certificate], attachmentEntryIds: [certificate.entryId],
223
312
  }),
224
313
  ]);
package/dist/index.d.ts CHANGED
@@ -11,6 +11,7 @@ export * from './group.js';
11
11
  export * from './health-dcat.js';
12
12
  export * from './immunization.js';
13
13
  export * from './immunization-flat-claims.js';
14
+ export * from './immunization-coding-review.js';
14
15
  export * from './immunization-draft.js';
15
16
  export * from './international-health-card.js';
16
17
  export * from './index-projection-tags.js';
@@ -20,6 +21,7 @@ export * from './lost-animal-access.js';
20
21
  export * from './organization-application.js';
21
22
  export * from './payment.js';
22
23
  export * from './place-service-directory.js';
24
+ export * from './professional-access-policy.js';
23
25
  export * from './provenance.js';
24
26
  export * from './research-study.js';
25
27
  export * from './sectors.js';
package/dist/index.js CHANGED
@@ -11,6 +11,7 @@ export * from './group.js';
11
11
  export * from './health-dcat.js';
12
12
  export * from './immunization.js';
13
13
  export * from './immunization-flat-claims.js';
14
+ export * from './immunization-coding-review.js';
14
15
  export * from './immunization-draft.js';
15
16
  export * from './international-health-card.js';
16
17
  export * from './index-projection-tags.js';
@@ -20,6 +21,7 @@ export * from './lost-animal-access.js';
20
21
  export * from './organization-application.js';
21
22
  export * from './payment.js';
22
23
  export * from './place-service-directory.js';
24
+ export * from './professional-access-policy.js';
23
25
  export * from './provenance.js';
24
26
  export * from './research-study.js';
25
27
  export * from './sectors.js';
@@ -7,6 +7,7 @@ export declare const OrganizationParticipationRoles: Readonly<{
7
7
  readonly IndividualIndexServiceProvider: "individual-index-service-provider";
8
8
  readonly IndividualIndexDataProvider: "individual-index-data-provider";
9
9
  readonly IndividualIndexDataConsumer: "individual-index-data-consumer";
10
+ readonly IndividualPersonalBackupServiceProvider: "individual-personal-backup-service-provider";
10
11
  readonly DigitalTwinIndexServiceProvider: "digital-twin-index-service-provider";
11
12
  readonly DigitalTwinDataProvider: "digital-twin-data-provider";
12
13
  readonly DigitalTwinDataConsumer: "digital-twin-data-consumer";
@@ -35,7 +36,7 @@ export type ReusableOrganizationApplication = Readonly<{
35
36
  participationRoles: readonly OrganizationParticipationRole[];
36
37
  }>;
37
38
  /**
38
- * Converts the six explicit application roles into GW's canonical capability
39
+ * Converts the explicit application roles into the shared canonical capability
39
40
  * claim. Business roles remain distinct in the application and credential;
40
41
  * this projection only grants their corresponding provider or reader runtime
41
42
  * operations and removes duplicates.
@@ -1,5 +1,6 @@
1
1
  import { isIso3166CountryCode, isIso3166SubdivisionForCountry } from './iso-jurisdictions.js';
2
2
  import { serializeServiceCapabilityTokens, ServiceCapability, } from 'gdc-common-utils-ts/constants/service-capabilities';
3
+ import { ContinuityServiceCapability } from 'sos-data-utils-ts/continuity-service-capabilities';
3
4
  export const OrganizationOfficialIdTypes = Object.freeze({
4
5
  Tax: 'TAX',
5
6
  EmployerIdentificationNumber: 'EIN',
@@ -9,6 +10,7 @@ export const OrganizationParticipationRoles = Object.freeze({
9
10
  IndividualIndexServiceProvider: 'individual-index-service-provider',
10
11
  IndividualIndexDataProvider: 'individual-index-data-provider',
11
12
  IndividualIndexDataConsumer: 'individual-index-data-consumer',
13
+ IndividualPersonalBackupServiceProvider: 'individual-personal-backup-service-provider',
12
14
  DigitalTwinIndexServiceProvider: 'digital-twin-index-service-provider',
13
15
  DigitalTwinDataProvider: 'digital-twin-data-provider',
14
16
  DigitalTwinDataConsumer: 'digital-twin-data-consumer',
@@ -16,7 +18,7 @@ export const OrganizationParticipationRoles = Object.freeze({
16
18
  const officialIdTypes = new Set(Object.values(OrganizationOfficialIdTypes));
17
19
  const participationRoles = new Set(Object.values(OrganizationParticipationRoles));
18
20
  /**
19
- * Converts the six explicit application roles into GW's canonical capability
21
+ * Converts the explicit application roles into the shared canonical capability
20
22
  * claim. Business roles remain distinct in the application and credential;
21
23
  * this projection only grants their corresponding provider or reader runtime
22
24
  * operations and removes duplicates.
@@ -29,6 +31,8 @@ export function organizationServiceTypeForParticipationRoles(roles) {
29
31
  return ServiceCapability.IndexProvider;
30
32
  case OrganizationParticipationRoles.IndividualIndexDataConsumer:
31
33
  return ServiceCapability.IndexReader;
34
+ case OrganizationParticipationRoles.IndividualPersonalBackupServiceProvider:
35
+ return ContinuityServiceCapability.DocumentBackupProvider;
32
36
  case OrganizationParticipationRoles.DigitalTwinIndexServiceProvider:
33
37
  case OrganizationParticipationRoles.DigitalTwinDataProvider:
34
38
  return ServiceCapability.DigitalTwinProvider;
@@ -0,0 +1,2 @@
1
+ /** Vet compatibility surface; the product-neutral policy is owned by SOS. */
2
+ export * from 'sos-data-utils-ts/professional-access-policy';
@@ -0,0 +1,2 @@
1
+ /** Vet compatibility surface; the product-neutral policy is owned by SOS. */
2
+ export * from 'sos-data-utils-ts/professional-access-policy';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vet-data-utils-ts",
3
- "version": "0.5.32",
3
+ "version": "0.5.37",
4
4
  "description": "Browser-safe governed VetChain data contracts",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Connecting Solution & Applications Ltd",
@@ -60,6 +60,10 @@
60
60
  "types": "./dist/immunization-flat-claims.d.ts",
61
61
  "default": "./dist/immunization-flat-claims.js"
62
62
  },
63
+ "./immunization-coding-review": {
64
+ "types": "./dist/immunization-coding-review.d.ts",
65
+ "default": "./dist/immunization-coding-review.js"
66
+ },
63
67
  "./immunization-draft": {
64
68
  "types": "./dist/immunization-draft.d.ts",
65
69
  "default": "./dist/immunization-draft.js"
@@ -100,6 +104,10 @@
100
104
  "types": "./dist/place-service-directory.d.ts",
101
105
  "default": "./dist/place-service-directory.js"
102
106
  },
107
+ "./professional-access-policy": {
108
+ "types": "./dist/professional-access-policy.d.ts",
109
+ "default": "./dist/professional-access-policy.js"
110
+ },
103
111
  "./research-study": {
104
112
  "types": "./dist/research-study.d.ts",
105
113
  "default": "./dist/research-study.js"
@@ -149,9 +157,10 @@
149
157
  "typescript": "^5.5.4"
150
158
  },
151
159
  "dependencies": {
160
+ "fhir-data-utils-ts": "0.2.4",
152
161
  "gdc-common-utils-ts": "2.9.12",
153
162
  "pako": "^2.2.0",
154
- "sos-data-utils-ts": "0.3.1"
163
+ "sos-data-utils-ts": "0.3.7"
155
164
  },
156
165
  "engines": {
157
166
  "node": ">=20"