diff --git a/backend/compact-connect/lambdas/nodejs/package.json b/backend/compact-connect/lambdas/nodejs/package.json index 7d007fc78b..763bd27246 100644 --- a/backend/compact-connect/lambdas/nodejs/package.json +++ b/backend/compact-connect/lambdas/nodejs/package.json @@ -5,7 +5,8 @@ "description": "NodeJS lambdas for CompactConnect", "resolutions": { "fast-xml-parser": "5.7.3", - "postcss": "8.5.24" + "postcss": "8.5.24", + "sanitize-html": "2.17.5" }, "scripts": { "build": "tsc", diff --git a/backend/compact-connect/lambdas/nodejs/yarn.lock b/backend/compact-connect/lambdas/nodejs/yarn.lock index e6daa62f01..3c986c512f 100644 --- a/backend/compact-connect/lambdas/nodejs/yarn.lock +++ b/backend/compact-connect/lambdas/nodejs/yarn.lock @@ -3311,6 +3311,11 @@ csstype@^3.2.2: resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.2.3.tgz#ec48c0f3e993e50648c86da559e2610995cf989a" integrity sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ== +dayjs@^1.11.7: + version "1.11.21" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.21.tgz#57f87562e62de76f3c704bd2b8d522fc33068eb2" + integrity sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA== + debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.5, debug@^4.4.3: version "4.4.3" resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" @@ -3386,7 +3391,7 @@ domhandler@^5.0.2, domhandler@^5.0.3: dependencies: domelementtype "^2.3.0" -domutils@^3.0.1: +domutils@^3.2.2: version "3.2.2" resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.2.2.tgz#edbfe2b668b0c1d97c24baf0f1062b132221bc78" integrity sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw== @@ -3430,11 +3435,16 @@ enabled@2.0.x: resolved "https://registry.yarnpkg.com/enabled/-/enabled-2.0.0.tgz#f9dd92ec2d6f4bbc0d5d1e64e21d61cd4665e7c2" integrity sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ== -entities@^4.2.0, entities@^4.4.0: +entities@^4.2.0: version "4.5.0" resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== +entities@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/entities/-/entities-7.0.1.tgz#26e8a88889db63417dcb9a1e79a3f1bc92b5976b" + integrity sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA== + error-ex@^1.3.1: version "1.3.4" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.4.tgz#b3a8d8bb6f92eecc1629e3e27d3c8607a8a32414" @@ -3856,15 +3866,15 @@ html-escaper@^2.0.0: resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== -htmlparser2@^8.0.0: - version "8.0.2" - resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-8.0.2.tgz#f002151705b383e62433b5cf466f5b716edaec21" - integrity sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA== +htmlparser2@^10.1.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-10.1.0.tgz#fe3f2e12c73b6e462d4e10395db9c1119e4d6ae4" + integrity sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ== dependencies: domelementtype "^2.3.0" domhandler "^5.0.3" - domutils "^3.0.1" - entities "^4.4.0" + domutils "^3.2.2" + entities "^7.0.1" human-signals@^2.1.0: version "2.1.0" @@ -4447,6 +4457,13 @@ lambda-local@^2.2.0: dotenv "^16.3.1" winston "^3.10.0" +launder@^1.7.1: + version "1.7.1" + resolved "https://registry.yarnpkg.com/launder/-/launder-1.7.1.tgz#ef7155ab0c3ddec2323089c961d2e9a249aa5b0d" + integrity sha512-mU6WRz5EusL9ZZuiZ5SO4Y6C0P9PAUR9iwdb6bzj4KDihm28DiHFw+/yk9DBH4f+Pv1wuzQ4e2jV3oQ7mkIqvw== + dependencies: + dayjs "^1.11.7" + leven@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" @@ -4982,15 +4999,16 @@ safe-stable-stringify@^2.3.1: resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz#4ca2f8e385f2831c432a719b108a3bf7af42a1dd" integrity sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA== -sanitize-html@^2.17.0: - version "2.17.0" - resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.17.0.tgz#a8f66420a6be981d8fe412e3397cc753782598e4" - integrity sha512-dLAADUSS8rBwhaevT12yCezvioCA+bmUTPH/u57xKPT8d++voeYE6HeluA/bPbQ15TwDBG2ii+QZIEmYx8VdxA== +sanitize-html@2.17.5, sanitize-html@^2.17.0: + version "2.17.5" + resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.17.5.tgz#bef1d103773760e1c52a01b1f58178bee9c10556" + integrity sha512-ZmU1joGRrvoyctKIiuwUxqR6moLoU2Wk+2bMccN6f7UwhAmwYDvWziqPxRDDN2Qip62NqnIrVrT9akbL6Wretg== dependencies: deepmerge "^4.2.2" escape-string-regexp "^4.0.0" - htmlparser2 "^8.0.0" + htmlparser2 "^10.1.0" is-plain-object "^5.0.0" + launder "^1.7.1" parse-srcset "^1.0.2" postcss "^8.3.11" diff --git a/backend/compact-connect/lambdas/python/common/cc_common/data_model/schema/provider/api.py b/backend/compact-connect/lambdas/python/common/cc_common/data_model/schema/provider/api.py index 14a15603f7..6552de85cb 100644 --- a/backend/compact-connect/lambdas/python/common/cc_common/data_model/schema/provider/api.py +++ b/backend/compact-connect/lambdas/python/common/cc_common/data_model/schema/provider/api.py @@ -1,11 +1,11 @@ # ruff: noqa: N801, N815, ARG002 invalid-name unused-argument from datetime import timedelta -from marshmallow import ValidationError, validates_schema -from marshmallow.fields import UUID, AwareDateTime, Date, Email, Integer, List, Nested, Raw, String +from marshmallow import ValidationError, pre_load, validates_schema +from marshmallow.fields import UUID, AwareDateTime, Date, Dict, Email, Integer, List, Nested, Raw, String from marshmallow.validate import Length, OneOf, Range, Regexp -from cc_common.data_model.schema.base_record import ForgivingSchema +from cc_common.data_model.schema.base_record import ForgivingSchema, StrictSchema from cc_common.data_model.schema.common import CCRequestSchema, MilitaryStatus from cc_common.data_model.schema.fields import ( ActiveInactive, @@ -78,6 +78,193 @@ class ProviderSSNResponseSchema(ForgivingSchema): ssn = SocialSecurityNumber(required=True, allow_none=False) +# Maximum nesting depth accepted in a caller-supplied query. The deepest query the frontend can +# build is 13 levels (the encumbrance-date condition: bool > should > nested > query > nested > +# query > range > field > bound), so this leaves room for the UI to grow while staying well clear +# of the point where recursing through the schema would exhaust the Python stack and surface as an +# unhandled 500 instead of a 400. +_MAX_QUERY_DEPTH = 20 + + +def _validate_query_depth(query) -> None: + """ + Validate that a caller-supplied query is not nested beyond _MAX_QUERY_DEPTH. + + Walked iteratively rather than recursively: this guard has to be able to measure a query that + is deeper than the interpreter's own recursion limit, which is the very thing it exists to + reject. + + :param query: The caller-supplied query body + :raises ValidationError: If the query nests deeper than _MAX_QUERY_DEPTH + """ + stack = [(query, 1)] + while stack: + node, depth = stack.pop() + if depth > _MAX_QUERY_DEPTH: + raise ValidationError(f'Query is too deeply nested (maximum depth: {_MAX_QUERY_DEPTH}).') + if isinstance(node, dict): + stack.extend((value, depth + 1) for value in node.values()) + elif isinstance(node, list): + stack.extend((item, depth + 1) for item in node) + + +def _validate_query_input(query) -> None: + """ + Run the checks that must happen before a query is deserialized through the schema. + + Ordering matters. The depth guard runs first because ``_validate_no_cross_index_keys`` is itself + recursive and would blow the stack on a pathologically deep query. Both run ahead of field + deserialization (from a ``pre_load`` hook) so that a cross-index attempt is always reported as + such, rather than being pre-empted by, or skipped because of, an unrelated field error. + + :param query: The caller-supplied query body + :raises ValidationError: If the query is too deeply nested or attempts a cross-index lookup + """ + _validate_query_depth(query) + _validate_no_cross_index_keys(query) + + +# Keys permitted inside a nested clause's `inner_hits`. Deliberately excludes `_source`, so a caller +# cannot use inner_hits to select which nested fields come back. +_ALLOWED_INNER_HITS_KEYS = ('from', 'name', 'size') + + +class BoolQuerySchema(StrictSchema): + """ + A `bool` query clause. + + Each occupant holds further query clauses, so they recurse back into QueryClauseSchema (via a + lambda, since that schema is defined below). Inheriting StrictSchema is what rejects any bool + key the frontend does not send. + + Serialization direction: + API -> load() -> Python + """ + + must = List(Nested(lambda: QueryClauseSchema()), required=False, allow_none=False) + must_not = List(Nested(lambda: QueryClauseSchema()), required=False, allow_none=False) + should = List(Nested(lambda: QueryClauseSchema()), required=False, allow_none=False) + filter = List(Nested(lambda: QueryClauseSchema()), required=False, allow_none=False) + # Left untyped, matching the previous hand-rolled validation, which allowlisted the key without + # constraining its value. + minimum_should_match = Raw(required=False, allow_none=False) + + +class NestedQuerySchema(StrictSchema): + """ + A `nested` query clause. + + `query` recurses back into QueryClauseSchema, which is what keeps a free-form clause from being + smuggled in below a nested path. + + Serialization direction: + API -> load() -> Python + """ + + path = String(required=True, allow_none=False) + query = Nested(lambda: QueryClauseSchema(), required=True, allow_none=False) + inner_hits = Dict( + keys=String( + validate=OneOf( + _ALLOWED_INNER_HITS_KEYS, + error="'{input}' is not allowed in inner_hits. Allowed: {choices}.", + ) + ), + values=Raw(), + required=False, + allow_none=False, + ) + # Left untyped, matching the previous hand-rolled validation, which allowlisted these keys + # without constraining their values. + score_mode = Raw(required=False, allow_none=False) + ignore_unmapped = Raw(required=False, allow_none=False) + + +class QueryClauseSchema(StrictSchema): + """ + A single OpenSearch DSL query clause, restricted to the clause types the CompactConnect frontend + actually builds (see prepRequestSearchParams in webroot/src/network/searchApi/data.api.ts). + + Because a request is *loaded* through this schema rather than merely inspected, only structure + described here can reach OpenSearch at all. + + The field-keyed clauses take arbitrary field names, so their contents stay Raw: restricting + which fields may be *queried* is the job of the permission checks in the search handler, not of + this schema. Note that `terms` in particular stays permissive so that a terms lookup carrying an + `index` key is still caught by _validate_no_cross_index_keys with a cross-index error. + + Serialization direction: + API -> load() -> Python + """ + + match_all = Dict(required=False, allow_none=False) + term = Dict(keys=String(), values=Raw(), required=False, allow_none=False) + terms = Dict( + keys=String(), + values=List( + Raw(), + error_messages={'invalid': 'A terms query must be a plain list of values.'}, + ), + required=False, + allow_none=False, + ) + match = Dict(keys=String(), values=Raw(), required=False, allow_none=False) + match_phrase_prefix = Dict(keys=String(), values=Raw(), required=False, allow_none=False) + range = Dict(keys=String(), values=Raw(), required=False, allow_none=False) + bool = Nested(BoolQuerySchema, required=False, allow_none=False) + nested = Nested(NestedQuerySchema, required=False, allow_none=False) + + +# Sort shape the search API accepts, derived from what the CompactConnect frontend actually emits +# (see prepRequestSearchParams in webroot/src/network/searchApi/data.api.ts), which is exactly: +# +# sort: [{'.keyword': {'order': 'asc' | 'desc'}}] +# +# This is modelled declaratively rather than walked, so anything not described here is rejected by +# marshmallow itself. +_ALLOWED_SORT_ORDERS = ('asc', 'desc') + +# Fields a caller is permitted to sort by. The frontend only ever sorts by family name, but +# providerId and the date fields are kept available so `search_after` cursor pagination has a unique +# tiebreaker to sort on. +# +# This list is deliberately restricted to non-sensitive fields. Sort values are echoed back to the +# caller as `lastSort` (see the search handler), which is outside the response schema that strips +# restricted fields -- so whatever is sortable is also readable. Sensitive fields (dateOfBirth, +# emailAddress, phoneNumber, home address, ...) are therefore not sortable by anyone, regardless of +# scopes. Note this is independent of *querying*: querying by dateOfBirth is still supported for +# callers holding readPrivate. +# +# Text fields are only listed via their `.keyword` subfield, since OpenSearch rejects sorting on an +# analyzed text field anyway. +_ALLOWED_SORT_FIELDS = ( + 'dateOfExpiration', + 'dateOfUpdate', + 'familyName.keyword', + 'givenName.keyword', + 'providerId', +) + + +class SortOptionsSchema(StrictSchema): + """ + Sort options for a single sorted field, e.g. {'order': 'asc'}. + + Inheriting StrictSchema (unknown = RAISE) is what rejects every sort option the frontend does + not send -- including `nested`, whose `filter` would otherwise be an unvalidated query-DSL + position. + + Serialization direction: + API -> load() -> Python + """ + + order = String( + required=True, + allow_none=False, + validate=OneOf(_ALLOWED_SORT_ORDERS, error="Invalid sort order '{input}'. Allowed values: {choices}."), + ) + + class ProviderReadPrivateResponseSchema(ForgivingSchema): """ Provider object fields that are sanitized for users with the 'readPrivate' permission. @@ -494,7 +681,9 @@ class SearchProvidersRequestSchema(CCRequestSchema): Schema for advanced search providers requests. This schema is used to validate incoming requests to the advanced search providers API endpoint. - It accepts an OpenSearch DSL query body for flexible querying of the provider index. + It accepts an OpenSearch DSL query body, restricted to the clause types QueryClauseSchema + declares -- the ones the frontend actually builds. Free-form and scripted clauses are not + accepted; see QueryClauseSchema for why. The request body closely mirrors OpenSearch DSL for pagination using `search_after`. See: https://docs.opensearch.org/latest/search-plugins/searching-data/paginate/#the-search_after-parameter @@ -503,8 +692,8 @@ class SearchProvidersRequestSchema(CCRequestSchema): API -> load() -> Python """ - # The OpenSearch query body - we use Raw to allow the full flexibility of OpenSearch queries - query = Raw(required=True, allow_none=False) + # The OpenSearch query body, restricted to the clause types QueryClauseSchema declares + query = Nested(QueryClauseSchema, required=True, allow_none=False) # Pagination parameters following OpenSearch DSL # 'from' is a reserved word in Python, so we use 'from_' with data_key='from' @@ -513,28 +702,51 @@ class SearchProvidersRequestSchema(CCRequestSchema): # Sort order - required when using search_after pagination # Example: [{"providerId": "asc"}, {"dateOfUpdate": "desc"}] - sort = Raw(required=False, allow_none=False) + sort = List( + Dict( + keys=String( + validate=OneOf( + _ALLOWED_SORT_FIELDS, + error="Sorting by '{input}' is not allowed. Sortable fields: {choices}.", + ) + ), + values=Nested(SortOptionsSchema), + ), + required=False, + allow_none=False, + ) # The search_after parameter for cursor-based pagination # This should be the 'sort' values from the last hit of the previous page # Example: ["provider-uuid-123", "2024-01-15T10:30:00Z"] search_after = Raw(required=False, allow_none=False) - @validates_schema - def validate_no_cross_index_queries(self, data, **kwargs): + @pre_load + def validate_search_request_dsl(self, data, **kwargs): """ - Validate that the query does not contain cross-index lookup attempts. - - This is a defense-in-depth security measure to prevent queries that attempt to access - data from other compact indices. The primary protection is the OpenSearch domain setting - `rest.action.multi.allow_explicit_index: false`, but this validation provides an - additional application-layer check. - - Dangerous patterns blocked: - - Terms lookup with external index: {"terms": {"field": {"index": "other_index", ...}}} - - More like this with external docs: {"more_like_this": {"like": [{"_index": "other_index"}]}} + Validate the caller-supplied OpenSearch DSL in the search request body. + + Three application-layer checks are applied: + + 1. Query clause allowlist: only the structured query types the frontend builds are accepted. + Free-form and scripted clauses are rejected, since they can carry a field reference + inside a string value and thereby slip past field-level permission checks. + 2. Cross-index lookups: queries that attempt to reach another compact's index are blocked. + Note that this application-layer check is currently the only protection against + cross-index lookups. The OpenSearch domain setting + `rest.action.multi.allow_explicit_index: false` would provide a second layer, but it is + not configured on the domain due to issues it causes with blue/green deployments + (see stacks/search_persistent_stack/provider_search_domain.py). + + Dangerous patterns blocked: + - Terms lookup with external index: {"terms": {"field": {"index": "other_index", ...}}} + - More like this with external docs: {"more_like_this": {"like": [{"_index": "other"}]}} """ - _validate_no_cross_index_keys(data.get('query', {})) + # Runs ahead of field deserialization, so `data` is still exactly what the caller sent and + # is not guaranteed to be an object. Anything else is left for the field layer to reject. + if isinstance(data, dict): + _validate_query_input(data.get('query', {})) + return data class ExportPrivilegesRequestSchema(CCRequestSchema): @@ -542,20 +754,26 @@ class ExportPrivilegesRequestSchema(CCRequestSchema): Schema for Exporting list of privileges into CSV file. This schema is used to validate incoming requests to the advanced search providers API endpoint. - It accepts an OpenSearch DSL query body for flexible querying of the provider index. + It accepts an OpenSearch DSL query body, restricted to the clause types QueryClauseSchema + declares -- the ones the frontend actually builds. Free-form and scripted clauses are not + accepted; see QueryClauseSchema for why. Serialization direction: API -> load() -> Python """ - # The OpenSearch query body - we use Raw to allow the full flexibility of OpenSearch queries - query = Raw(required=True, allow_none=False) + # The OpenSearch query body, restricted to the clause types QueryClauseSchema declares + query = Nested(QueryClauseSchema, required=True, allow_none=False) - @validates_schema - def validate_no_cross_index_queries(self, data, **kwargs): + @pre_load + def validate_query_dsl(self, data, **kwargs): """ - Validate that the query does not contain cross-index lookup attempts. + Apply the pre-deserialization query checks (nesting depth, cross-index lookups). - This is a defense-in-depth security measure. See SearchProvidersRequestSchema for details. + See SearchProvidersRequestSchema for details. """ - _validate_no_cross_index_keys(data.get('query', {})) + # Runs ahead of field deserialization, so `data` is still exactly what the caller sent and + # is not guaranteed to be an object. Anything else is left for the field layer to reject. + if isinstance(data, dict): + _validate_query_input(data.get('query', {})) + return data diff --git a/backend/compact-connect/lambdas/python/purchases/requirements-dev.in b/backend/compact-connect/lambdas/python/purchases/requirements-dev.in index 88e733e35a..8cbec55232 100644 --- a/backend/compact-connect/lambdas/python/purchases/requirements-dev.in +++ b/backend/compact-connect/lambdas/python/purchases/requirements-dev.in @@ -11,7 +11,7 @@ Faker>=40.28.1, <41 argon2-cffi>=25.1.0, <26.0.0 aws-lambda-powertools>=3.31.0, <4 boto3>=1.43.46, <2 -cryptography>=48.0.1, <49 +cryptography>=50.0.0, <51 marshmallow>=4.3.0, <5.0.0 requests>=2.31.0, <3.0.0 urllib3>=2.7.0, <3 diff --git a/backend/compact-connect/lambdas/python/purchases/requirements-dev.txt b/backend/compact-connect/lambdas/python/purchases/requirements-dev.txt index 6f1fe8e884..1668157038 100644 --- a/backend/compact-connect/lambdas/python/purchases/requirements-dev.txt +++ b/backend/compact-connect/lambdas/python/purchases/requirements-dev.txt @@ -1,5 +1,5 @@ # -# This file is autogenerated by pip-compile with Python 3.12 +# This file is autogenerated by pip-compile with Python 3.14 # by the following command: # # pip-compile --no-emit-index-url --no-strip-extras requirements-dev.in @@ -8,15 +8,15 @@ argon2-cffi==25.1.0 # via -r requirements-dev.in argon2-cffi-bindings==25.1.0 # via argon2-cffi -aws-lambda-powertools==3.31.0 +aws-lambda-powertools==3.31.1 # via -r requirements-dev.in boolean-py==5.0 # via license-expression -boto3==1.43.46 +boto3==1.43.63 # via # -r requirements-dev.in # moto -botocore==1.43.46 +botocore==1.43.63 # via # boto3 # moto @@ -27,21 +27,21 @@ cachecontrol[filecache]==0.14.4 # via # cachecontrol # pip-audit -certifi==2026.6.17 +certifi==2026.7.22 # via requests -cffi==2.0.0 +cffi==2.1.1 # via # argon2-cffi-bindings # cryptography -charset-normalizer==3.4.7 +charset-normalizer==3.4.9 # via requests -click==8.4.1 +click==8.4.2 # via pip-tools -coverage[toml]==7.15.1 +coverage[toml]==7.15.3 # via # -r requirements-dev.in # pytest-cov -cryptography==48.0.1 +cryptography==50.0.0 # via # -r requirements-dev.in # moto @@ -49,11 +49,11 @@ cyclonedx-python-lib==11.11.0 # via pip-audit defusedxml==0.7.1 # via py-serializable -docker==7.1.0 +docker==7.2.0 # via moto -faker==40.28.1 +faker==40.36.0 # via -r requirements-dev.in -filelock==3.29.4 +filelock==3.32.2 # via cachecontrol idna==3.18 # via requests @@ -80,7 +80,7 @@ msgpack==1.2.1 # via cachecontrol packageurl-python==0.17.6 # via cyclonedx-python-lib -packaging==26.2 +packaging==26.3 # via # build # pip-audit @@ -93,9 +93,9 @@ pip-audit==2.10.1 # via -r requirements-dev.in pip-requirements-parser==32.0.1 # via pip-audit -pip-tools==7.5.3 +pip-tools==7.6.0 # via -r requirements-dev.in -platformdirs==4.10.0 +platformdirs==4.11.0 # via pip-audit pluggy==1.6.0 # via @@ -137,13 +137,13 @@ requests==2.34.2 # moto # pip-audit # responses -responses==0.26.1 +responses==0.26.2 # via moto rich==15.0.0 # via pip-audit -ruff==0.15.21 +ruff==0.16.1 # via -r requirements-dev.in -s3transfer==0.19.0 +s3transfer==0.19.2 # via boto3 six==1.17.0 # via python-dateutil @@ -153,10 +153,8 @@ tomli==2.4.1 # via pip-audit tomli-w==1.2.0 # via pip-audit -typing-extensions==4.15.0 - # via - # aws-lambda-powertools - # cyclonedx-python-lib +typing-extensions==4.16.0 + # via aws-lambda-powertools urllib3==2.7.0 # via # -r requirements-dev.in diff --git a/backend/compact-connect/lambdas/python/purchases/requirements.txt b/backend/compact-connect/lambdas/python/purchases/requirements.txt index f3dcf61fd5..e9788ea89a 100644 --- a/backend/compact-connect/lambdas/python/purchases/requirements.txt +++ b/backend/compact-connect/lambdas/python/purchases/requirements.txt @@ -1,20 +1,20 @@ # -# This file is autogenerated by pip-compile with Python 3.12 +# This file is autogenerated by pip-compile with Python 3.14 # by the following command: # # pip-compile --no-emit-index-url --no-strip-extras requirements.in # authorizenet==1.1.6 # via -r requirements.in -certifi==2026.6.17 +certifi==2026.7.22 # via requests -charset-normalizer==3.4.7 +charset-normalizer==3.4.9 # via requests idna==3.18 # via requests lxml==4.9.4 # via authorizenet -pyxb-x==1.2.6.3 +pyxb-x==1.2.6.4 # via authorizenet requests==2.34.2 # via authorizenet diff --git a/backend/compact-connect/lambdas/python/search/tests/function/test_search_privileges.py b/backend/compact-connect/lambdas/python/search/tests/function/test_search_privileges.py index 3ada04c72e..f6d019c3e2 100644 --- a/backend/compact-connect/lambdas/python/search/tests/function/test_search_privileges.py +++ b/backend/compact-connect/lambdas/python/search/tests/function/test_search_privileges.py @@ -680,22 +680,25 @@ def test_export_query_with_index_key_returns_400(self): self.assertIn("'index'", body['message']) def test_export_query_with_underscore_index_key_returns_400(self): - """Test that export queries containing '_index' key are rejected with 400 error.""" + """ + Test that export queries containing an '_index' key are rejected with a 400 error. + + The original vector for '_index' was more_like_this, which the query clause allowlist now + rejects outright (covered by test_export_with_disallowed_free_form_clauses_returns_400). + This exercises the cross-index check itself, by placing '_index' inside an allowlisted + clause so it has to be caught on its own merits rather than by the allowlist. + """ from handlers.search import search_api_handler - # Test with '_index' key (more_like_this attack pattern) event = self._create_api_event( 'aslp', body={ 'query': { - 'more_like_this': { - 'fields': ['familyName', 'givenName'], - 'like': [ - { - '_index': 'compact_octp_providers', - '_id': 'target-provider-uuid', - } - ], + 'terms': { + 'providerId': { + '_index': 'compact_octp_providers', + '_id': 'target-provider-uuid', + } } } }, @@ -708,6 +711,37 @@ def test_export_query_with_underscore_index_key_returns_400(self): self.assertIn('Cross-index queries are not allowed', body['message']) self.assertIn("'_index'", body['message']) + @patch('handlers.search.opensearch_client') + def test_export_with_disallowed_free_form_clauses_returns_400(self, mock_opensearch_client): + """ + The privilege export endpoint accepts the same raw DSL as provider search, so it must + apply the same query clause allowlist. + """ + from handlers.search import search_api_handler + + disallowed_queries = { + 'query_string': {'query_string': {'query': 'licenses.ssnLastFour:1234'}}, + 'simple_query_string': {'simple_query_string': {'query': 'licenses.ssnLastFour:1234'}}, + 'wildcard': {'wildcard': {'familyName': {'value': 'Do*'}}}, + 'regexp': {'regexp': {'familyName': 'Do.*'}}, + 'script': {'script': {'script': {'source': "doc['familyName'].size() > 0"}}}, + 'exists': {'exists': {'field': 'licenses.ssnLastFour'}}, + 'multi_match': {'multi_match': {'query': 'Doe', 'fields': ['givenName', 'licenses.ssnLastFour']}}, + 'more_like_this': {'more_like_this': {'like': [{'_index': 'compact_octp_providers'}]}}, + } + + for clause_name, query in disallowed_queries.items(): + with self.subTest(clause=clause_name): + event = self._create_api_event('aslp', body={'query': query}) + + response = search_api_handler(event, self.mock_context) + + self.assertEqual(400, response['statusCode']) + body = json.loads(response['body']) + self.assertIn(clause_name, body['message']) + + mock_opensearch_client.search.assert_not_called() + def test_export_query_with_nested_index_key_returns_400(self): """Test that export queries with nested 'index' key at any level are rejected.""" from handlers.search import search_api_handler @@ -831,3 +865,38 @@ def test_privilege_with_mismatched_compact_is_filtered_from_response(self, mock_ self.assertEqual(404, response['statusCode']) body = json.loads(response['body']) self.assertEqual('The search parameters did not match any privileges.', body['message']) + + @patch('handlers.search.opensearch_client') + def test_export_with_excessively_nested_query_returns_400(self, mock_opensearch_client): + """The export endpoint uses its own request schema, so it must apply the same depth guard.""" + from handlers.search import search_api_handler + + query = {'match_all': {}} + for _ in range(300): + query = {'bool': {'must': [query]}} + + event = self._create_api_event('aslp', body={'query': query}) + + response = search_api_handler(event, self.mock_context) + + self.assertEqual(400, response['statusCode']) + body = json.loads(response['body']) + self.assertIn('too deeply nested', body['message']) + mock_opensearch_client.search.assert_not_called() + + @patch('handlers.search.opensearch_client') + def test_export_with_terms_lookup_object_returns_400(self, mock_opensearch_client): + """The export endpoint must reject the terms-lookup object form, same as provider search.""" + from handlers.search import search_api_handler + + event = self._create_api_event( + 'aslp', + body={'query': {'terms': {'providerId': {'id': 'some-other-provider', 'path': 'providerId'}}}}, + ) + + response = search_api_handler(event, self.mock_context) + + self.assertEqual(400, response['statusCode']) + body = json.loads(response['body']) + self.assertIn('terms', body['message']) + mock_opensearch_client.search.assert_not_called() diff --git a/backend/compact-connect/lambdas/python/search/tests/function/test_search_providers.py b/backend/compact-connect/lambdas/python/search/tests/function/test_search_providers.py index 02ae82529f..04107f71a0 100644 --- a/backend/compact-connect/lambdas/python/search/tests/function/test_search_providers.py +++ b/backend/compact-connect/lambdas/python/search/tests/function/test_search_providers.py @@ -189,7 +189,7 @@ def test_search_with_sort_parameter(self, mock_opensearch_client): self._when_testing_mock_opensearch_client(mock_opensearch_client) - sort_config = [{'providerId': 'asc'}, {'dateOfUpdate': 'desc'}] + sort_config = [{'providerId': {'order': 'asc'}}, {'dateOfUpdate': {'order': 'desc'}}] search_after_values = ['provider-uuid-123'] event = self._create_api_event( 'aslp', @@ -313,7 +313,7 @@ def test_search_response_includes_last_sort_for_pagination(self, mock_opensearch 'aslp', body={ 'query': {'match_all': {}}, - 'sort': [{'providerId': 'asc'}, {'dateOfUpdate': 'asc'}], + 'sort': [{'providerId': {'order': 'asc'}}, {'dateOfUpdate': {'order': 'asc'}}], }, ) @@ -381,22 +381,23 @@ def test_query_with_index_key_returns_400(self): self.assertIn("'index'", body['message']) def test_query_with_underscore_index_key_returns_400(self): - """Test that queries containing '_index' key are rejected with 400 error.""" + """ + Test that queries containing an '_index' key are rejected with a 400 error. + + This exercises the cross-index check itself, by placing '_index' inside an allowlisted + clause so it has to be caught on its own merits rather than by the allowlist. + """ from handlers.search import search_api_handler - # Test with '_index' key (more_like_this attack pattern) event = self._create_api_event( 'aslp', body={ 'query': { - 'more_like_this': { - 'fields': ['familyName', 'givenName'], - 'like': [ - { - '_index': 'compact_octp_providers', - '_id': 'target-provider-uuid', - } - ], + 'terms': { + 'providerId': { + '_index': 'compact_octp_providers', + '_id': 'target-provider-uuid', + } } } }, @@ -409,6 +410,182 @@ def test_query_with_underscore_index_key_returns_400(self): self.assertIn('Cross-index queries are not allowed', body['message']) self.assertIn("'_index'", body['message']) + @patch('handlers.search.opensearch_client') + def test_search_with_query_string_clause_returns_400(self, mock_opensearch_client): + """ + A free-form query_string clause must be rejected outright. + + query_string smuggles the field reference inside a string value rather than exposing it as + a structural key, which defeats any field-name-based permission gate and lets a caller use + hit-vs-no-hit as an oracle against fields they should not be able to query. + """ + from handlers.search import search_api_handler + + self._when_testing_mock_opensearch_client(mock_opensearch_client) + + event = self._create_api_event( + 'aslp', + body={ + 'query': { + 'bool': { + 'must': [ + {'term': {'providerId': ''}}, + {'query_string': {'query': 'licenses.ssnLastFour:1234'}}, + ] + } + } + }, + ) + + response = search_api_handler(event, self.mock_context) + + self.assertEqual(400, response['statusCode']) + body = json.loads(response['body']) + self.assertIn('query_string', body['message']) + mock_opensearch_client.search.assert_not_called() + + @patch('handlers.search.opensearch_client') + def test_search_with_disallowed_free_form_clauses_returns_400(self, mock_opensearch_client): + """Every free-form/scripted clause type outside the allowlist must be rejected with a 400.""" + from handlers.search import search_api_handler + + self._when_testing_mock_opensearch_client(mock_opensearch_client) + + disallowed_queries = { + 'simple_query_string': {'simple_query_string': {'query': 'licenses.ssnLastFour:1234'}}, + 'wildcard': {'wildcard': {'familyName': {'value': 'Do*'}}}, + 'regexp': {'regexp': {'familyName': 'Do.*'}}, + 'script': {'script': {'script': {'source': "doc['familyName'].size() > 0"}}}, + 'exists': {'exists': {'field': 'licenses.ssnLastFour'}}, + 'multi_match': {'multi_match': {'query': 'Doe', 'fields': ['givenName', 'licenses.ssnLastFour']}}, + 'more_like_this': {'more_like_this': {'like': [{'_index': 'compact_octp_providers'}]}}, + } + + for clause_name, query in disallowed_queries.items(): + with self.subTest(clause=clause_name): + event = self._create_api_event('aslp', body={'query': query}) + + response = search_api_handler(event, self.mock_context) + + self.assertEqual(400, response['statusCode']) + body = json.loads(response['body']) + self.assertIn(clause_name, body['message']) + + mock_opensearch_client.search.assert_not_called() + + @patch('handlers.search.opensearch_client') + def test_search_with_disallowed_clause_nested_deep_in_query_returns_400(self, mock_opensearch_client): + """A disallowed clause buried inside nested/bool structures must still be rejected.""" + from handlers.search import search_api_handler + + self._when_testing_mock_opensearch_client(mock_opensearch_client) + + event = self._create_api_event( + 'aslp', + body={ + 'query': { + 'bool': { + 'must': [ + {'match': {'givenName': 'John'}}, + { + 'nested': { + 'path': 'privileges', + 'query': { + 'bool': { + 'should': [ + {'term': {'privileges.jurisdiction': 'oh'}}, + {'query_string': {'query': 'privileges.privilegeId:*'}}, + ] + } + }, + } + }, + ] + } + } + }, + ) + + response = search_api_handler(event, self.mock_context) + + self.assertEqual(400, response['statusCode']) + body = json.loads(response['body']) + self.assertIn('query_string', body['message']) + mock_opensearch_client.search.assert_not_called() + + @patch('handlers.search.opensearch_client') + def test_search_accepts_the_full_query_shape_built_by_the_frontend(self, mock_opensearch_client): + """ + Regression guard: the complete clause vocabulary the frontend builds must remain accepted. + + Mirrors prepRequestSearchParams in webroot/src/network/searchApi/data.api.ts, combining every + clause type it can emit into a single request. + """ + from handlers.search import search_api_handler + + self._when_testing_mock_opensearch_client(mock_opensearch_client) + + event = self._create_api_event( + 'aslp', + body={ + 'query': { + 'bool': { + 'must': [ + {'match_phrase_prefix': {'givenName': 'Jo'}}, + {'match_phrase_prefix': {'familyName': 'Do'}}, + {'term': {'licenseJurisdiction': 'oh'}}, + {'term': {'militaryStatus': 'active'}}, + {'match': {'npi': '1234567890'}}, + {'match': {'licenseNumber': 'A-123'}}, + { + 'nested': { + 'path': 'privileges', + 'inner_hits': {'size': 100}, + 'query': { + 'bool': { + 'must': [{'term': {'privileges.jurisdiction': 'ky'}}], + 'should': [ + {'range': {'privileges.dateOfIssuance': {'gte': '2024-01-01'}}}, + {'range': {'privileges.dateOfRenewal': {'lte': '2024-12-31'}}}, + ], + 'minimum_should_match': 1, + } + }, + } + }, + { + 'bool': { + 'must_not': [ + { + 'nested': { + 'path': 'licenses', + 'query': { + 'nested': { + 'path': 'licenses.investigations', + 'query': { + 'term': {'licenses.investigations.type': 'investigation'} + }, + } + }, + } + } + ] + } + }, + ] + } + }, + 'size': 25, + 'from': 25, + 'sort': [{'familyName.keyword': {'order': 'asc'}}], + }, + ) + + response = search_api_handler(event, self.mock_context) + + self.assertEqual(200, response['statusCode']) + mock_opensearch_client.search.assert_called_once() + def test_query_with_nested_index_key_returns_400(self): """Test that queries with nested 'index' key at any level are rejected.""" from handlers.search import search_api_handler @@ -460,7 +637,7 @@ def test_opensearch_request_error_returns_400_with_error_message(self, mock_open 'aslp', body={ 'query': {'match_all': {}}, - 'sort': [{'familyName': 'asc'}], # Sorting on text field causes this error + 'sort': [{'familyName.keyword': {'order': 'asc'}}], }, ) @@ -523,3 +700,368 @@ def test_provider_with_mismatched_compact_is_filtered_from_response(self, mock_o body = json.loads(response['body']) # should be empty list with total value of 0 self.assertEqual({'providers': [], 'total': {'relation': 'eq', 'value': 0}}, body) + + @patch('handlers.search.opensearch_client') + def test_search_with_script_sort_returns_400(self, mock_opensearch_client): + """ + A scripted sort must be rejected outright. + + A _script sort computes an arbitrary value per document, and the handler echoes sort values + straight back to the caller as `lastSort` -- outside the response schema that otherwise + strips restricted fields. That makes a scripted sort a direct read primitive for any indexed + field, not merely an oracle, so it must never reach OpenSearch. + """ + from handlers.search import search_api_handler + + self._when_testing_mock_opensearch_client(mock_opensearch_client) + + event = self._create_api_event( + 'aslp', + body={ + 'query': {'match_all': {}}, + 'sort': [ + { + '_script': { + 'type': 'string', + 'script': {'source': "doc['licenses.emailAddress'].value"}, + 'order': 'asc', + } + } + ], + }, + ) + + response = search_api_handler(event, self.mock_context) + + self.assertEqual(400, response['statusCode']) + body = json.loads(response['body']) + self.assertIn('_script', body['message']) + mock_opensearch_client.search.assert_not_called() + + @patch('handlers.search.opensearch_client') + def test_search_with_nested_filter_in_sort_returns_400(self, mock_opensearch_client): + """ + A sort entry's `nested.filter` is a full query-DSL position, so it must not be accepted. + + Without this, a free-form clause could be smuggled through sort even though the query itself + is allowlisted, re-opening the restricted-field oracle through a different door. + """ + from handlers.search import search_api_handler + + self._when_testing_mock_opensearch_client(mock_opensearch_client) + + event = self._create_api_event( + 'aslp', + body={ + 'query': {'match_all': {}}, + 'sort': [ + { + 'dateOfExpiration': { + 'order': 'asc', + 'nested': { + 'path': 'licenses', + 'filter': { + 'query_string': {'query': 'licenses.emailAddress:[1985-01-01 TO 1985-06-30]'} + }, + }, + } + } + ], + }, + ) + + response = search_api_handler(event, self.mock_context) + + self.assertEqual(400, response['statusCode']) + body = json.loads(response['body']) + self.assertIn('nested', body['message']) + mock_opensearch_client.search.assert_not_called() + + @patch('handlers.search.opensearch_client') + def test_search_with_disallowed_sort_options_returns_400(self, mock_opensearch_client): + """Only the `order` sort option the frontend emits is accepted; every other option is rejected.""" + from handlers.search import search_api_handler + + self._when_testing_mock_opensearch_client(mock_opensearch_client) + + disallowed_sorts = { + 'nested': [{'dateOfExpiration': {'order': 'asc', 'nested': {'path': 'licenses'}}}], + 'missing': [{'familyName.keyword': {'order': 'asc', 'missing': '_last'}}], + 'unmapped_type': [{'familyName.keyword': {'order': 'asc', 'unmapped_type': 'keyword'}}], + 'mode': [{'dateOfExpiration': {'order': 'asc', 'mode': 'max'}}], + 'format': [{'dateOfExpiration': {'order': 'asc', 'format': 'strict_date_optional_time'}}], + } + + for option_name, sort in disallowed_sorts.items(): + with self.subTest(option=option_name): + event = self._create_api_event('aslp', body={'query': {'match_all': {}}, 'sort': sort}) + + response = search_api_handler(event, self.mock_context) + + self.assertEqual(400, response['statusCode']) + body = json.loads(response['body']) + self.assertIn(option_name, body['message']) + + mock_opensearch_client.search.assert_not_called() + + @patch('handlers.search.opensearch_client') + def test_search_with_invalid_sort_order_value_returns_400(self, mock_opensearch_client): + """The sort order must be one of the two values the frontend's SortDirection enum can emit.""" + from handlers.search import search_api_handler + + self._when_testing_mock_opensearch_client(mock_opensearch_client) + + event = self._create_api_event( + 'aslp', + body={'query': {'match_all': {}}, 'sort': [{'familyName.keyword': {'order': 'sideways'}}]}, + ) + + response = search_api_handler(event, self.mock_context) + + self.assertEqual(400, response['statusCode']) + body = json.loads(response['body']) + self.assertIn('sideways', body['message']) + mock_opensearch_client.search.assert_not_called() + + @patch('handlers.search.opensearch_client') + def test_search_accepts_the_sort_shape_built_by_the_frontend(self, mock_opensearch_client): + """ + Regression guard: the sort shape the frontend builds must remain accepted. + + Mirrors prepRequestSearchParams in webroot/src/network/searchApi/data.api.ts, which emits + [{'.keyword': {'order': 'asc' | 'desc'}}]. + """ + from handlers.search import search_api_handler + + for order in ('asc', 'desc'): + with self.subTest(order=order): + mock_opensearch_client.reset_mock() + self._when_testing_mock_opensearch_client(mock_opensearch_client) + + event = self._create_api_event( + 'aslp', + body={'query': {'match_all': {}}, 'sort': [{'familyName.keyword': {'order': order}}]}, + ) + + response = search_api_handler(event, self.mock_context) + + self.assertEqual(200, response['statusCode']) + mock_opensearch_client.search.assert_called_once() + + @patch('handlers.search.opensearch_client') + def test_search_accepts_multi_field_sort_for_search_after_pagination(self, mock_opensearch_client): + """Multiple sort entries must remain accepted, since search_after pagination needs a tiebreaker.""" + from handlers.search import search_api_handler + + self._when_testing_mock_opensearch_client(mock_opensearch_client) + + event = self._create_api_event( + 'aslp', + body={ + 'query': {'match_all': {}}, + 'sort': [{'familyName.keyword': {'order': 'asc'}}, {'providerId': {'order': 'asc'}}], + 'search_after': ['Doe', '00000000-0000-0000-0000-000000000001'], + }, + ) + + response = search_api_handler(event, self.mock_context) + + self.assertEqual(200, response['statusCode']) + mock_opensearch_client.search.assert_called_once() + + @patch('handlers.search.opensearch_client') + def test_search_with_non_allowlisted_sort_field_returns_400(self, mock_opensearch_client): + """ + Sorting is restricted to an allowlist of non-sensitive fields. + + Sort values are echoed back to the caller as `lastSort`, outside the response schema that + strips restricted fields. Restricting which fields can be sorted on is what keeps that echo + from becoming a read primitive for sensitive data. + """ + from handlers.search import search_api_handler + + self._when_testing_mock_opensearch_client(mock_opensearch_client) + + sensitive_sort_fields = [ + 'licenses.emailAddress', + 'licenses.phoneNumber', + 'licenses.homeAddressStreet1', + 'licenses.homeAddressPostalCode', + 'ssnLastFour', + ] + + for field_name in sensitive_sort_fields: + with self.subTest(field=field_name): + event = self._create_api_event( + 'aslp', + body={'query': {'match_all': {}}, 'sort': [{field_name: {'order': 'asc'}}]}, + ) + + response = search_api_handler(event, self.mock_context) + + self.assertEqual(400, response['statusCode']) + body = json.loads(response['body']) + self.assertIn(field_name, body['message']) + + mock_opensearch_client.search.assert_not_called() + + @patch('handlers.search.opensearch_client') + def test_search_accepts_every_allowlisted_sort_field(self, mock_opensearch_client): + """Regression guard: each allowlisted sort field must remain usable.""" + from handlers.search import search_api_handler + + allowed_sort_fields = [ + 'familyName.keyword', + 'givenName.keyword', + 'providerId', + 'dateOfUpdate', + 'dateOfExpiration', + ] + + for field_name in allowed_sort_fields: + with self.subTest(field=field_name): + mock_opensearch_client.reset_mock() + self._when_testing_mock_opensearch_client(mock_opensearch_client) + + event = self._create_api_event( + 'aslp', + body={'query': {'match_all': {}}, 'sort': [{field_name: {'order': 'asc'}}]}, + ) + + response = search_api_handler(event, self.mock_context) + + self.assertEqual(200, response['statusCode']) + mock_opensearch_client.search.assert_called_once() + + @patch('handlers.search.opensearch_client') + def test_search_with_excessively_nested_query_returns_400(self, mock_opensearch_client): + """ + A pathologically nested query is refused with a 400 rather than blowing the Python stack. + + The declarative schema recurses through nested/bool clauses, so an arbitrarily deep query + would otherwise raise RecursionError and surface as an unhandled 500. + """ + from handlers.search import search_api_handler + + self._when_testing_mock_opensearch_client(mock_opensearch_client) + + query = {'match_all': {}} + for _ in range(300): + query = {'bool': {'must': [query]}} + + event = self._create_api_event('aslp', body={'query': query}) + + response = search_api_handler(event, self.mock_context) + + self.assertEqual(400, response['statusCode']) + body = json.loads(response['body']) + self.assertIn('too deeply nested', body['message']) + mock_opensearch_client.search.assert_not_called() + + @patch('handlers.search.opensearch_client') + def test_search_with_terms_lookup_object_returns_400(self, mock_opensearch_client): + """ + A terms clause must be a plain list of values; the terms-lookup object form is refused. + + This is enforced structurally by the schema, independently of the cross-index key check: + the lookup below names no index at all, so it reads from the current index and carries no + 'index'/'_index' key for that check to find. + """ + from handlers.search import search_api_handler + + self._when_testing_mock_opensearch_client(mock_opensearch_client) + + event = self._create_api_event( + 'aslp', + body={'query': {'terms': {'providerId': {'id': 'some-other-provider', 'path': 'providerId'}}}}, + ) + + response = search_api_handler(event, self.mock_context) + + self.assertEqual(400, response['statusCode']) + body = json.loads(response['body']) + self.assertIn('terms', body['message']) + mock_opensearch_client.search.assert_not_called() + + @patch('handlers.search.opensearch_client') + def test_search_with_non_object_request_body_returns_400(self, mock_opensearch_client): + """ + A request body that is not a JSON object is refused with a 400, not an unhandled error. + + The checks that run ahead of field deserialization see the caller's raw input, which is not + guaranteed to be an object, so they must not assume they can index into it. + """ + from handlers.search import search_api_handler + + self._when_testing_mock_opensearch_client(mock_opensearch_client) + + for raw_body in ('[]', '"a string"', 'null', '42'): + with self.subTest(body=raw_body): + event = self._create_api_event('aslp', body={'query': {'match_all': {}}}) + event['body'] = raw_body + + response = search_api_handler(event, self.mock_context) + + self.assertEqual(400, response['statusCode']) + + mock_opensearch_client.search.assert_not_called() + + @patch('handlers.search.opensearch_client') + def test_search_accepts_the_deepest_query_the_frontend_can_build(self, mock_opensearch_client): + """ + The deepest query the frontend can emit must stay within the nesting cap. + + This is the encumbrance-date condition from prepRequestSearchParams, which pairs two nested + levels with a range bound and is the frontend's worst case. It pins _MAX_QUERY_DEPTH against + a real query, so the cap cannot be tuned down far enough to break the UI unnoticed. + """ + from handlers.search import search_api_handler + + self._when_testing_mock_opensearch_client(mock_opensearch_client) + + def encumbrance_condition(top_path: str) -> dict: + nested_path = f'{top_path}.adverseActions' + return { + 'nested': { + 'path': top_path, + 'query': { + 'nested': { + 'path': nested_path, + 'query': { + 'range': { + f'{nested_path}.effectiveStartDate': { + 'gte': '2024-01-01', + 'lte': '2024-12-31', + } + } + }, + } + }, + } + } + + event = self._create_api_event( + 'aslp', + body={ + 'query': { + 'bool': { + 'must': [ + { + 'bool': { + 'should': [ + encumbrance_condition('licenses'), + encumbrance_condition('privileges'), + ], + 'minimum_should_match': 1, + } + } + ] + } + } + }, + ) + + response = search_api_handler(event, self.mock_context) + + self.assertEqual(200, response['statusCode']) + mock_opensearch_client.search.assert_called_once() diff --git a/backend/compact-connect/lambdas/python/staff-users/handlers/users.py b/backend/compact-connect/lambdas/python/staff-users/handlers/users.py index 3f27ff6649..a6f0d084ac 100644 --- a/backend/compact-connect/lambdas/python/staff-users/handlers/users.py +++ b/backend/compact-connect/lambdas/python/staff-users/handlers/users.py @@ -3,7 +3,12 @@ from aws_lambda_powertools.utilities.typing import LambdaContext from botocore.exceptions import ClientError from cc_common.config import config, logger, metrics -from cc_common.exceptions import CCAccessDeniedException, CCInternalException, CCNotFoundException +from cc_common.exceptions import ( + CCAccessDeniedException, + CCInternalException, + CCInvalidRequestException, + CCNotFoundException, +) from cc_common.utils import ( api_handler, authorize_compact, @@ -130,8 +135,15 @@ def post_user(event: dict, context: LambdaContext): # noqa: ARG001 unused-argum # this will raise an exception if the caller was disabled _verify_caller_is_active(event) - # Verify that the client has permission to create a user with the requested permissions - for compact, compact_permissions in body['permissions'].items(): + # Verify that the client has permission to create a user with the requested permissions, and that every + # compact key in the request body matches the path compact. A new user can only ever be created within the + # compact named in the URL; without this check, a caller with admin rights in multiple compacts could have + # the created user silently assigned to whichever compact happened to be the last key in the body. + for body_compact, compact_permissions in body['permissions'].items(): + if body_compact != compact: + raise CCInvalidRequestException( + f"Requested permissions for compact '{body_compact}' do not match the path compact '{compact}'." + ) # This method will raise an exception if they request an inappropriate permission for the new user collect_and_authorize_changes(path_compact=compact, scopes=scopes, compact_changes=compact_permissions) diff --git a/backend/compact-connect/lambdas/python/staff-users/tests/function/test_handlers/test_post_user.py b/backend/compact-connect/lambdas/python/staff-users/tests/function/test_handlers/test_post_user.py index 8d0019c01d..c2647f1e9b 100644 --- a/backend/compact-connect/lambdas/python/staff-users/tests/function/test_handlers/test_post_user.py +++ b/backend/compact-connect/lambdas/python/staff-users/tests/function/test_handlers/test_post_user.py @@ -137,6 +137,46 @@ def test_post_user_forbidden_compact_read_ssn(self): self.assertEqual(403, resp['statusCode']) + def test_post_user_rejects_permissions_for_compact_other_than_path_compact(self): + """ + A caller with admin rights in multiple compacts must not be able to create a user under a compact + other than the one named in the URL path, regardless of what other compacts appear in the request + body's 'permissions' object. + """ + from handlers.users import post_user + + self._load_compact_active_member_jurisdictions(compact='aslp') + self._load_compact_active_member_jurisdictions(compact='octp') + caller_id = self._when_testing_with_valid_caller() + + with open('tests/resources/api-event.json') as f: + event = json.load(f) + + with open('tests/resources/api/user-post.json') as f: + api_user = json.load(f) + + # api_user already requests aslp permissions; add a second, fully-authorized compact's permissions. + api_user['permissions']['octp'] = { + 'actions': {'readPrivate': True}, + 'jurisdictions': {'oh': {'actions': {'write': True}}}, + } + event['body'] = json.dumps(api_user) + + # The caller is a legitimate admin in both compacts + event['requestContext']['authorizer']['claims']['sub'] = caller_id + event['requestContext']['authorizer']['claims']['scope'] = ( + 'openid email aslp/admin oh/aslp.admin octp/admin oh/octp.admin' + ) + event['pathParameters'] = {'compact': 'aslp'} + + resp = post_user(event, self.mock_context) + + self.assertEqual(400, resp['statusCode']) + body = json.loads(resp['body']) + self.assertEqual( + {'message': "Requested permissions for compact 'octp' do not match the path compact 'aslp'."}, body + ) + def test_post_user_returns_400_if_invalid_jurisdiction_permission_set(self): from handlers.users import post_user diff --git a/backend/compact-connect/stacks/search_persistent_stack/provider_search_domain.py b/backend/compact-connect/stacks/search_persistent_stack/provider_search_domain.py index 944a97f388..10186805f9 100644 --- a/backend/compact-connect/stacks/search_persistent_stack/provider_search_domain.py +++ b/backend/compact-connect/stacks/search_persistent_stack/provider_search_domain.py @@ -168,6 +168,10 @@ def __init__( # https://docs.aws.amazon.com/opensearch-service/latest/developerguide/managedomains-configuration-changes.html # consider working with stakeholders to schedule a maintenance window during low-traffic periods where # advanced search may become inaccessible during the update, to give you time to verify changes. + # We determined through testing that setting the `rest.action.multi.allow_explicit_index: false` domain + # setting causes blue/green deployments to fail consistently. The assumption was that there were Kibana + # related queries that AWS uses under the hood as part of the blue/green cut-over which could not be run + # with that setting set to false, though this was never conclusively verified. version=EngineVersion.OPENSEARCH_3_3, capacity=capacity_config, enable_auto_software_update=True, diff --git a/backend/cosmetology-app/lambdas/nodejs/package.json b/backend/cosmetology-app/lambdas/nodejs/package.json index 7d007fc78b..763bd27246 100644 --- a/backend/cosmetology-app/lambdas/nodejs/package.json +++ b/backend/cosmetology-app/lambdas/nodejs/package.json @@ -5,7 +5,8 @@ "description": "NodeJS lambdas for CompactConnect", "resolutions": { "fast-xml-parser": "5.7.3", - "postcss": "8.5.24" + "postcss": "8.5.24", + "sanitize-html": "2.17.5" }, "scripts": { "build": "tsc", diff --git a/backend/cosmetology-app/lambdas/nodejs/yarn.lock b/backend/cosmetology-app/lambdas/nodejs/yarn.lock index e6daa62f01..3c986c512f 100644 --- a/backend/cosmetology-app/lambdas/nodejs/yarn.lock +++ b/backend/cosmetology-app/lambdas/nodejs/yarn.lock @@ -3311,6 +3311,11 @@ csstype@^3.2.2: resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.2.3.tgz#ec48c0f3e993e50648c86da559e2610995cf989a" integrity sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ== +dayjs@^1.11.7: + version "1.11.21" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.21.tgz#57f87562e62de76f3c704bd2b8d522fc33068eb2" + integrity sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA== + debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.5, debug@^4.4.3: version "4.4.3" resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" @@ -3386,7 +3391,7 @@ domhandler@^5.0.2, domhandler@^5.0.3: dependencies: domelementtype "^2.3.0" -domutils@^3.0.1: +domutils@^3.2.2: version "3.2.2" resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.2.2.tgz#edbfe2b668b0c1d97c24baf0f1062b132221bc78" integrity sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw== @@ -3430,11 +3435,16 @@ enabled@2.0.x: resolved "https://registry.yarnpkg.com/enabled/-/enabled-2.0.0.tgz#f9dd92ec2d6f4bbc0d5d1e64e21d61cd4665e7c2" integrity sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ== -entities@^4.2.0, entities@^4.4.0: +entities@^4.2.0: version "4.5.0" resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== +entities@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/entities/-/entities-7.0.1.tgz#26e8a88889db63417dcb9a1e79a3f1bc92b5976b" + integrity sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA== + error-ex@^1.3.1: version "1.3.4" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.4.tgz#b3a8d8bb6f92eecc1629e3e27d3c8607a8a32414" @@ -3856,15 +3866,15 @@ html-escaper@^2.0.0: resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== -htmlparser2@^8.0.0: - version "8.0.2" - resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-8.0.2.tgz#f002151705b383e62433b5cf466f5b716edaec21" - integrity sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA== +htmlparser2@^10.1.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-10.1.0.tgz#fe3f2e12c73b6e462d4e10395db9c1119e4d6ae4" + integrity sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ== dependencies: domelementtype "^2.3.0" domhandler "^5.0.3" - domutils "^3.0.1" - entities "^4.4.0" + domutils "^3.2.2" + entities "^7.0.1" human-signals@^2.1.0: version "2.1.0" @@ -4447,6 +4457,13 @@ lambda-local@^2.2.0: dotenv "^16.3.1" winston "^3.10.0" +launder@^1.7.1: + version "1.7.1" + resolved "https://registry.yarnpkg.com/launder/-/launder-1.7.1.tgz#ef7155ab0c3ddec2323089c961d2e9a249aa5b0d" + integrity sha512-mU6WRz5EusL9ZZuiZ5SO4Y6C0P9PAUR9iwdb6bzj4KDihm28DiHFw+/yk9DBH4f+Pv1wuzQ4e2jV3oQ7mkIqvw== + dependencies: + dayjs "^1.11.7" + leven@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" @@ -4982,15 +4999,16 @@ safe-stable-stringify@^2.3.1: resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz#4ca2f8e385f2831c432a719b108a3bf7af42a1dd" integrity sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA== -sanitize-html@^2.17.0: - version "2.17.0" - resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.17.0.tgz#a8f66420a6be981d8fe412e3397cc753782598e4" - integrity sha512-dLAADUSS8rBwhaevT12yCezvioCA+bmUTPH/u57xKPT8d++voeYE6HeluA/bPbQ15TwDBG2ii+QZIEmYx8VdxA== +sanitize-html@2.17.5, sanitize-html@^2.17.0: + version "2.17.5" + resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.17.5.tgz#bef1d103773760e1c52a01b1f58178bee9c10556" + integrity sha512-ZmU1joGRrvoyctKIiuwUxqR6moLoU2Wk+2bMccN6f7UwhAmwYDvWziqPxRDDN2Qip62NqnIrVrT9akbL6Wretg== dependencies: deepmerge "^4.2.2" escape-string-regexp "^4.0.0" - htmlparser2 "^8.0.0" + htmlparser2 "^10.1.0" is-plain-object "^5.0.0" + launder "^1.7.1" parse-srcset "^1.0.2" postcss "^8.3.11" diff --git a/backend/cosmetology-app/lambdas/python/common/cc_common/data_model/schema/provider/api.py b/backend/cosmetology-app/lambdas/python/common/cc_common/data_model/schema/provider/api.py index e62ab1e650..870cdaed59 100644 --- a/backend/cosmetology-app/lambdas/python/common/cc_common/data_model/schema/provider/api.py +++ b/backend/cosmetology-app/lambdas/python/common/cc_common/data_model/schema/provider/api.py @@ -1,10 +1,10 @@ # ruff: noqa: N801, N815, ARG002 invalid-name unused-argument -from marshmallow import ValidationError, validates_schema -from marshmallow.fields import Integer, List, Nested, Raw, String -from marshmallow.validate import Length, Range, Regexp +from marshmallow import ValidationError, pre_load +from marshmallow.fields import Dict, Integer, List, Nested, Raw, String +from marshmallow.validate import Length, OneOf, Range, Regexp from cc_common.data_model.schema.adverse_action.api import AdverseActionGeneralResponseSchema -from cc_common.data_model.schema.base_record import ForgivingSchema +from cc_common.data_model.schema.base_record import ForgivingSchema, StrictSchema from cc_common.data_model.schema.common import CCRequestSchema from cc_common.data_model.schema.fields import ( ActiveInactive, @@ -56,6 +56,193 @@ def _validate_no_cross_index_keys(obj, path: str = 'query') -> None: # Scalar values (str, int, bool, None) are safe - we only check keys +# Maximum nesting depth accepted in a caller-supplied query. The deepest query the frontend can +# build is 13 levels (the encumbrance-date condition: bool > should > nested > query > nested > +# query > range > field > bound), so this leaves room for the UI to grow while staying well clear +# of the point where recursing through the schema would exhaust the Python stack and surface as an +# unhandled 500 instead of a 400. +_MAX_QUERY_DEPTH = 20 + + +def _validate_query_depth(query) -> None: + """ + Validate that a caller-supplied query is not nested beyond _MAX_QUERY_DEPTH. + + Walked iteratively rather than recursively: this guard has to be able to measure a query that + is deeper than the interpreter's own recursion limit, which is the very thing it exists to + reject. + + :param query: The caller-supplied query body + :raises ValidationError: If the query nests deeper than _MAX_QUERY_DEPTH + """ + stack = [(query, 1)] + while stack: + node, depth = stack.pop() + if depth > _MAX_QUERY_DEPTH: + raise ValidationError(f'Query is too deeply nested (maximum depth: {_MAX_QUERY_DEPTH}).') + if isinstance(node, dict): + stack.extend((value, depth + 1) for value in node.values()) + elif isinstance(node, list): + stack.extend((item, depth + 1) for item in node) + + +def _validate_query_input(query) -> None: + """ + Run the checks that must happen before a query is deserialized through the schema. + + Ordering matters. The depth guard runs first because ``_validate_no_cross_index_keys`` is itself + recursive and would blow the stack on a pathologically deep query. Both run ahead of field + deserialization (from a ``pre_load`` hook) so that a cross-index attempt is always reported as + such, rather than being pre-empted by, or skipped because of, an unrelated field error. + + :param query: The caller-supplied query body + :raises ValidationError: If the query is too deeply nested or attempts a cross-index lookup + """ + _validate_query_depth(query) + _validate_no_cross_index_keys(query) + + +# Keys permitted inside a nested clause's `inner_hits`. Deliberately excludes `_source`, so a caller +# cannot use inner_hits to select which nested fields come back. +_ALLOWED_INNER_HITS_KEYS = ('from', 'name', 'size') + + +class BoolQuerySchema(StrictSchema): + """ + A `bool` query clause. + + Each occupant holds further query clauses, so they recurse back into QueryClauseSchema (via a + lambda, since that schema is defined below). Inheriting StrictSchema is what rejects any bool + key the frontend does not send. + + Serialization direction: + API -> load() -> Python + """ + + must = List(Nested(lambda: QueryClauseSchema()), required=False, allow_none=False) + must_not = List(Nested(lambda: QueryClauseSchema()), required=False, allow_none=False) + should = List(Nested(lambda: QueryClauseSchema()), required=False, allow_none=False) + filter = List(Nested(lambda: QueryClauseSchema()), required=False, allow_none=False) + # Left untyped, matching the previous hand-rolled validation, which allowlisted the key without + # constraining its value. + minimum_should_match = Raw(required=False, allow_none=False) + + +class NestedQuerySchema(StrictSchema): + """ + A `nested` query clause. + + `query` recurses back into QueryClauseSchema, which is what keeps a free-form clause from being + smuggled in below a nested path. + + Serialization direction: + API -> load() -> Python + """ + + path = String(required=True, allow_none=False) + query = Nested(lambda: QueryClauseSchema(), required=True, allow_none=False) + inner_hits = Dict( + keys=String( + validate=OneOf( + _ALLOWED_INNER_HITS_KEYS, + error="'{input}' is not allowed in inner_hits. Allowed: {choices}.", + ) + ), + values=Raw(), + required=False, + allow_none=False, + ) + # Left untyped, matching the previous hand-rolled validation, which allowlisted these keys + # without constraining their values. + score_mode = Raw(required=False, allow_none=False) + ignore_unmapped = Raw(required=False, allow_none=False) + + +class QueryClauseSchema(StrictSchema): + """ + A single OpenSearch DSL query clause, restricted to the clause types the CompactConnect frontend + actually builds (see prepRequestSearchParams in webroot/src/network/searchApi/data.api.ts). + + Because a request is *loaded* through this schema rather than merely inspected, only structure + described here can reach OpenSearch at all. + + The field-keyed clauses take arbitrary field names, so their contents stay Raw: restricting + which fields may be *queried* is the job of the permission checks in the search handler, not of + this schema. Note that `terms` in particular stays permissive so that a terms lookup carrying an + `index` key is still caught by _validate_no_cross_index_keys with a cross-index error. + + Serialization direction: + API -> load() -> Python + """ + + match_all = Dict(required=False, allow_none=False) + term = Dict(keys=String(), values=Raw(), required=False, allow_none=False) + terms = Dict( + keys=String(), + values=List( + Raw(), + error_messages={'invalid': 'A terms query must be a plain list of values.'}, + ), + required=False, + allow_none=False, + ) + match = Dict(keys=String(), values=Raw(), required=False, allow_none=False) + match_phrase_prefix = Dict(keys=String(), values=Raw(), required=False, allow_none=False) + range = Dict(keys=String(), values=Raw(), required=False, allow_none=False) + bool = Nested(BoolQuerySchema, required=False, allow_none=False) + nested = Nested(NestedQuerySchema, required=False, allow_none=False) + + +# Sort shape the search API accepts, derived from what the CompactConnect frontend actually emits +# (see prepRequestSearchParams in webroot/src/network/searchApi/data.api.ts), which is exactly: +# +# sort: [{'.keyword': {'order': 'asc' | 'desc'}}] +# +# This is modelled declaratively rather than walked, so anything not described here is rejected by +# marshmallow itself. +_ALLOWED_SORT_ORDERS = ('asc', 'desc') + +# Fields a caller is permitted to sort by. The frontend only ever sorts by family name, but +# providerId and the date fields are kept available so `search_after` cursor pagination has a unique +# tiebreaker to sort on. +# +# This list is deliberately restricted to non-sensitive fields. Sort values are echoed back to the +# caller as `lastSort` (see the search handler), which is outside the response schema that strips +# restricted fields -- so whatever is sortable is also readable. Sensitive fields (dateOfBirth, +# emailAddress, phoneNumber, home address, ...) are therefore not sortable by anyone, regardless of +# scopes. Note this is independent of *querying*: querying by dateOfBirth is still supported for +# callers holding readPrivate. +# +# Text fields are only listed via their `.keyword` subfield, since OpenSearch rejects sorting on an +# analyzed text field anyway. +_ALLOWED_SORT_FIELDS = ( + 'dateOfExpiration', + 'dateOfUpdate', + 'familyName.keyword', + 'givenName.keyword', + 'providerId', +) + + +class SortOptionsSchema(StrictSchema): + """ + Sort options for a single sorted field, e.g. {'order': 'asc'}. + + Inheriting StrictSchema (unknown = RAISE) is what rejects every sort option the frontend does + not send -- including `nested`, whose `filter` would otherwise be an unvalidated query-DSL + position. + + Serialization direction: + API -> load() -> Python + """ + + order = String( + required=True, + allow_none=False, + validate=OneOf(_ALLOWED_SORT_ORDERS, error="Invalid sort order '{input}'. Allowed values: {choices}."), + ) + + class ProviderReadPrivateResponseSchema(ForgivingSchema): """ Provider object fields that are sanitized for users with the 'readPrivate' permission. @@ -284,7 +471,9 @@ class SearchProvidersRequestSchema(CCRequestSchema): Schema for advanced search providers requests. This schema is used to validate incoming requests to the advanced search providers API endpoint. - It accepts an OpenSearch DSL query body for flexible querying of the provider index. + It accepts an OpenSearch DSL query body, restricted to the clause types QueryClauseSchema + declares -- the ones the frontend actually builds. Free-form and scripted clauses are not + accepted; see QueryClauseSchema for why. The request body closely mirrors OpenSearch DSL for pagination using `search_after`. See: https://docs.opensearch.org/latest/search-plugins/searching-data/paginate/#the-search_after-parameter @@ -293,8 +482,8 @@ class SearchProvidersRequestSchema(CCRequestSchema): API -> load() -> Python """ - # The OpenSearch query body - we use Raw to allow the full flexibility of OpenSearch queries - query = Raw(required=True, allow_none=False) + # The OpenSearch query body, restricted to the clause types QueryClauseSchema declares + query = Nested(QueryClauseSchema, required=True, allow_none=False) # Pagination parameters following OpenSearch DSL # 'from' is a reserved word in Python, so we use 'from_' with data_key='from' @@ -303,25 +492,48 @@ class SearchProvidersRequestSchema(CCRequestSchema): # Sort order - required when using search_after pagination # Example: [{"providerId": "asc"}, {"dateOfUpdate": "desc"}] - sort = Raw(required=False, allow_none=False) + sort = List( + Dict( + keys=String( + validate=OneOf( + _ALLOWED_SORT_FIELDS, + error="Sorting by '{input}' is not allowed. Sortable fields: {choices}.", + ) + ), + values=Nested(SortOptionsSchema), + ), + required=False, + allow_none=False, + ) # The search_after parameter for cursor-based pagination # This should be the 'sort' values from the last hit of the previous page # Example: ["provider-uuid-123", "2024-01-15T10:30:00Z"] search_after = Raw(required=False, allow_none=False) - @validates_schema - def validate_no_cross_index_queries(self, data, **kwargs): + @pre_load + def validate_search_request_dsl(self, data, **kwargs): """ - Validate that the query does not contain cross-index lookup attempts. - - This is a defense-in-depth security measure to prevent queries that attempt to access - data from other compact indices. The primary protection is the OpenSearch domain setting - `rest.action.multi.allow_explicit_index: false`, but this validation provides an - additional application-layer check. - - Dangerous patterns blocked: - - Terms lookup with external index: {"terms": {"field": {"index": "other_index", ...}}} - - More like this with external docs: {"more_like_this": {"like": [{"_index": "other_index"}]}} + Validate the caller-supplied OpenSearch DSL in the search request body. + + Three application-layer checks are applied: + + 1. Query clause allowlist: only the structured query types the frontend builds are accepted. + Free-form and scripted clauses are rejected, since they can carry a field reference + inside a string value and thereby slip past field-level permission checks. + 2. Cross-index lookups: queries that attempt to reach another compact's index are blocked. + Note that this application-layer check is currently the only protection against + cross-index lookups. The OpenSearch domain setting + `rest.action.multi.allow_explicit_index: false` would provide a second layer, but it is + not configured on the domain due to issues it causes with blue/green deployments + (see stacks/search_persistent_stack/provider_search_domain.py). + + Dangerous patterns blocked: + - Terms lookup with external index: {"terms": {"field": {"index": "other_index", ...}}} + - More like this with external docs: {"more_like_this": {"like": [{"_index": "other"}]}} """ - _validate_no_cross_index_keys(data.get('query', {})) + # Runs ahead of field deserialization, so `data` is still exactly what the caller sent and + # is not guaranteed to be an object. Anything else is left for the field layer to reject. + if isinstance(data, dict): + _validate_query_input(data.get('query', {})) + return data diff --git a/backend/cosmetology-app/lambdas/python/search/tests/function/test_search_providers.py b/backend/cosmetology-app/lambdas/python/search/tests/function/test_search_providers.py index 8277372641..95d8d61716 100644 --- a/backend/cosmetology-app/lambdas/python/search/tests/function/test_search_providers.py +++ b/backend/cosmetology-app/lambdas/python/search/tests/function/test_search_providers.py @@ -213,7 +213,7 @@ def test_search_with_sort_parameter(self, mock_opensearch_client): self._when_testing_mock_opensearch_client(mock_opensearch_client) - sort_config = [{'providerId': 'asc'}, {'dateOfUpdate': 'desc'}] + sort_config = [{'providerId': {'order': 'asc'}}, {'dateOfUpdate': {'order': 'desc'}}] search_after_values = ['provider-uuid-123'] event = self._create_api_event( 'cosm', @@ -338,7 +338,7 @@ def test_search_response_includes_last_sort_for_pagination(self, mock_opensearch 'cosm', body={ 'query': {'match_all': {}}, - 'sort': [{'providerId': 'asc'}, {'dateOfUpdate': 'asc'}], + 'sort': [{'providerId': {'order': 'asc'}}, {'dateOfUpdate': {'order': 'asc'}}], }, ) @@ -406,22 +406,23 @@ def test_query_with_index_key_returns_400(self): self.assertIn("'index'", body['message']) def test_query_with_underscore_index_key_returns_400(self): - """Test that queries containing '_index' key are rejected with 400 error.""" + """ + Test that queries containing an '_index' key are rejected with a 400 error. + + This exercises the cross-index check itself, by placing '_index' inside an allowlisted + clause so it has to be caught on its own merits rather than by the allowlist. + """ from handlers.search import search_api_handler - # Test with '_index' key (more_like_this attack pattern) event = self._create_api_event( 'cosm', body={ 'query': { - 'more_like_this': { - 'fields': ['familyName', 'givenName'], - 'like': [ - { - '_index': 'compact_cosm_providers', - '_id': 'target-provider-uuid', - } - ], + 'terms': { + 'providerId': { + '_index': 'compact_cosm_providers', + '_id': 'target-provider-uuid', + } } } }, @@ -484,7 +485,7 @@ def test_opensearch_request_error_returns_400_with_error_message(self, mock_open 'cosm', body={ 'query': {'match_all': {}}, - 'sort': [{'familyName': 'asc'}], # Sorting on text field causes this error + 'sort': [{'familyName.keyword': {'order': 'asc'}}], }, ) @@ -494,6 +495,195 @@ def test_opensearch_request_error_returns_400_with_error_message(self, mock_open body = json.loads(response['body']) self.assertEqual(error_reason, body['message']) + @patch('handlers.search.opensearch_client') + def test_search_with_query_string_clause_returns_400(self, mock_opensearch_client): + """ + A free-form query_string clause must be rejected outright. + + query_string smuggles the field reference inside a string value, which defeats any + field-name-based permission gate (e.g. the dateOfBirth/readPrivate check) and would + otherwise let a readGeneral-only caller use hit-vs-no-hit as an oracle for a provider's + date of birth. + """ + from handlers.search import search_api_handler + + self._when_testing_mock_opensearch_client(mock_opensearch_client) + + event = self._create_api_event( + 'cosm', + body={ + 'query': { + 'bool': { + 'must': [ + {'term': {'providerId': ''}}, + {'query_string': {'query': 'licenses.dateOfBirth:[1985-01-01 TO 1985-06-30]'}}, + ] + } + } + }, + ) + + response = search_api_handler(event, self.mock_context) + + self.assertEqual(400, response['statusCode']) + body = json.loads(response['body']) + self.assertIn('query_string', body['message']) + mock_opensearch_client.search.assert_not_called() + + @patch('handlers.search.opensearch_client') + def test_search_with_disallowed_free_form_clauses_returns_400(self, mock_opensearch_client): + """Every free-form/scripted clause type outside the allowlist must be rejected with a 400.""" + from handlers.search import search_api_handler + + self._when_testing_mock_opensearch_client(mock_opensearch_client) + + disallowed_queries = { + 'simple_query_string': {'simple_query_string': {'query': 'licenses.dateOfBirth:1985*'}}, + 'wildcard': {'wildcard': {'licenses.dateOfBirth': {'value': '1985*'}}}, + 'regexp': {'regexp': {'licenses.dateOfBirth': '198[0-9].*'}}, + 'script': {'script': {'script': {'source': "doc['licenses.dateOfBirth'].size() > 0"}}}, + 'exists': {'exists': {'field': 'dateOfBirth'}}, + 'multi_match': {'multi_match': {'query': '1985', 'fields': ['givenName', 'dateOfBirth']}}, + 'more_like_this': {'more_like_this': {'like': [{'_index': 'compact_socw_providers'}]}}, + } + + for clause_name, query in disallowed_queries.items(): + with self.subTest(clause=clause_name): + event = self._create_api_event('cosm', body={'query': query}) + + response = search_api_handler(event, self.mock_context) + + self.assertEqual(400, response['statusCode']) + body = json.loads(response['body']) + self.assertIn(clause_name, body['message']) + + mock_opensearch_client.search.assert_not_called() + + @patch('handlers.search.opensearch_client') + def test_search_with_disallowed_clause_nested_deep_in_query_returns_400(self, mock_opensearch_client): + """A disallowed clause buried inside nested/bool structures must still be rejected.""" + from handlers.search import search_api_handler + + self._when_testing_mock_opensearch_client(mock_opensearch_client) + + event = self._create_api_event( + 'cosm', + body={ + 'query': { + 'bool': { + 'must': [ + {'match': {'givenName': 'John'}}, + { + 'nested': { + 'path': 'licenses', + 'query': { + 'bool': { + 'should': [ + {'term': {'licenses.jurisdiction': 'oh'}}, + {'query_string': {'query': 'licenses.dateOfBirth:1985*'}}, + ] + } + }, + } + }, + ] + } + } + }, + ) + + response = search_api_handler(event, self.mock_context) + + self.assertEqual(400, response['statusCode']) + body = json.loads(response['body']) + self.assertIn('query_string', body['message']) + mock_opensearch_client.search.assert_not_called() + + @patch('handlers.search.opensearch_client') + def test_search_accepts_the_full_query_shape_built_by_the_frontend(self, mock_opensearch_client): + """ + Regression guard: the complete clause vocabulary the frontend builds must remain accepted. + + Mirrors prepRequestSearchParams in webroot/src/network/searchApi/data.api.ts, combining every + clause type it can emit into a single request. + """ + from handlers.search import search_api_handler + + self._when_testing_mock_opensearch_client(mock_opensearch_client) + + event = self._create_api_event( + 'cosm', + body={ + 'query': { + 'bool': { + 'must': [ + {'match_phrase_prefix': {'givenName': 'Jo'}}, + {'match_phrase_prefix': {'familyName': 'Do'}}, + {'term': {'licenseJurisdiction': 'oh'}}, + {'match': {'licenseNumber': 'A-123'}}, + { + 'nested': { + 'path': 'privileges', + 'inner_hits': {'size': 100}, + 'query': { + 'bool': { + 'must': [{'term': {'privileges.jurisdiction': 'ky'}}], + 'should': [ + {'range': {'privileges.dateOfIssuance': {'gte': '2024-01-01'}}}, + {'range': {'privileges.dateOfRenewal': {'lte': '2024-12-31'}}}, + ], + 'minimum_should_match': 1, + } + }, + } + }, + { + 'bool': { + 'must_not': [ + { + 'nested': { + 'path': 'licenses', + 'query': { + 'nested': { + 'path': 'licenses.investigations', + 'query': { + 'term': {'licenses.investigations.type': 'investigation'} + }, + } + }, + } + } + ] + } + }, + ] + } + }, + 'size': 25, + 'from': 25, + 'sort': [{'familyName.keyword': {'order': 'asc'}}], + }, + ) + + response = search_api_handler(event, self.mock_context) + + self.assertEqual(200, response['statusCode']) + mock_opensearch_client.search.assert_called_once() + + @patch('handlers.search.opensearch_client') + def test_search_with_match_all_query_is_accepted(self, mock_opensearch_client): + """The frontend's no-search-terms default (match_all) must remain accepted.""" + from handlers.search import search_api_handler + + self._when_testing_mock_opensearch_client(mock_opensearch_client) + + event = self._create_api_event('cosm', body={'query': {'match_all': {}}}) + + response = search_api_handler(event, self.mock_context) + + self.assertEqual(200, response['statusCode']) + mock_opensearch_client.search.assert_called_once() + @patch('handlers.search.opensearch_client') def test_search_with_date_of_birth_query_allowed_for_compact_level_read_private_scope(self, mock_opensearch_client): """Test that a query containing dateOfBirth succeeds when the caller has compact-level readPrivate scope.""" @@ -596,12 +786,17 @@ def test_search_with_nested_date_of_birth_query_rejected_without_read_private_sc @patch('handlers.search.opensearch_client') def test_search_with_exists_field_date_of_birth_rejected_without_read_private_scope(self, mock_opensearch_client): - """Test that query with dateOfBirth as field value (e.g. exists) is rejected without readPrivate scope.""" + """ + A readGeneral-only caller must not be able to probe dateOfBirth via an "exists" clause. + + "exists" names its field by value ({"exists": {"field": "dateOfBirth"}}) rather than by key. + That vector is now closed structurally: "exists" is not an allowlisted query type, so the + request is rejected before the field-name-based readPrivate check is ever consulted. + """ from handlers.search import search_api_handler self._when_testing_mock_opensearch_client(mock_opensearch_client) - # OpenSearch "exists" query references field by value: {"exists": {"field": "dateOfBirth"}} event = self._create_api_event( 'cosm', body={ @@ -613,13 +808,17 @@ def test_search_with_exists_field_date_of_birth_rejected_without_read_private_sc self.assertEqual(400, response['statusCode']) body = json.loads(response['body']) - self.assertIn('dateOfBirth', body['message']) - self.assertIn('readPrivate', body['message']) + self.assertIn('exists', body['message']) mock_opensearch_client.search.assert_not_called() @patch('handlers.search.opensearch_client') def test_search_with_date_of_birth_string_in_list_rejected_without_read_private_scope(self, mock_opensearch_client): - """dateOfBirth as a list element (e.g. multi_match fields) must not bypass readPrivate checks.""" + """ + A readGeneral-only caller must not be able to probe dateOfBirth via multi_match fields. + + dateOfBirth appears only as a list element here, never as a key. Like "exists", multi_match + is not an allowlisted query type, so the request is rejected outright. + """ from handlers.search import search_api_handler self._when_testing_mock_opensearch_client(mock_opensearch_client) @@ -640,13 +839,19 @@ def test_search_with_date_of_birth_string_in_list_rejected_without_read_private_ self.assertEqual(400, response['statusCode']) body = json.loads(response['body']) - self.assertIn('dateOfBirth', body['message']) - self.assertIn('readPrivate', body['message']) + self.assertIn('multi_match', body['message']) mock_opensearch_client.search.assert_not_called() @patch('handlers.search.opensearch_client') def test_search_with_sort_by_date_of_birth_rejected_without_read_private_scope(self, mock_opensearch_client): - """Test that sort clause referencing dateOfBirth is rejected when caller lacks readPrivate scope.""" + """ + A sort clause referencing dateOfBirth is rejected for a readGeneral-only caller. + + dateOfBirth is not in the sortable-field allowlist, so this is now refused on the field + rather than on the caller's scopes. See + test_search_with_sort_by_date_of_birth_rejected_even_with_read_private_scope for the + readPrivate case, which is refused for the same reason. + """ from handlers.search import search_api_handler self._when_testing_mock_opensearch_client(mock_opensearch_client) @@ -656,7 +861,7 @@ def test_search_with_sort_by_date_of_birth_rejected_without_read_private_scope(s 'cosm', body={ 'query': {'match_all': {}}, - 'sort': [{'providerId': 'asc'}, {'licenses.dateOfBirth': 'desc'}], + 'sort': [{'providerId': {'order': 'asc'}}, {'licenses.dateOfBirth': {'order': 'desc'}}], }, ) @@ -665,12 +870,158 @@ def test_search_with_sort_by_date_of_birth_rejected_without_read_private_scope(s self.assertEqual(400, response['statusCode']) body = json.loads(response['body']) self.assertIn('dateOfBirth', body['message']) - self.assertIn('readPrivate', body['message']) mock_opensearch_client.search.assert_not_called() @patch('handlers.search.opensearch_client') - def test_search_with_sort_by_date_of_birth_allowed_with_read_private_scope(self, mock_opensearch_client): - """Test that sort by dateOfBirth succeeds when caller has readPrivate scope.""" + def test_search_with_script_sort_returns_400(self, mock_opensearch_client): + """ + A scripted sort must be rejected outright. + + A _script sort computes an arbitrary value per document, and the handler echoes sort values + straight back to the caller as `lastSort` -- outside the response schema that otherwise + strips restricted fields. That makes a scripted sort a direct read primitive for any indexed + field, not merely an oracle, so it must never reach OpenSearch. + """ + from handlers.search import search_api_handler + + self._when_testing_mock_opensearch_client(mock_opensearch_client) + + event = self._create_api_event( + 'cosm', + body={ + 'query': {'match_all': {}}, + 'sort': [ + { + '_script': { + 'type': 'string', + 'script': {'source': "doc['licenses.dateOfBirth'].value"}, + 'order': 'asc', + } + } + ], + }, + ) + + response = search_api_handler(event, self.mock_context) + + self.assertEqual(400, response['statusCode']) + body = json.loads(response['body']) + self.assertIn('_script', body['message']) + mock_opensearch_client.search.assert_not_called() + + @patch('handlers.search.opensearch_client') + def test_search_with_nested_filter_in_sort_returns_400(self, mock_opensearch_client): + """ + A sort entry's `nested.filter` is a full query-DSL position, so it must not be accepted. + + Without this, a free-form clause could be smuggled through sort even though the query itself + is allowlisted, re-opening the dateOfBirth oracle through a different door. + """ + from handlers.search import search_api_handler + + self._when_testing_mock_opensearch_client(mock_opensearch_client) + + event = self._create_api_event( + 'cosm', + body={ + 'query': {'match_all': {}}, + 'sort': [ + { + 'dateOfExpiration': { + 'order': 'asc', + 'nested': { + 'path': 'licenses', + 'filter': { + 'query_string': {'query': 'licenses.dateOfBirth:[1985-01-01 TO 1985-06-30]'} + }, + }, + } + } + ], + }, + ) + + response = search_api_handler(event, self.mock_context) + + self.assertEqual(400, response['statusCode']) + body = json.loads(response['body']) + self.assertIn('nested', body['message']) + mock_opensearch_client.search.assert_not_called() + + @patch('handlers.search.opensearch_client') + def test_search_with_disallowed_sort_options_returns_400(self, mock_opensearch_client): + """Only the `order` sort option the frontend emits is accepted; every other option is rejected.""" + from handlers.search import search_api_handler + + self._when_testing_mock_opensearch_client(mock_opensearch_client) + + disallowed_sorts = { + 'nested': [{'dateOfExpiration': {'order': 'asc', 'nested': {'path': 'licenses'}}}], + 'missing': [{'familyName.keyword': {'order': 'asc', 'missing': '_last'}}], + 'unmapped_type': [{'familyName.keyword': {'order': 'asc', 'unmapped_type': 'keyword'}}], + 'mode': [{'dateOfExpiration': {'order': 'asc', 'mode': 'max'}}], + 'format': [{'dateOfExpiration': {'order': 'asc', 'format': 'strict_date_optional_time'}}], + } + + for option_name, sort in disallowed_sorts.items(): + with self.subTest(option=option_name): + event = self._create_api_event('cosm', body={'query': {'match_all': {}}, 'sort': sort}) + + response = search_api_handler(event, self.mock_context) + + self.assertEqual(400, response['statusCode']) + body = json.loads(response['body']) + self.assertIn(option_name, body['message']) + + mock_opensearch_client.search.assert_not_called() + + @patch('handlers.search.opensearch_client') + def test_search_with_invalid_sort_order_value_returns_400(self, mock_opensearch_client): + """The sort order must be one of the two values the frontend's SortDirection enum can emit.""" + from handlers.search import search_api_handler + + self._when_testing_mock_opensearch_client(mock_opensearch_client) + + event = self._create_api_event( + 'cosm', + body={'query': {'match_all': {}}, 'sort': [{'familyName.keyword': {'order': 'sideways'}}]}, + ) + + response = search_api_handler(event, self.mock_context) + + self.assertEqual(400, response['statusCode']) + body = json.loads(response['body']) + self.assertIn('sideways', body['message']) + mock_opensearch_client.search.assert_not_called() + + @patch('handlers.search.opensearch_client') + def test_search_accepts_the_sort_shape_built_by_the_frontend(self, mock_opensearch_client): + """ + Regression guard: the sort shape the frontend builds must remain accepted. + + Mirrors prepRequestSearchParams in webroot/src/network/searchApi/data.api.ts, which emits + [{'.keyword': {'order': 'asc' | 'desc'}}]. + """ + from handlers.search import search_api_handler + + for order in ('asc', 'desc'): + with self.subTest(order=order): + mock_opensearch_client.reset_mock() + self._when_testing_mock_opensearch_client(mock_opensearch_client) + + event = self._create_api_event( + 'cosm', + body={'query': {'match_all': {}}, 'sort': [{'familyName.keyword': {'order': order}}]}, + ) + + response = search_api_handler(event, self.mock_context) + + self.assertEqual(200, response['statusCode']) + mock_opensearch_client.search.assert_called_once() + + @patch('handlers.search.opensearch_client') + def test_search_accepts_multi_field_sort_for_search_after_pagination(self, mock_opensearch_client): + """Multiple sort entries must remain accepted, since search_after pagination needs a tiebreaker.""" from handlers.search import search_api_handler self._when_testing_mock_opensearch_client(mock_opensearch_client) @@ -679,12 +1030,236 @@ def test_search_with_sort_by_date_of_birth_allowed_with_read_private_scope(self, 'cosm', body={ 'query': {'match_all': {}}, - 'sort': [{'licenses.dateOfBirth': 'desc'}, {'providerId': 'asc'}], + 'sort': [{'familyName.keyword': {'order': 'asc'}}, {'providerId': {'order': 'asc'}}], + 'search_after': ['Doe', '00000000-0000-0000-0000-000000000001'], }, + ) + + response = search_api_handler(event, self.mock_context) + + self.assertEqual(200, response['statusCode']) + mock_opensearch_client.search.assert_called_once() + + @patch('handlers.search.opensearch_client') + def test_search_with_non_allowlisted_sort_field_returns_400(self, mock_opensearch_client): + """ + Sorting is restricted to an allowlist of non-sensitive fields. + + Sort values are echoed back to the caller as `lastSort`, outside the response schema that + strips restricted fields. Restricting which fields can be sorted on is what keeps that echo + from becoming a read primitive for sensitive data. + """ + from handlers.search import search_api_handler + + self._when_testing_mock_opensearch_client(mock_opensearch_client) + + sensitive_sort_fields = [ + 'licenses.dateOfBirth', + 'licenses.emailAddress', + 'licenses.phoneNumber', + 'licenses.homeAddressStreet1', + 'licenses.homeAddressPostalCode', + 'ssnLastFour', + ] + + for field_name in sensitive_sort_fields: + with self.subTest(field=field_name): + event = self._create_api_event( + 'cosm', + body={'query': {'match_all': {}}, 'sort': [{field_name: {'order': 'asc'}}]}, + ) + + response = search_api_handler(event, self.mock_context) + + self.assertEqual(400, response['statusCode']) + body = json.loads(response['body']) + self.assertIn(field_name, body['message']) + + mock_opensearch_client.search.assert_not_called() + + @patch('handlers.search.opensearch_client') + def test_search_with_sort_by_date_of_birth_rejected_even_with_read_private_scope(self, mock_opensearch_client): + """ + dateOfBirth is not a sortable field, even for a caller holding readPrivate. + + Sorting is a separate capability from reading: the sort field allowlist applies to everyone, + so the `lastSort` echo cannot surface a date of birth regardless of the caller's scopes. + Querying by dateOfBirth with readPrivate remains supported. + """ + from handlers.search import search_api_handler + + self._when_testing_mock_opensearch_client(mock_opensearch_client) + + event = self._create_api_event( + 'cosm', + body={'query': {'match_all': {}}, 'sort': [{'licenses.dateOfBirth': {'order': 'desc'}}]}, scopes_override='openid email cosm/readGeneral cosm/readPrivate', ) response = search_api_handler(event, self.mock_context) + self.assertEqual(400, response['statusCode']) + body = json.loads(response['body']) + self.assertIn('dateOfBirth', body['message']) + mock_opensearch_client.search.assert_not_called() + + @patch('handlers.search.opensearch_client') + def test_search_accepts_every_allowlisted_sort_field(self, mock_opensearch_client): + """Regression guard: each allowlisted sort field must remain usable.""" + from handlers.search import search_api_handler + + allowed_sort_fields = [ + 'familyName.keyword', + 'givenName.keyword', + 'providerId', + 'dateOfUpdate', + 'dateOfExpiration', + ] + + for field_name in allowed_sort_fields: + with self.subTest(field=field_name): + mock_opensearch_client.reset_mock() + self._when_testing_mock_opensearch_client(mock_opensearch_client) + + event = self._create_api_event( + 'cosm', + body={'query': {'match_all': {}}, 'sort': [{field_name: {'order': 'asc'}}]}, + ) + + response = search_api_handler(event, self.mock_context) + + self.assertEqual(200, response['statusCode']) + mock_opensearch_client.search.assert_called_once() + + @patch('handlers.search.opensearch_client') + def test_search_with_excessively_nested_query_returns_400(self, mock_opensearch_client): + """ + A pathologically nested query is refused with a 400 rather than blowing the Python stack. + + The declarative schema recurses through nested/bool clauses, so an arbitrarily deep query + would otherwise raise RecursionError and surface as an unhandled 500. + """ + from handlers.search import search_api_handler + + self._when_testing_mock_opensearch_client(mock_opensearch_client) + + query = {'match_all': {}} + for _ in range(300): + query = {'bool': {'must': [query]}} + + event = self._create_api_event('cosm', body={'query': query}) + + response = search_api_handler(event, self.mock_context) + + self.assertEqual(400, response['statusCode']) + body = json.loads(response['body']) + self.assertIn('too deeply nested', body['message']) + mock_opensearch_client.search.assert_not_called() + + @patch('handlers.search.opensearch_client') + def test_search_with_terms_lookup_object_returns_400(self, mock_opensearch_client): + """ + A terms clause must be a plain list of values; the terms-lookup object form is refused. + + This is enforced structurally by the schema, independently of the cross-index key check: + the lookup below names no index at all, so it reads from the current index and carries no + 'index'/'_index' key for that check to find. + """ + from handlers.search import search_api_handler + + self._when_testing_mock_opensearch_client(mock_opensearch_client) + + event = self._create_api_event( + 'cosm', + body={'query': {'terms': {'providerId': {'id': 'some-other-provider', 'path': 'providerId'}}}}, + ) + + response = search_api_handler(event, self.mock_context) + + self.assertEqual(400, response['statusCode']) + body = json.loads(response['body']) + self.assertIn('terms', body['message']) + mock_opensearch_client.search.assert_not_called() + + @patch('handlers.search.opensearch_client') + def test_search_with_non_object_request_body_returns_400(self, mock_opensearch_client): + """ + A request body that is not a JSON object is refused with a 400, not an unhandled error. + + The checks that run ahead of field deserialization see the caller's raw input, which is not + guaranteed to be an object, so they must not assume they can index into it. + """ + from handlers.search import search_api_handler + + self._when_testing_mock_opensearch_client(mock_opensearch_client) + + for raw_body in ('[]', '"a string"', 'null', '42'): + with self.subTest(body=raw_body): + event = self._create_api_event('cosm', body={'query': {'match_all': {}}}) + event['body'] = raw_body + + response = search_api_handler(event, self.mock_context) + + self.assertEqual(400, response['statusCode']) + + mock_opensearch_client.search.assert_not_called() + + @patch('handlers.search.opensearch_client') + def test_search_accepts_the_deepest_query_the_frontend_can_build(self, mock_opensearch_client): + """ + The deepest query the frontend can emit must stay within the nesting cap. + + This is the encumbrance-date condition from prepRequestSearchParams, which pairs two nested + levels with a range bound and is the frontend's worst case. It pins _MAX_QUERY_DEPTH against + a real query, so the cap cannot be tuned down far enough to break the UI unnoticed. + """ + from handlers.search import search_api_handler + + self._when_testing_mock_opensearch_client(mock_opensearch_client) + + def encumbrance_condition(top_path: str) -> dict: + nested_path = f'{top_path}.adverseActions' + return { + 'nested': { + 'path': top_path, + 'query': { + 'nested': { + 'path': nested_path, + 'query': { + 'range': { + f'{nested_path}.effectiveStartDate': { + 'gte': '2024-01-01', + 'lte': '2024-12-31', + } + } + }, + } + }, + } + } + + event = self._create_api_event( + 'cosm', + body={ + 'query': { + 'bool': { + 'must': [ + { + 'bool': { + 'should': [ + encumbrance_condition('licenses'), + encumbrance_condition('privileges'), + ], + 'minimum_should_match': 1, + } + } + ] + } + } + }, + ) + + response = search_api_handler(event, self.mock_context) + self.assertEqual(200, response['statusCode']) mock_opensearch_client.search.assert_called_once() diff --git a/backend/cosmetology-app/lambdas/python/staff-users/handlers/users.py b/backend/cosmetology-app/lambdas/python/staff-users/handlers/users.py index 96e5ba7822..c7d9236991 100644 --- a/backend/cosmetology-app/lambdas/python/staff-users/handlers/users.py +++ b/backend/cosmetology-app/lambdas/python/staff-users/handlers/users.py @@ -3,7 +3,12 @@ from aws_lambda_powertools.utilities.typing import LambdaContext from botocore.exceptions import ClientError from cc_common.config import config, logger, metrics -from cc_common.exceptions import CCAccessDeniedException, CCInternalException, CCNotFoundException +from cc_common.exceptions import ( + CCAccessDeniedException, + CCInternalException, + CCInvalidRequestException, + CCNotFoundException, +) from cc_common.utils import ( api_handler, authorize_compact, @@ -130,8 +135,15 @@ def post_user(event: dict, context: LambdaContext): # noqa: ARG001 unused-argum # this will raise an exception if the caller was disabled _verify_caller_is_active(event) - # Verify that the client has permission to create a user with the requested permissions - for compact, compact_permissions in body['permissions'].items(): + # Verify that the client has permission to create a user with the requested permissions, and that every + # compact key in the request body matches the path compact. A new user can only ever be created within the + # compact named in the URL; without this check, a caller with admin rights in multiple compacts could have + # the created user silently assigned to whichever compact happened to be the last key in the body. + for body_compact, compact_permissions in body['permissions'].items(): + if body_compact != compact: + raise CCInvalidRequestException( + f"Requested permissions for compact '{body_compact}' do not match the path compact '{compact}'." + ) # This method will raise an exception if they request an inappropriate permission for the new user collect_and_authorize_changes(path_compact=compact, scopes=scopes, compact_changes=compact_permissions) diff --git a/backend/cosmetology-app/lambdas/python/staff-users/tests/function/test_handlers/test_post_user.py b/backend/cosmetology-app/lambdas/python/staff-users/tests/function/test_handlers/test_post_user.py index 13711ff414..e94a09d5fd 100644 --- a/backend/cosmetology-app/lambdas/python/staff-users/tests/function/test_handlers/test_post_user.py +++ b/backend/cosmetology-app/lambdas/python/staff-users/tests/function/test_handlers/test_post_user.py @@ -113,6 +113,38 @@ def test_post_user_unauthorized(self): self.assertEqual(403, resp['statusCode']) + def test_post_user_rejects_permissions_for_compact_other_than_path_compact(self): + """ + A caller must not be able to create a user under a compact other than the one named in the URL + path, regardless of what other compact keys appear in the request body's 'permissions' object. + """ + from handlers.users import post_user + + self._when_testing_with_valid_jurisdiction() + caller_id = self._when_testing_with_valid_caller() + + with open('tests/resources/api-event.json') as f: + event = json.load(f) + + with open('tests/resources/api/user-post.json') as f: + api_user = json.load(f) + + # api_user already requests cosm permissions; add a second, mismatched compact key. + api_user['permissions']['aslp'] = {'actions': {}, 'jurisdictions': {}} + event['body'] = json.dumps(api_user) + + event['requestContext']['authorizer']['claims']['sub'] = caller_id + event['requestContext']['authorizer']['claims']['scope'] = 'openid email cosm/admin oh/cosm.admin' + event['pathParameters'] = {'compact': 'cosm'} + + resp = post_user(event, self.mock_context) + + self.assertEqual(400, resp['statusCode']) + body = json.loads(resp['body']) + self.assertEqual( + {'message': "Requested permissions for compact 'aslp' do not match the path compact 'cosm'."}, body + ) + def test_post_user_returns_400_if_invalid_jurisdiction_permission_set(self): from handlers.users import post_user diff --git a/backend/cosmetology-app/stacks/search_persistent_stack/provider_search_domain.py b/backend/cosmetology-app/stacks/search_persistent_stack/provider_search_domain.py index 7de6832b1a..5ef1b2bf29 100644 --- a/backend/cosmetology-app/stacks/search_persistent_stack/provider_search_domain.py +++ b/backend/cosmetology-app/stacks/search_persistent_stack/provider_search_domain.py @@ -168,6 +168,10 @@ def __init__( # https://docs.aws.amazon.com/opensearch-service/latest/developerguide/managedomains-configuration-changes.html # consider working with stakeholders to schedule a maintenance window during low-traffic periods where # advanced search may become inaccessible during the update, to give you time to verify changes. + # We determined through testing that setting the `rest.action.multi.allow_explicit_index: false` domain + # setting causes blue/green deployments to fail consistently. The assumption was that there were Kibana + # related queries that AWS uses under the hood as part of the blue/green cut-over which could not be run + # with that setting set to false, though this was never conclusively verified. version=EngineVersion.OPENSEARCH_3_3, capacity=capacity_config, enable_auto_software_update=True, diff --git a/backend/social-work-app/docs/api-specification/latest-oas30.json b/backend/social-work-app/docs/api-specification/latest-oas30.json index 72e608ac6a..ada2c6d936 100644 --- a/backend/social-work-app/docs/api-specification/latest-oas30.json +++ b/backend/social-work-app/docs/api-specification/latest-oas30.json @@ -2,7 +2,7 @@ "openapi": "3.0.1", "info": { "title": "StateApi", - "version": "2026-06-01T19:36:46Z" + "version": "2026-06-30T23:31:06Z" }, "servers": [ { @@ -78,15 +78,59 @@ "TestBackendSocialWorkTestStateAPIStackStateApiStateAuthAuthorizerD3C7A18B": [ "socw/write", "al/socw.write", + "ak/socw.write", "az/socw.write", + "ar/socw.write", + "ca/socw.write", "co/socw.write", + "ct/socw.write", + "de/socw.write", + "dc/socw.write", + "fl/socw.write", + "ga/socw.write", + "gu/socw.write", + "hi/socw.write", + "id/socw.write", + "il/socw.write", + "in/socw.write", + "ia/socw.write", "ks/socw.write", "ky/socw.write", + "la/socw.write", + "me/socw.write", "md/socw.write", + "ma/socw.write", + "mi/socw.write", + "mn/socw.write", + "ms/socw.write", + "mo/socw.write", + "mt/socw.write", + "mp/socw.write", + "ne/socw.write", + "nv/socw.write", + "nh/socw.write", + "nj/socw.write", + "nm/socw.write", + "ny/socw.write", + "nc/socw.write", + "nd/socw.write", "oh/socw.write", + "ok/socw.write", + "or/socw.write", + "pa/socw.write", + "ri/socw.write", + "sc/socw.write", + "sd/socw.write", "tn/socw.write", + "tx/socw.write", + "ut/socw.write", + "vt/socw.write", "va/socw.write", - "wa/socw.write" + "vi/socw.write", + "wa/socw.write", + "wv/socw.write", + "wi/socw.write", + "wy/socw.write" ] } ] @@ -137,15 +181,59 @@ "TestBackendSocialWorkTestStateAPIStackStateApiStateAuthAuthorizerD3C7A18B": [ "socw/write", "al/socw.write", + "ak/socw.write", "az/socw.write", + "ar/socw.write", + "ca/socw.write", "co/socw.write", + "ct/socw.write", + "de/socw.write", + "dc/socw.write", + "fl/socw.write", + "ga/socw.write", + "gu/socw.write", + "hi/socw.write", + "id/socw.write", + "il/socw.write", + "in/socw.write", + "ia/socw.write", "ks/socw.write", "ky/socw.write", + "la/socw.write", + "me/socw.write", "md/socw.write", + "ma/socw.write", + "mi/socw.write", + "mn/socw.write", + "ms/socw.write", + "mo/socw.write", + "mt/socw.write", + "mp/socw.write", + "ne/socw.write", + "nv/socw.write", + "nh/socw.write", + "nj/socw.write", + "nm/socw.write", + "ny/socw.write", + "nc/socw.write", + "nd/socw.write", "oh/socw.write", + "ok/socw.write", + "or/socw.write", + "pa/socw.write", + "ri/socw.write", + "sc/socw.write", + "sd/socw.write", "tn/socw.write", + "tx/socw.write", + "ut/socw.write", + "vt/socw.write", "va/socw.write", - "wa/socw.write" + "vi/socw.write", + "wa/socw.write", + "wv/socw.write", + "wi/socw.write", + "wy/socw.write" ] } ] diff --git a/backend/social-work-app/docs/design/README.md b/backend/social-work-app/docs/design/README.md index 54025f495f..239c3add2c 100644 --- a/backend/social-work-app/docs/design/README.md +++ b/backend/social-work-app/docs/design/README.md @@ -282,7 +282,7 @@ License-related sort keys use a shared suffix of the form expiration date, jurisdiction-reported status, and encumbrance state) - Optional encumbrance status (`encumberedStatus`) - License expiration date (`dateOfExpiration`) - - Optional public Compact Unique Identifier (`publicCompactIdentifier`) — **planned, not yet implemented**; see + - Optional public Compact Unique Identifier (`publicCompactIdentifier`); see [Commission Unique Identifiers: RID and CUID](#commission-unique-identifiers-rid-and-cuid) below Contact information and home address live on individual **license** records, not on the provider record. @@ -341,34 +341,41 @@ uses a [version 4 UUID](https://en.wikipedia.org/wiki/Universally_unique_identif portal. - **Visible to staff users**: although hidden from the public portal, the RID is visible to staff users through the staff user search. -#### Compact Unique Identifier (CUID) — Planned +#### Compact Unique Identifier (CUID) -The CUID is the Compact's persistent, public-facing, human-shorthand identifier for individuals who hold -multistate authorization (i.e. at least one generated privilege — see [Multi-State License Model / Privilege -Generation](#multi-state-license-model--privilege-generation)). Unlike the RID, the CUID is designed to be -short and easy for a member of the public to read, share, and search by. Per Commission rule, the CUID will be: +The CUID is the Compact's persistent, public-facing, human-shorthand identifier for practitioners who hold a +paired multi-state and single-state license. Unlike the RID, the CUID is designed to be short and easy for a +member of the public to read, share, and search by. Per Commission rule, the CUID is: - **Issued only once a practitioner has a paired multi-state and single-state license** — i.e. generated the first time a practitioner has *both* a single-state and a multi-state license uploaded for the **same license type and the same jurisdiction** (the same pairing concept used for privilege generation, see [License Composite Identifier](#license-composite-identifier)), rather than at first multi-state license upload alone. A multi-state license uploaded without its paired single-state license (or vice versa) does **not** trigger CUID generation (contrast with the RID, which is assigned on first license ingestion regardless of scope or pairing). + The pairing itself is the only condition: it is evaluated independently of each license's displayed eligibility + and active status, so holding a CUID does **not** imply the practitioner has any generated privileges. Privilege + generation imposes further eligibility conditions on top of the pairing (see [Privilege Runtime Generation for + Multi-State Licenses](#privilege-runtime-generation-for-multi-state-licenses)), so a practitioner whose paired + licenses are ineligible or expired still holds their CUID while having no privileges. - **Public-facing**, unlike the RID, required by Commission rule to be shown in public lookup, and searchable by the public. -- **Person-based, not license-based**, similar to the RID, persistent for mobility: the CUID will remain the same even if the practitioner's home state later changes under Compact rules (see [Home State Changes](#home-state-changes)). +- **Person-based, not license-based**, similar to the RID, persistent for mobility: the CUID remains the same even if the practitioner's home state later changes under Compact rules (see [Home State Changes](#home-state-changes)). - **Permanently linked to the practitioner's RID** (`providerId`), so historical and future records remain consistent regardless of any home-state changes. -**Format:** The CUID will use the format `SWC--`, where `` is a four-digit random segment +**Format:** The CUID uses the format `SWC--`, where `` is a four-digit random segment (`0000`–`9999`) and `` is a monotonically-increasing integer (e.g. `SWC-4548-1`, `SWC-5126-2`, `SWC-8219-3`, ...). The `` segment is what guarantees uniqueness; the `` segment is an additional -guard described under [Random segment](#random-segment) below. The counter will be tracked in a dedicated DynamoDB -item keyed by `pk`/`sk` of `{compact}#CUID_COUNT`, and claimed atomically via a new `claim_cuid_number` method on +guard described under [Random segment](#random-segment) below. The counter is tracked in a dedicated DynamoDB +item keyed by `pk`/`sk` of `{compact}#CUID_COUNT`, and claimed atomically via the `claim_cuid_number` method on the data client, following the same atomic-increment pattern used by `claim_privilege_number` for JCC privilege ID generation (see [`claim_privilege_number`](../../../compact-connect/lambdas/python/common/cc_common/data_model/data_client.py)). Each call to `claim_cuid_number` issues an `ADD`-based `UpdateItem` against the counter item, which DynamoDB guarantees to be atomic even under concurrent invocations, ensuring no two practitioners are ever issued the same counter value. Because the counter is only ever incremented and never read-then-written non-atomically, uniqueness -is guaranteed regardless of concurrent ingest activity across jurisdictions. +is guaranteed regardless of concurrent ingest activity across jurisdictions. The `_generate_cuid` helper in +[ingest.py](../../lambdas/python/provider-data-v1/handlers/ingest.py) is the only place in the codebase that +constructs a CUID: it claims the counter and generates the random segment internally, so a caller can never pass +in (and potentially reuse or mismatch) a counter value. ##### Random segment @@ -378,8 +385,9 @@ claimed counter value. Its purpose is to make it harder for a member of the publ mistyped. Because the counter alone is a short, dense, monotonically-increasing sequence, most nearby integers correspond to real CUIDs, so a simple typo can silently resolve to the wrong practitioner. Interposing four random digits means a transposition or single-character error must *also* match the random segment to collide with a real -record, which is far less likely. This protection depends on CUID lookups always matching the **full** CUID string. -partial or counter-only matches will not be supported. +record, which is far less likely. This protection depends on CUID lookups always matching the **full** CUID string; +partial, wildcard, or counter-only matches are rejected by request validation before ever reaching OpenSearch +(see [Public Provider Search](#public-provider-search) below). The random segment is generated using Python's standard-library [`secrets`](https://docs.python.org/3/library/secrets.html) module — specifically `secrets.randbelow(10000)`, @@ -396,15 +404,20 @@ reclaim or reuse it. Consequently, CUIDs are guaranteed to be **unique** but not **gap-free**. Consumers of the CUID (including the public) should not assume that a CUID whose counter segment is `` implies exactly `n` practitioners have been issued a CUID, only that each issued CUID is distinct. -Once implemented, the CUID will be stored as the optional `publicCompactIdentifier` field on the top-level -[Provider Record](#record-types-in-detail). Generation is planned as part of the license ingest flow in +The CUID is stored as the optional `publicCompactIdentifier` field on the top-level +[Provider Record](#record-types-in-detail). Generation happens as part of the license ingest flow in [ingest.py](../../lambdas/python/provider-data-v1/handlers/ingest.py): whenever **either** a single-state or a -multi-state license is uploaded for a practitioner, the ingest handler will check whether the practitioner now +multi-state license is uploaded for a practitioner, the ingest handler checks whether the practitioner now has at least one paired single-state/multi-state license (matching jurisdiction and license type) across their -full set of stored licenses. If a pairing exists and `publicCompactIdentifier` is not yet set, a new CUID will be generated at that point -(via `claim_cuid_number`) and written to the provider record. Because the check is against the existing value on the -provider record, the CUID is generated exactly once and is never regenerated on subsequent license uploads, including after a [home state change](#home-state-changes). Because it will live on the top-level provider record rather than on an individual -license, the CUID will also be added as a top-level field in the [OpenSearch index mapping](#index-mapping), making +full set of stored licenses (`ProviderRecordUtility.has_paired_single_and_multi_state_license`). If a pairing exists and `publicCompactIdentifier` is not yet set, a new CUID is generated at that point +(via `_generate_cuid`, which calls `claim_cuid_number`) and written to the provider record. When the same license +upload also triggers the top-level provider record `Put` (see [Ingest Flow](#ingest-flow)), the CUID is included +directly in that write; otherwise — for example, when the *losing* license of a newly-completed pairing does not +win the provider-record update decision — the CUID is assigned via a separate conditional `Update` guarded by +`attribute_not_exists(publicCompactIdentifier)`, which also bumps `dateOfUpdate`/`providerDateOfUpdate` so the +DynamoDB stream fires and OpenSearch is reindexed with the new CUID. Because the check is against the existing value on the +provider record, the CUID is generated exactly once and is never regenerated on subsequent license uploads, including after a [home state change](#home-state-changes). Because it lives on the top-level provider record rather than on an individual +license, the CUID is also a top-level field in the [OpenSearch index mapping](#index-mapping), making practitioners searchable by CUID (see [Advanced Data Search](#advanced-data-search)). The intent is to only use the CUID specifically for display and search in the public search UI. It should never be used for partitioning DynamoDB records or for internal identification of records when performing operations in the system. The RID `providerId` field should be primarily used for all internal operations. @@ -519,6 +532,8 @@ Once a home MSL is confirmed eligible for a given license type, remote-state Mul Privileges are generated from a multi-state home license: one privilege per compact member jurisdiction (other than the home jurisdiction) for that license type. If the most recent multi-state license is ineligible or its associated single-state license is ineligible, no privileges are generated for that license type (there is no fallback to an older multi-state license from another jurisdiction). This means that privileges are only returned from the API for a practitioner when the most recently issued or renewed multi-state license is eligible and paired with an eligible single-state license. +Note that pairing is checked independently of the displayed eligibility: a multi-state license with **no** matching single-state license on file is still shown with its own calculated `compactEligibility`/`licenseEligibility` (reflecting only the jurisdiction-reported status, expiration, and encumbrance of that license), but it will never generate privileges, since privilege generation requires the pairing to exist in the first place. A displayed "eligible" license therefore does not by itself imply the practitioner has multistate authorization anywhere. + The following flow describes how the home state license is assigned. ([Social Work Practitioner License Assignment Flow](./practitioner-home-state-assignment.pdf)) @@ -833,8 +848,8 @@ Each indexed document corresponds to **one license** and uses the same overall s The index uses a custom ASCII-folding analyzer for name fields, which allows searching for names with international characters using their ASCII equivalents (e.g., searching "Jose" matches "José"). -**Planned:** once the Compact Unique Identifier (CUID) is implemented (see -[Commission Unique Identifiers: RID and CUID](#commission-unique-identifiers-rid-and-cuid)), the index mapping will be updated to add `publicCompactIdentifier` as a top-level, searchable field. This will allow staff users and the public to search for a practitioner by their CUID. It will also be used to filter public search results — see [Public Provider Search](#public-provider-search) below. +The index mapping also includes `publicCompactIdentifier` (see [Commission Unique Identifiers: RID and +CUID](#commission-unique-identifiers-rid-and-cuid)) as a top-level, `keyword`-typed, searchable field. This allows staff users and the public to search for a practitioner by their CUID. It is also used to filter public search results — see [Public Provider Search](#public-provider-search) below. ### Search API Endpoints @@ -859,12 +874,22 @@ search for practitioners by multi-state license number, jurisdiction, family nam The public query filters documents down to a practitioner's home multi-state license per license type (`licenses.licenseScope: multi-state` and `licenses.mostRecentLicenseForType: true`). It does not display any of their single-state licenses. -**Planned:** once the CUID is implemented (see [Commission Unique Identifiers: RID and -CUID](#commission-unique-identifiers-rid-and-cuid)), the public query will add an additional filter requiring the -top-level `publicCompactIdentifier` field to be populated (i.e. non-null/non-empty) on the document. Since the CUID -is only generated once a practitioner has a paired single-state/multi-state license on file (see [Compact Unique -Identifier (CUID)](#compact-unique-identifier-cuid--planned)), this filter has the effect of excluding practitioners -who do not have the needed license pairing for privileges from public search results entirely. This ensures the public can only find and reference practitioners using a CUID that has actually been issued to them. +The public query always adds an additional filter requiring the top-level `publicCompactIdentifier` field to be +populated (i.e. an `exists` clause) on the document. Since the CUID is only generated once a practitioner has a +paired single-state/multi-state license on file (see [Compact Unique Identifier +(CUID)](#compact-unique-identifier-cuid)), this filter has the effect of excluding practitioners who do not have +the needed license pairing for privileges from public search results entirely. This ensures the public can only +find and reference practitioners using a CUID that has actually been issued to them. + +In addition to `licenseNumber`, `jurisdiction`, `familyName`, and `givenName`, the public query request body +accepts two more structured fields under `query`: +- `cuid`: an exact, case-insensitive match against the full CUID string (validated against the CUID format at the + schema layer before it ever reaches OpenSearch — partial, wildcard, and counter-only values are rejected with a + 400). Maps to a top-level `term` query on `publicCompactIdentifier` (normalized to uppercase). +- `licenseType`: matches license type/profession designation, validated against the license types + configured for the path compact and resolved to that compact's canonically-configured casing (see + `LicenseUtility.get_license_type_by_name`). Maps to a nested `term` query on `licenses.licenseType`, which is + indexed as a case-sensitive keyword using that same canonical casing. ### Document Indexing diff --git a/backend/social-work-app/docs/design/provider-data-model-erd.mmd b/backend/social-work-app/docs/design/provider-data-model-erd.mmd index e9b695e6db..f288bde5ae 100644 --- a/backend/social-work-app/docs/design/provider-data-model-erd.mmd +++ b/backend/social-work-app/docs/design/provider-data-model-erd.mmd @@ -9,7 +9,7 @@ erDiagram string type "provider" datetime dateOfUpdate string providerId "RID, UUID4 string, internal" - string publicCompactIdentifier "optional, CUID, UUID4 string, public" + string publicCompactIdentifier "optional, CUID, format SWC-nnnn-counter, public" string compact string licenseJurisdiction "home jurisdiction" string jurisdictionUploadedLicenseStatus diff --git a/backend/social-work-app/docs/internal/api-specification/latest-oas30.json b/backend/social-work-app/docs/internal/api-specification/latest-oas30.json index 7528c8772d..83b3bbafbf 100644 --- a/backend/social-work-app/docs/internal/api-specification/latest-oas30.json +++ b/backend/social-work-app/docs/internal/api-specification/latest-oas30.json @@ -2,7 +2,7 @@ "openapi": "3.0.1", "info": { "title": "LicenseApi", - "version": "2026-06-01T19:40:20Z" + "version": "2026-08-03T17:06:48Z" }, "servers": [ { @@ -39,7 +39,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TestALicenz60E9GxgxvRT" + "$ref": "#/components/schemas/TestALicenfa1lTrBaH6fm" } } } @@ -76,7 +76,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TestALicenyPPmB5QK4Mwi" + "$ref": "#/components/schemas/TestALicenEZKeRGLs4K99" } } }, @@ -88,7 +88,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TestALicennE6IeKjO4KkD" + "$ref": "#/components/schemas/TestALicenuiANnWJRg2CR" } } } @@ -99,15 +99,59 @@ "TestBackendSocialWorkTestAPIStackLicenseApiStaffUsersPoolAuthorizer3D7855A2": [ "socw/admin", "al/socw.admin", + "ak/socw.admin", "az/socw.admin", + "ar/socw.admin", + "ca/socw.admin", "co/socw.admin", + "ct/socw.admin", + "de/socw.admin", + "dc/socw.admin", + "fl/socw.admin", + "ga/socw.admin", + "gu/socw.admin", + "hi/socw.admin", + "id/socw.admin", + "il/socw.admin", + "in/socw.admin", + "ia/socw.admin", "ks/socw.admin", "ky/socw.admin", + "la/socw.admin", + "me/socw.admin", "md/socw.admin", + "ma/socw.admin", + "mi/socw.admin", + "mn/socw.admin", + "ms/socw.admin", + "mo/socw.admin", + "mt/socw.admin", + "mp/socw.admin", + "ne/socw.admin", + "nv/socw.admin", + "nh/socw.admin", + "nj/socw.admin", + "nm/socw.admin", + "ny/socw.admin", + "nc/socw.admin", + "nd/socw.admin", "oh/socw.admin", + "ok/socw.admin", + "or/socw.admin", + "pa/socw.admin", + "ri/socw.admin", + "sc/socw.admin", + "sd/socw.admin", "tn/socw.admin", + "tx/socw.admin", + "ut/socw.admin", + "vt/socw.admin", "va/socw.admin", - "wa/socw.admin" + "vi/socw.admin", + "wa/socw.admin", + "wv/socw.admin", + "wi/socw.admin", + "wy/socw.admin" ] } ] @@ -139,7 +183,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TestALicenT0BWB0XcccEO" + "$ref": "#/components/schemas/TestALicen8l1lLxH6cpVc" } } } @@ -188,7 +232,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TestALicen49toJAATzdfY" + "$ref": "#/components/schemas/TestALicenBLppmGZCgMCA" } } } @@ -233,7 +277,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TestALicenmi91EHypaiFi" + "$ref": "#/components/schemas/TestALicen02H1rt7iqxOE" } } }, @@ -245,7 +289,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TestALicennE6IeKjO4KkD" + "$ref": "#/components/schemas/TestALicenuiANnWJRg2CR" } } } @@ -256,15 +300,59 @@ "TestBackendSocialWorkTestAPIStackLicenseApiStaffUsersPoolAuthorizer3D7855A2": [ "socw/admin", "al/socw.admin", + "ak/socw.admin", "az/socw.admin", + "ar/socw.admin", + "ca/socw.admin", "co/socw.admin", + "ct/socw.admin", + "de/socw.admin", + "dc/socw.admin", + "fl/socw.admin", + "ga/socw.admin", + "gu/socw.admin", + "hi/socw.admin", + "id/socw.admin", + "il/socw.admin", + "in/socw.admin", + "ia/socw.admin", "ks/socw.admin", "ky/socw.admin", + "la/socw.admin", + "me/socw.admin", "md/socw.admin", + "ma/socw.admin", + "mi/socw.admin", + "mn/socw.admin", + "ms/socw.admin", + "mo/socw.admin", + "mt/socw.admin", + "mp/socw.admin", + "ne/socw.admin", + "nv/socw.admin", + "nh/socw.admin", + "nj/socw.admin", + "nm/socw.admin", + "ny/socw.admin", + "nc/socw.admin", + "nd/socw.admin", "oh/socw.admin", + "ok/socw.admin", + "or/socw.admin", + "pa/socw.admin", + "ri/socw.admin", + "sc/socw.admin", + "sd/socw.admin", "tn/socw.admin", + "tx/socw.admin", + "ut/socw.admin", + "vt/socw.admin", "va/socw.admin", - "wa/socw.admin" + "vi/socw.admin", + "wa/socw.admin", + "wv/socw.admin", + "wi/socw.admin", + "wy/socw.admin" ] } ] @@ -304,7 +392,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TestALicent8Cwee4tcVEF" + "$ref": "#/components/schemas/TestALicen5hRh5X7Ztohi" } } } @@ -315,15 +403,59 @@ "TestBackendSocialWorkTestAPIStackLicenseApiStaffUsersPoolAuthorizer3D7855A2": [ "socw/write", "al/socw.write", + "ak/socw.write", "az/socw.write", + "ar/socw.write", + "ca/socw.write", "co/socw.write", + "ct/socw.write", + "de/socw.write", + "dc/socw.write", + "fl/socw.write", + "ga/socw.write", + "gu/socw.write", + "hi/socw.write", + "id/socw.write", + "il/socw.write", + "in/socw.write", + "ia/socw.write", "ks/socw.write", "ky/socw.write", + "la/socw.write", + "me/socw.write", "md/socw.write", + "ma/socw.write", + "mi/socw.write", + "mn/socw.write", + "ms/socw.write", + "mo/socw.write", + "mt/socw.write", + "mp/socw.write", + "ne/socw.write", + "nv/socw.write", + "nh/socw.write", + "nj/socw.write", + "nm/socw.write", + "ny/socw.write", + "nc/socw.write", + "nd/socw.write", "oh/socw.write", + "ok/socw.write", + "or/socw.write", + "pa/socw.write", + "ri/socw.write", + "sc/socw.write", + "sd/socw.write", "tn/socw.write", + "tx/socw.write", + "ut/socw.write", + "vt/socw.write", "va/socw.write", - "wa/socw.write" + "vi/socw.write", + "wa/socw.write", + "wv/socw.write", + "wi/socw.write", + "wy/socw.write" ] } ] @@ -353,7 +485,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TestALicen4oBA2oRHRolJ" + "$ref": "#/components/schemas/TestALicenMKPmf1mi0Wzd" } } }, @@ -365,7 +497,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TestALicenNEZjECY0EIXB" + "$ref": "#/components/schemas/TestALicenFZgseK5NUIOL" } } } @@ -414,7 +546,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TestALicenEuuJ2hePr1dE" + "$ref": "#/components/schemas/TestALicen50rcrMXljKAv" } } } @@ -477,7 +609,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TestALicennkQ1XbRgr9l0" + "$ref": "#/components/schemas/TestALicendpUtSr8g3DI4" } } }, @@ -489,7 +621,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TestALicennE6IeKjO4KkD" + "$ref": "#/components/schemas/TestALicenuiANnWJRg2CR" } } } @@ -500,15 +632,59 @@ "TestBackendSocialWorkTestAPIStackLicenseApiStaffUsersPoolAuthorizer3D7855A2": [ "socw/admin", "al/socw.admin", + "ak/socw.admin", "az/socw.admin", + "ar/socw.admin", + "ca/socw.admin", "co/socw.admin", + "ct/socw.admin", + "de/socw.admin", + "dc/socw.admin", + "fl/socw.admin", + "ga/socw.admin", + "gu/socw.admin", + "hi/socw.admin", + "id/socw.admin", + "il/socw.admin", + "in/socw.admin", + "ia/socw.admin", "ks/socw.admin", "ky/socw.admin", + "la/socw.admin", + "me/socw.admin", "md/socw.admin", + "ma/socw.admin", + "mi/socw.admin", + "mn/socw.admin", + "ms/socw.admin", + "mo/socw.admin", + "mt/socw.admin", + "mp/socw.admin", + "ne/socw.admin", + "nv/socw.admin", + "nh/socw.admin", + "nj/socw.admin", + "nm/socw.admin", + "ny/socw.admin", + "nc/socw.admin", + "nd/socw.admin", "oh/socw.admin", + "ok/socw.admin", + "or/socw.admin", + "pa/socw.admin", + "ri/socw.admin", + "sc/socw.admin", + "sd/socw.admin", "tn/socw.admin", + "tx/socw.admin", + "ut/socw.admin", + "vt/socw.admin", "va/socw.admin", - "wa/socw.admin" + "vi/socw.admin", + "wa/socw.admin", + "wv/socw.admin", + "wi/socw.admin", + "wy/socw.admin" ] } ] @@ -570,7 +746,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TestALicenpht9Tj1tkyAU" + "$ref": "#/components/schemas/TestALicenQPmgNWK0HpLx" } } }, @@ -582,7 +758,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TestALicennE6IeKjO4KkD" + "$ref": "#/components/schemas/TestALicenuiANnWJRg2CR" } } } @@ -593,15 +769,59 @@ "TestBackendSocialWorkTestAPIStackLicenseApiStaffUsersPoolAuthorizer3D7855A2": [ "socw/admin", "al/socw.admin", + "ak/socw.admin", "az/socw.admin", + "ar/socw.admin", + "ca/socw.admin", "co/socw.admin", + "ct/socw.admin", + "de/socw.admin", + "dc/socw.admin", + "fl/socw.admin", + "ga/socw.admin", + "gu/socw.admin", + "hi/socw.admin", + "id/socw.admin", + "il/socw.admin", + "in/socw.admin", + "ia/socw.admin", "ks/socw.admin", "ky/socw.admin", + "la/socw.admin", + "me/socw.admin", "md/socw.admin", + "ma/socw.admin", + "mi/socw.admin", + "mn/socw.admin", + "ms/socw.admin", + "mo/socw.admin", + "mt/socw.admin", + "mp/socw.admin", + "ne/socw.admin", + "nv/socw.admin", + "nh/socw.admin", + "nj/socw.admin", + "nm/socw.admin", + "ny/socw.admin", + "nc/socw.admin", + "nd/socw.admin", "oh/socw.admin", + "ok/socw.admin", + "or/socw.admin", + "pa/socw.admin", + "ri/socw.admin", + "sc/socw.admin", + "sd/socw.admin", "tn/socw.admin", + "tx/socw.admin", + "ut/socw.admin", + "vt/socw.admin", "va/socw.admin", - "wa/socw.admin" + "vi/socw.admin", + "wa/socw.admin", + "wv/socw.admin", + "wi/socw.admin", + "wy/socw.admin" ] } ] @@ -655,7 +875,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TestALicenmyF8U9gFO2mb" + "$ref": "#/components/schemas/TestALicenkIOuhcYR4oy0" } } }, @@ -667,7 +887,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TestALicennE6IeKjO4KkD" + "$ref": "#/components/schemas/TestALicenuiANnWJRg2CR" } } } @@ -678,15 +898,59 @@ "TestBackendSocialWorkTestAPIStackLicenseApiStaffUsersPoolAuthorizer3D7855A2": [ "socw/admin", "al/socw.admin", + "ak/socw.admin", "az/socw.admin", + "ar/socw.admin", + "ca/socw.admin", "co/socw.admin", + "ct/socw.admin", + "de/socw.admin", + "dc/socw.admin", + "fl/socw.admin", + "ga/socw.admin", + "gu/socw.admin", + "hi/socw.admin", + "id/socw.admin", + "il/socw.admin", + "in/socw.admin", + "ia/socw.admin", "ks/socw.admin", "ky/socw.admin", + "la/socw.admin", + "me/socw.admin", "md/socw.admin", + "ma/socw.admin", + "mi/socw.admin", + "mn/socw.admin", + "ms/socw.admin", + "mo/socw.admin", + "mt/socw.admin", + "mp/socw.admin", + "ne/socw.admin", + "nv/socw.admin", + "nh/socw.admin", + "nj/socw.admin", + "nm/socw.admin", + "ny/socw.admin", + "nc/socw.admin", + "nd/socw.admin", "oh/socw.admin", + "ok/socw.admin", + "or/socw.admin", + "pa/socw.admin", + "ri/socw.admin", + "sc/socw.admin", + "sd/socw.admin", "tn/socw.admin", + "tx/socw.admin", + "ut/socw.admin", + "vt/socw.admin", "va/socw.admin", - "wa/socw.admin" + "vi/socw.admin", + "wa/socw.admin", + "wv/socw.admin", + "wi/socw.admin", + "wy/socw.admin" ] } ] @@ -748,7 +1012,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TestALicenbkno2SNuWuvY" + "$ref": "#/components/schemas/TestALicenmQla1snuDbv9" } } }, @@ -760,7 +1024,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TestALicennE6IeKjO4KkD" + "$ref": "#/components/schemas/TestALicenuiANnWJRg2CR" } } } @@ -771,15 +1035,59 @@ "TestBackendSocialWorkTestAPIStackLicenseApiStaffUsersPoolAuthorizer3D7855A2": [ "socw/admin", "al/socw.admin", + "ak/socw.admin", "az/socw.admin", + "ar/socw.admin", + "ca/socw.admin", "co/socw.admin", + "ct/socw.admin", + "de/socw.admin", + "dc/socw.admin", + "fl/socw.admin", + "ga/socw.admin", + "gu/socw.admin", + "hi/socw.admin", + "id/socw.admin", + "il/socw.admin", + "in/socw.admin", + "ia/socw.admin", "ks/socw.admin", "ky/socw.admin", + "la/socw.admin", + "me/socw.admin", "md/socw.admin", + "ma/socw.admin", + "mi/socw.admin", + "mn/socw.admin", + "ms/socw.admin", + "mo/socw.admin", + "mt/socw.admin", + "mp/socw.admin", + "ne/socw.admin", + "nv/socw.admin", + "nh/socw.admin", + "nj/socw.admin", + "nm/socw.admin", + "ny/socw.admin", + "nc/socw.admin", + "nd/socw.admin", "oh/socw.admin", + "ok/socw.admin", + "or/socw.admin", + "pa/socw.admin", + "ri/socw.admin", + "sc/socw.admin", + "sd/socw.admin", "tn/socw.admin", + "tx/socw.admin", + "ut/socw.admin", + "vt/socw.admin", "va/socw.admin", - "wa/socw.admin" + "vi/socw.admin", + "wa/socw.admin", + "wv/socw.admin", + "wi/socw.admin", + "wy/socw.admin" ] } ] @@ -833,7 +1141,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TestALicenssEUeQmNqrM6" + "$ref": "#/components/schemas/TestALicenAQXO3VijbS3U" } } }, @@ -845,7 +1153,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TestALicennE6IeKjO4KkD" + "$ref": "#/components/schemas/TestALicenuiANnWJRg2CR" } } } @@ -856,15 +1164,59 @@ "TestBackendSocialWorkTestAPIStackLicenseApiStaffUsersPoolAuthorizer3D7855A2": [ "socw/admin", "al/socw.admin", + "ak/socw.admin", "az/socw.admin", + "ar/socw.admin", + "ca/socw.admin", "co/socw.admin", + "ct/socw.admin", + "de/socw.admin", + "dc/socw.admin", + "fl/socw.admin", + "ga/socw.admin", + "gu/socw.admin", + "hi/socw.admin", + "id/socw.admin", + "il/socw.admin", + "in/socw.admin", + "ia/socw.admin", "ks/socw.admin", "ky/socw.admin", + "la/socw.admin", + "me/socw.admin", "md/socw.admin", + "ma/socw.admin", + "mi/socw.admin", + "mn/socw.admin", + "ms/socw.admin", + "mo/socw.admin", + "mt/socw.admin", + "mp/socw.admin", + "ne/socw.admin", + "nv/socw.admin", + "nh/socw.admin", + "nj/socw.admin", + "nm/socw.admin", + "ny/socw.admin", + "nc/socw.admin", + "nd/socw.admin", "oh/socw.admin", + "ok/socw.admin", + "or/socw.admin", + "pa/socw.admin", + "ri/socw.admin", + "sc/socw.admin", + "sd/socw.admin", "tn/socw.admin", + "tx/socw.admin", + "ut/socw.admin", + "vt/socw.admin", "va/socw.admin", - "wa/socw.admin" + "vi/socw.admin", + "wa/socw.admin", + "wv/socw.admin", + "wi/socw.admin", + "wy/socw.admin" ] } ] @@ -926,7 +1278,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TestALicenYxu8vMXnF28M" + "$ref": "#/components/schemas/TestALicenNuwYXlN2vJVp" } } }, @@ -938,7 +1290,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TestALicennE6IeKjO4KkD" + "$ref": "#/components/schemas/TestALicenuiANnWJRg2CR" } } } @@ -949,15 +1301,59 @@ "TestBackendSocialWorkTestAPIStackLicenseApiStaffUsersPoolAuthorizer3D7855A2": [ "socw/admin", "al/socw.admin", + "ak/socw.admin", "az/socw.admin", + "ar/socw.admin", + "ca/socw.admin", "co/socw.admin", + "ct/socw.admin", + "de/socw.admin", + "dc/socw.admin", + "fl/socw.admin", + "ga/socw.admin", + "gu/socw.admin", + "hi/socw.admin", + "id/socw.admin", + "il/socw.admin", + "in/socw.admin", + "ia/socw.admin", "ks/socw.admin", "ky/socw.admin", + "la/socw.admin", + "me/socw.admin", "md/socw.admin", + "ma/socw.admin", + "mi/socw.admin", + "mn/socw.admin", + "ms/socw.admin", + "mo/socw.admin", + "mt/socw.admin", + "mp/socw.admin", + "ne/socw.admin", + "nv/socw.admin", + "nh/socw.admin", + "nj/socw.admin", + "nm/socw.admin", + "ny/socw.admin", + "nc/socw.admin", + "nd/socw.admin", "oh/socw.admin", + "ok/socw.admin", + "or/socw.admin", + "pa/socw.admin", + "ri/socw.admin", + "sc/socw.admin", + "sd/socw.admin", "tn/socw.admin", + "tx/socw.admin", + "ut/socw.admin", + "vt/socw.admin", "va/socw.admin", - "wa/socw.admin" + "vi/socw.admin", + "wa/socw.admin", + "wv/socw.admin", + "wi/socw.admin", + "wy/socw.admin" ] } ] @@ -1011,7 +1407,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TestALicenEVsrIP1Cbe0h" + "$ref": "#/components/schemas/TestALicenMgJ48N3eFq37" } } }, @@ -1023,7 +1419,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TestALicennE6IeKjO4KkD" + "$ref": "#/components/schemas/TestALicenuiANnWJRg2CR" } } } @@ -1034,15 +1430,59 @@ "TestBackendSocialWorkTestAPIStackLicenseApiStaffUsersPoolAuthorizer3D7855A2": [ "socw/admin", "al/socw.admin", + "ak/socw.admin", "az/socw.admin", + "ar/socw.admin", + "ca/socw.admin", "co/socw.admin", + "ct/socw.admin", + "de/socw.admin", + "dc/socw.admin", + "fl/socw.admin", + "ga/socw.admin", + "gu/socw.admin", + "hi/socw.admin", + "id/socw.admin", + "il/socw.admin", + "in/socw.admin", + "ia/socw.admin", "ks/socw.admin", "ky/socw.admin", + "la/socw.admin", + "me/socw.admin", "md/socw.admin", + "ma/socw.admin", + "mi/socw.admin", + "mn/socw.admin", + "ms/socw.admin", + "mo/socw.admin", + "mt/socw.admin", + "mp/socw.admin", + "ne/socw.admin", + "nv/socw.admin", + "nh/socw.admin", + "nj/socw.admin", + "nm/socw.admin", + "ny/socw.admin", + "nc/socw.admin", + "nd/socw.admin", "oh/socw.admin", + "ok/socw.admin", + "or/socw.admin", + "pa/socw.admin", + "ri/socw.admin", + "sc/socw.admin", + "sd/socw.admin", "tn/socw.admin", + "tx/socw.admin", + "ut/socw.admin", + "vt/socw.admin", "va/socw.admin", - "wa/socw.admin" + "vi/socw.admin", + "wa/socw.admin", + "wv/socw.admin", + "wi/socw.admin", + "wy/socw.admin" ] } ] @@ -1104,7 +1544,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TestALicenPU78UBpLgMaP" + "$ref": "#/components/schemas/TestALicenv73HP1pQkT4T" } } }, @@ -1116,7 +1556,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TestALicennE6IeKjO4KkD" + "$ref": "#/components/schemas/TestALicenuiANnWJRg2CR" } } } @@ -1127,15 +1567,59 @@ "TestBackendSocialWorkTestAPIStackLicenseApiStaffUsersPoolAuthorizer3D7855A2": [ "socw/admin", "al/socw.admin", + "ak/socw.admin", "az/socw.admin", + "ar/socw.admin", + "ca/socw.admin", "co/socw.admin", + "ct/socw.admin", + "de/socw.admin", + "dc/socw.admin", + "fl/socw.admin", + "ga/socw.admin", + "gu/socw.admin", + "hi/socw.admin", + "id/socw.admin", + "il/socw.admin", + "in/socw.admin", + "ia/socw.admin", "ks/socw.admin", "ky/socw.admin", + "la/socw.admin", + "me/socw.admin", "md/socw.admin", + "ma/socw.admin", + "mi/socw.admin", + "mn/socw.admin", + "ms/socw.admin", + "mo/socw.admin", + "mt/socw.admin", + "mp/socw.admin", + "ne/socw.admin", + "nv/socw.admin", + "nh/socw.admin", + "nj/socw.admin", + "nm/socw.admin", + "ny/socw.admin", + "nc/socw.admin", + "nd/socw.admin", "oh/socw.admin", + "ok/socw.admin", + "or/socw.admin", + "pa/socw.admin", + "ri/socw.admin", + "sc/socw.admin", + "sd/socw.admin", "tn/socw.admin", + "tx/socw.admin", + "ut/socw.admin", + "vt/socw.admin", "va/socw.admin", - "wa/socw.admin" + "vi/socw.admin", + "wa/socw.admin", + "wv/socw.admin", + "wi/socw.admin", + "wy/socw.admin" ] } ] @@ -1166,7 +1650,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TestALicenSnbehaHnpXHv" + "$ref": "#/components/schemas/TestALicen1p96CBusH3Yd" } } } @@ -1177,15 +1661,59 @@ "TestBackendSocialWorkTestAPIStackLicenseApiStaffUsersPoolAuthorizer3D7855A2": [ "socw/admin", "al/socw.admin", + "ak/socw.admin", "az/socw.admin", + "ar/socw.admin", + "ca/socw.admin", "co/socw.admin", + "ct/socw.admin", + "de/socw.admin", + "dc/socw.admin", + "fl/socw.admin", + "ga/socw.admin", + "gu/socw.admin", + "hi/socw.admin", + "id/socw.admin", + "il/socw.admin", + "in/socw.admin", + "ia/socw.admin", "ks/socw.admin", "ky/socw.admin", + "la/socw.admin", + "me/socw.admin", "md/socw.admin", + "ma/socw.admin", + "mi/socw.admin", + "mn/socw.admin", + "ms/socw.admin", + "mo/socw.admin", + "mt/socw.admin", + "mp/socw.admin", + "ne/socw.admin", + "nv/socw.admin", + "nh/socw.admin", + "nj/socw.admin", + "nm/socw.admin", + "ny/socw.admin", + "nc/socw.admin", + "nd/socw.admin", "oh/socw.admin", + "ok/socw.admin", + "or/socw.admin", + "pa/socw.admin", + "ri/socw.admin", + "sc/socw.admin", + "sd/socw.admin", "tn/socw.admin", + "tx/socw.admin", + "ut/socw.admin", + "vt/socw.admin", "va/socw.admin", - "wa/socw.admin" + "vi/socw.admin", + "wa/socw.admin", + "wv/socw.admin", + "wi/socw.admin", + "wy/socw.admin" ] } ] @@ -1205,7 +1733,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TestALicenAxFmX05QrlLj" + "$ref": "#/components/schemas/TestALicen3IyOh208lr9M" } } }, @@ -1224,7 +1752,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TestALicenwtaAzkUzgvMz" + "$ref": "#/components/schemas/TestALicenipCN6Fzx8gkn" } } } @@ -1235,15 +1763,59 @@ "TestBackendSocialWorkTestAPIStackLicenseApiStaffUsersPoolAuthorizer3D7855A2": [ "socw/admin", "al/socw.admin", + "ak/socw.admin", "az/socw.admin", + "ar/socw.admin", + "ca/socw.admin", "co/socw.admin", + "ct/socw.admin", + "de/socw.admin", + "dc/socw.admin", + "fl/socw.admin", + "ga/socw.admin", + "gu/socw.admin", + "hi/socw.admin", + "id/socw.admin", + "il/socw.admin", + "in/socw.admin", + "ia/socw.admin", "ks/socw.admin", "ky/socw.admin", + "la/socw.admin", + "me/socw.admin", "md/socw.admin", + "ma/socw.admin", + "mi/socw.admin", + "mn/socw.admin", + "ms/socw.admin", + "mo/socw.admin", + "mt/socw.admin", + "mp/socw.admin", + "ne/socw.admin", + "nv/socw.admin", + "nh/socw.admin", + "nj/socw.admin", + "nm/socw.admin", + "ny/socw.admin", + "nc/socw.admin", + "nd/socw.admin", "oh/socw.admin", + "ok/socw.admin", + "or/socw.admin", + "pa/socw.admin", + "ri/socw.admin", + "sc/socw.admin", + "sd/socw.admin", "tn/socw.admin", + "tx/socw.admin", + "ut/socw.admin", + "vt/socw.admin", "va/socw.admin", - "wa/socw.admin" + "vi/socw.admin", + "wa/socw.admin", + "wv/socw.admin", + "wi/socw.admin", + "wy/socw.admin" ] } ] @@ -1275,7 +1847,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TestALicennE6IeKjO4KkD" + "$ref": "#/components/schemas/TestALicenuiANnWJRg2CR" } } } @@ -1292,7 +1864,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TestALicenwtaAzkUzgvMz" + "$ref": "#/components/schemas/TestALicenipCN6Fzx8gkn" } } } @@ -1303,15 +1875,59 @@ "TestBackendSocialWorkTestAPIStackLicenseApiStaffUsersPoolAuthorizer3D7855A2": [ "socw/admin", "al/socw.admin", + "ak/socw.admin", "az/socw.admin", + "ar/socw.admin", + "ca/socw.admin", "co/socw.admin", + "ct/socw.admin", + "de/socw.admin", + "dc/socw.admin", + "fl/socw.admin", + "ga/socw.admin", + "gu/socw.admin", + "hi/socw.admin", + "id/socw.admin", + "il/socw.admin", + "in/socw.admin", + "ia/socw.admin", "ks/socw.admin", "ky/socw.admin", + "la/socw.admin", + "me/socw.admin", "md/socw.admin", + "ma/socw.admin", + "mi/socw.admin", + "mn/socw.admin", + "ms/socw.admin", + "mo/socw.admin", + "mt/socw.admin", + "mp/socw.admin", + "ne/socw.admin", + "nv/socw.admin", + "nh/socw.admin", + "nj/socw.admin", + "nm/socw.admin", + "ny/socw.admin", + "nc/socw.admin", + "nd/socw.admin", "oh/socw.admin", + "ok/socw.admin", + "or/socw.admin", + "pa/socw.admin", + "ri/socw.admin", + "sc/socw.admin", + "sd/socw.admin", "tn/socw.admin", + "tx/socw.admin", + "ut/socw.admin", + "vt/socw.admin", "va/socw.admin", - "wa/socw.admin" + "vi/socw.admin", + "wa/socw.admin", + "wv/socw.admin", + "wi/socw.admin", + "wy/socw.admin" ] } ] @@ -1341,7 +1957,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TestALicennE6IeKjO4KkD" + "$ref": "#/components/schemas/TestALicenuiANnWJRg2CR" } } } @@ -1351,7 +1967,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TestALicennE6IeKjO4KkD" + "$ref": "#/components/schemas/TestALicenuiANnWJRg2CR" } } } @@ -1362,15 +1978,59 @@ "TestBackendSocialWorkTestAPIStackLicenseApiStaffUsersPoolAuthorizer3D7855A2": [ "socw/admin", "al/socw.admin", + "ak/socw.admin", "az/socw.admin", + "ar/socw.admin", + "ca/socw.admin", "co/socw.admin", + "ct/socw.admin", + "de/socw.admin", + "dc/socw.admin", + "fl/socw.admin", + "ga/socw.admin", + "gu/socw.admin", + "hi/socw.admin", + "id/socw.admin", + "il/socw.admin", + "in/socw.admin", + "ia/socw.admin", "ks/socw.admin", "ky/socw.admin", + "la/socw.admin", + "me/socw.admin", "md/socw.admin", + "ma/socw.admin", + "mi/socw.admin", + "mn/socw.admin", + "ms/socw.admin", + "mo/socw.admin", + "mt/socw.admin", + "mp/socw.admin", + "ne/socw.admin", + "nv/socw.admin", + "nh/socw.admin", + "nj/socw.admin", + "nm/socw.admin", + "ny/socw.admin", + "nc/socw.admin", + "nd/socw.admin", "oh/socw.admin", + "ok/socw.admin", + "or/socw.admin", + "pa/socw.admin", + "ri/socw.admin", + "sc/socw.admin", + "sd/socw.admin", "tn/socw.admin", + "tx/socw.admin", + "ut/socw.admin", + "vt/socw.admin", "va/socw.admin", - "wa/socw.admin" + "vi/socw.admin", + "wa/socw.admin", + "wv/socw.admin", + "wi/socw.admin", + "wy/socw.admin" ] } ] @@ -1398,7 +2058,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TestALicen9OmWyHE3jPYa" + "$ref": "#/components/schemas/TestALicenGYxCbbaqiA8o" } } }, @@ -1410,7 +2070,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TestALicennE6IeKjO4KkD" + "$ref": "#/components/schemas/TestALicenuiANnWJRg2CR" } } } @@ -1427,7 +2087,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TestALicenwtaAzkUzgvMz" + "$ref": "#/components/schemas/TestALicenipCN6Fzx8gkn" } } } @@ -1438,15 +2098,59 @@ "TestBackendSocialWorkTestAPIStackLicenseApiStaffUsersPoolAuthorizer3D7855A2": [ "socw/admin", "al/socw.admin", + "ak/socw.admin", "az/socw.admin", + "ar/socw.admin", + "ca/socw.admin", "co/socw.admin", + "ct/socw.admin", + "de/socw.admin", + "dc/socw.admin", + "fl/socw.admin", + "ga/socw.admin", + "gu/socw.admin", + "hi/socw.admin", + "id/socw.admin", + "il/socw.admin", + "in/socw.admin", + "ia/socw.admin", "ks/socw.admin", "ky/socw.admin", + "la/socw.admin", + "me/socw.admin", "md/socw.admin", + "ma/socw.admin", + "mi/socw.admin", + "mn/socw.admin", + "ms/socw.admin", + "mo/socw.admin", + "mt/socw.admin", + "mp/socw.admin", + "ne/socw.admin", + "nv/socw.admin", + "nh/socw.admin", + "nj/socw.admin", + "nm/socw.admin", + "ny/socw.admin", + "nc/socw.admin", + "nd/socw.admin", "oh/socw.admin", + "ok/socw.admin", + "or/socw.admin", + "pa/socw.admin", + "ri/socw.admin", + "sc/socw.admin", + "sd/socw.admin", "tn/socw.admin", + "tx/socw.admin", + "ut/socw.admin", + "vt/socw.admin", "va/socw.admin", - "wa/socw.admin" + "vi/socw.admin", + "wa/socw.admin", + "wv/socw.admin", + "wi/socw.admin", + "wy/socw.admin" ] } ] @@ -1478,7 +2182,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TestALicennE6IeKjO4KkD" + "$ref": "#/components/schemas/TestALicenuiANnWJRg2CR" } } } @@ -1488,7 +2192,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TestALicennE6IeKjO4KkD" + "$ref": "#/components/schemas/TestALicenuiANnWJRg2CR" } } } @@ -1499,15 +2203,59 @@ "TestBackendSocialWorkTestAPIStackLicenseApiStaffUsersPoolAuthorizer3D7855A2": [ "socw/admin", "al/socw.admin", + "ak/socw.admin", "az/socw.admin", + "ar/socw.admin", + "ca/socw.admin", "co/socw.admin", + "ct/socw.admin", + "de/socw.admin", + "dc/socw.admin", + "fl/socw.admin", + "ga/socw.admin", + "gu/socw.admin", + "hi/socw.admin", + "id/socw.admin", + "il/socw.admin", + "in/socw.admin", + "ia/socw.admin", "ks/socw.admin", "ky/socw.admin", + "la/socw.admin", + "me/socw.admin", "md/socw.admin", + "ma/socw.admin", + "mi/socw.admin", + "mn/socw.admin", + "ms/socw.admin", + "mo/socw.admin", + "mt/socw.admin", + "mp/socw.admin", + "ne/socw.admin", + "nv/socw.admin", + "nh/socw.admin", + "nj/socw.admin", + "nm/socw.admin", + "ny/socw.admin", + "nc/socw.admin", + "nd/socw.admin", "oh/socw.admin", + "ok/socw.admin", + "or/socw.admin", + "pa/socw.admin", + "ri/socw.admin", + "sc/socw.admin", + "sd/socw.admin", "tn/socw.admin", + "tx/socw.admin", + "ut/socw.admin", + "vt/socw.admin", "va/socw.admin", - "wa/socw.admin" + "vi/socw.admin", + "wa/socw.admin", + "wv/socw.admin", + "wi/socw.admin", + "wy/socw.admin" ] } ] @@ -1529,7 +2277,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TestALicenuf1t1AOWtbEj" + "$ref": "#/components/schemas/TestALicenrqFo9bivqy6R" } } }, @@ -1541,7 +2289,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TestALicenTcO3YVaPklzG" + "$ref": "#/components/schemas/TestALicenMm93S3Be4KsL" } } } @@ -1567,7 +2315,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TestALicenT0BWB0XcccEO" + "$ref": "#/components/schemas/TestALicen8l1lLxH6cpVc" } } } @@ -1591,7 +2339,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TestALicen4oBA2oRHRolJ" + "$ref": "#/components/schemas/TestALicenqy3W03pETQIx" } } }, @@ -1603,7 +2351,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TestALicengf4L1EygQw9V" + "$ref": "#/components/schemas/TestALicenFHq7OPw0O1oA" } } } @@ -1637,7 +2385,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TestALicenI8ObIK8aJtpE" + "$ref": "#/components/schemas/TestALiceng17w0SubrxkD" } } } @@ -1662,7 +2410,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TestALicenhSWRXn61vgaB" + "$ref": "#/components/schemas/TestALicenVn3QXzPWFWzc" } } } @@ -1678,7 +2426,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TestALicennE6IeKjO4KkD" + "$ref": "#/components/schemas/TestALicenuiANnWJRg2CR" } } } @@ -1695,7 +2443,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TestALicenwtaAzkUzgvMz" + "$ref": "#/components/schemas/TestALicenipCN6Fzx8gkn" } } } @@ -1714,7 +2462,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TestALicenJRLZQUqRJlZW" + "$ref": "#/components/schemas/TestALicenpkpZkk1id4nC" } } }, @@ -1726,7 +2474,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TestALicennE6IeKjO4KkD" + "$ref": "#/components/schemas/TestALicenuiANnWJRg2CR" } } } @@ -1743,7 +2491,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TestALicenwtaAzkUzgvMz" + "$ref": "#/components/schemas/TestALicenipCN6Fzx8gkn" } } } @@ -1761,86 +2509,98 @@ }, "components": { "schemas": { - "TestALicenAxFmX05QrlLj": { - "required": [ - "attributes", - "permissions" - ], + "TestALicenVn3QXzPWFWzc": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "al", + "ak", + "az", + "ar", + "ca", + "co", + "ct", + "de", + "dc", + "fl", + "ga", + "gu", + "hi", + "id", + "il", + "in", + "ia", + "ks", + "ky", + "la", + "me", + "md", + "ma", + "mi", + "mn", + "ms", + "mo", + "mt", + "mp", + "ne", + "nv", + "nh", + "nj", + "nm", + "ny", + "nc", + "nd", + "oh", + "ok", + "or", + "pa", + "ri", + "sc", + "sd", + "tn", + "tx", + "ut", + "vt", + "va", + "vi", + "wa", + "wv", + "wi", + "wy" + ] + } + } + }, + "TestALicenrqFo9bivqy6R": { "type": "object", "properties": { - "permissions": { - "type": "object", - "additionalProperties": { - "type": "object", - "properties": { - "actions": { - "type": "object", - "properties": { - "readPrivate": { - "type": "boolean" - }, - "admin": { - "type": "boolean" - } - } - }, - "jurisdictions": { - "type": "object", - "additionalProperties": { - "type": "object", - "properties": { - "actions": { - "type": "object", - "properties": { - "readPrivate": { - "type": "boolean" - }, - "admin": { - "type": "boolean" - }, - "write": { - "type": "boolean" - } - }, - "additionalProperties": false - } - } - } - } - }, - "additionalProperties": false - } - }, - "attributes": { - "required": [ - "email", - "familyName", - "givenName" - ], + "context": { "type": "object", "properties": { - "givenName": { - "maxLength": 100, - "minLength": 1, - "type": "string" - }, - "familyName": { + "userId": { "maxLength": 100, "minLength": 1, - "type": "string" + "type": "string", + "description": "Optional user ID for feature flag evaluation" }, - "email": { - "maxLength": 100, - "minLength": 5, - "type": "string" + "customAttributes": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Optional custom attributes for feature flag evaluation" } }, - "additionalProperties": false + "additionalProperties": false, + "description": "Optional context for feature flag evaluation" } }, "additionalProperties": false }, - "TestALicen9OmWyHE3jPYa": { + "TestALicenGYxCbbaqiA8o": { "type": "object", "properties": { "permissions": { @@ -1889,54 +2649,85 @@ }, "additionalProperties": false }, - "TestALicenJRLZQUqRJlZW": { + "TestALicenAQXO3VijbS3U": { + "required": [ + "clinicalPrivilegeActionCategories", + "encumbranceEffectiveDate", + "encumbranceType" + ], "type": "object", "properties": { - "attributes": { - "type": "object", - "properties": { - "givenName": { - "maxLength": 100, - "minLength": 1, - "type": "string" - }, - "familyName": { - "maxLength": 100, - "minLength": 1, - "type": "string" - } - }, - "additionalProperties": false + "encumbranceEffectiveDate": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "description": "The effective date of the encumbrance", + "format": "date" + }, + "clinicalPrivilegeActionCategories": { + "minItems": 1, + "type": "array", + "description": "The categories of clinical privilege action", + "items": { + "type": "string", + "enum": [ + "Non-Compliance With Requirements", + "Conflict of Interest", + "Substandard Care or Patient Neglect/Abuse", + "Criminal Conviction or Adjudication", + "Confidentiality, Consent or Disclosure Violations", + "Fraud, Deception, or Misrepresentation", + "Improper Supervision or Allowing Unlicensed Practice", + "Improper Prescribing, Dispensing, Administering Medication/Drug Violation", + "Other" + ] + } + }, + "encumbranceType": { + "type": "string", + "description": "The type of encumbrance", + "enum": [ + "fine", + "reprimand", + "required supervision", + "completion of continuing education", + "public reprimand", + "probation", + "injunctive action", + "suspension", + "revocation", + "denial", + "surrender of license", + "modification of previous action-extension", + "modification of previous action-reduction", + "other monitoring", + "other adjudicated action not listed" + ] } }, - "additionalProperties": false + "additionalProperties": false, + "description": "Encumbrance data to create" }, - "TestALicengf4L1EygQw9V": { + "TestALicenMKPmf1mi0Wzd": { "required": [ - "pagination", - "providers" + "query" ], "type": "object", "properties": { "pagination": { "type": "object", "properties": { - "prevLastKey": { - "maxLength": 1024, - "minLength": 1, - "type": "object" - }, "lastKey": { "maxLength": 1024, "minLength": 1, - "type": "object" + "type": "string" }, "pageSize": { "maximum": 100, "minimum": 5, "type": "integer" } - } + }, + "additionalProperties": false }, "query": { "type": "object", @@ -1948,24 +2739,68 @@ }, "jurisdiction": { "type": "string", - "description": "Filter for providers with privilege/license in a jurisdiction", + "description": "Filter for providers with license in a jurisdiction", "enum": [ "al", + "ak", "az", + "ar", + "ca", "co", + "ct", + "de", + "dc", + "fl", + "ga", + "gu", + "hi", + "id", + "il", + "in", + "ia", "ks", "ky", + "la", + "me", "md", + "ma", + "mi", + "mn", + "ms", + "mo", + "mt", + "mp", + "ne", + "nv", + "nh", + "nj", + "nm", + "ny", + "nc", + "nd", "oh", + "ok", + "or", + "pa", + "ri", + "sc", + "sd", "tn", + "tx", + "ut", + "vt", "va", - "wa" + "vi", + "wa", + "wv", + "wi", + "wy" ] }, "givenName": { "maxLength": 100, "type": "string", - "description": "Filter for providers with a given name" + "description": "Filter for providers with a given name (familyName is required if givenName is provided)" }, "familyName": { "maxLength": 100, @@ -1978,7 +2813,9 @@ "type": "string", "description": "Filter for licenses with a specific license number" } - } + }, + "additionalProperties": false, + "description": "The query parameters" }, "sorting": { "required": [ @@ -2004,51 +2841,117 @@ } }, "description": "How to sort results" - }, - "providers": { - "maxLength": 100, + } + }, + "additionalProperties": false + }, + "TestALiceng17w0SubrxkD": { + "required": [ + "compact", + "dateOfUpdate", + "familyName", + "givenName", + "licenseJurisdiction", + "providerId", + "type" + ], + "type": "object", + "properties": { + "privileges": { "type": "array", "items": { "required": [ + "administratorSetStatus", "compact", - "familyName", - "givenName", - "licenseEligibility", + "dateOfExpiration", + "jurisdiction", "licenseJurisdiction", - "licenseNumber", - "licenseScope", "licenseType", - "providerId" + "providerId", + "status", + "type" ], "type": "object", "properties": { "licenseType": { "type": "string", - "description": "License type or profession designation for this license row" + "enum": [ + "licensed clinical social worker", + "licensed master social worker", + "licensed bachelors social worker" + ] }, - "licenseEligibility": { + "administratorSetStatus": { "type": "string", - "description": "Whether the license is eligible for compact participation in public search results", "enum": [ - "eligible", - "ineligible" + "active", + "inactive" ] }, "licenseJurisdiction": { "type": "string", "enum": [ "al", + "ak", "az", + "ar", + "ca", "co", + "ct", + "de", + "dc", + "fl", + "ga", + "gu", + "hi", + "id", + "il", + "in", + "ia", "ks", "ky", + "la", + "me", "md", + "ma", + "mi", + "mn", + "ms", + "mo", + "mt", + "mp", + "ne", + "nv", + "nh", + "nj", + "nm", + "ny", + "nc", + "nd", "oh", + "ok", + "or", + "pa", + "ri", + "sc", + "sd", "tn", + "tx", + "ut", + "vt", "va", - "wa" + "vi", + "wa", + "wv", + "wi", + "wy" ] }, + "dateOfExpiration": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" + }, "compact": { "type": "string", "enum": [ @@ -2059,207 +2962,191 @@ "pattern": "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab]{1}[0-9a-f]{3}-[0-9a-f]{12}", "type": "string" }, - "givenName": { - "maxLength": 100, - "minLength": 1, - "type": "string" - }, - "familyName": { - "maxLength": 100, - "minLength": 1, - "type": "string" - }, - "licenseNumber": { - "maxLength": 100, - "minLength": 1, - "type": "string" - }, - "licenseScope": { - "type": "string", - "enum": [ - "single-state", - "multi-state" - ] - } - } - } - } - } - }, - "TestALicenhSWRXn61vgaB": { - "type": "object", - "additionalProperties": { - "type": "array", - "items": { - "type": "string", - "enum": [ - "al", - "az", - "co", - "ks", - "ky", - "md", - "oh", - "tn", - "va", - "wa" - ] - } - } - }, - "TestALicenYxu8vMXnF28M": { - "required": [ - "effectiveLiftDate" - ], - "type": "object", - "properties": { - "effectiveLiftDate": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "description": "The effective date when the encumbrance will be lifted", - "format": "date" - } - }, - "additionalProperties": false - }, - "TestALicenEuuJ2hePr1dE": { - "required": [ - "adverseActions", - "birthMonthDay", - "compact", - "dateOfExpiration", - "dateOfUpdate", - "familyName", - "givenName", - "licenseJurisdiction", - "licenses", - "privileges", - "providerId", - "type" - ], - "type": "object", - "properties": { - "privileges": { - "type": "array", - "items": { - "required": [ - "administratorSetStatus", - "compact", - "compactTransactionId", - "dateOfExpiration", - "history", - "jurisdiction", - "licenseJurisdiction", - "licenseType", - "providerId", - "status", - "type" - ], - "type": "object", - "properties": { - "investigationStatus": { - "type": "string", - "description": "Status indicating if the privilege is under investigation", - "enum": [ - "underInvestigation" - ] - }, - "licenseJurisdiction": { + "jurisdiction": { "type": "string", "enum": [ "al", + "ak", "az", + "ar", + "ca", "co", + "ct", + "de", + "dc", + "fl", + "ga", + "gu", + "hi", + "id", + "il", + "in", + "ia", "ks", "ky", + "la", + "me", "md", + "ma", + "mi", + "mn", + "ms", + "mo", + "mt", + "mp", + "ne", + "nv", + "nh", + "nj", + "nm", + "ny", + "nc", + "nd", "oh", + "ok", + "or", + "pa", + "ri", + "sc", + "sd", "tn", + "tx", + "ut", + "vt", "va", - "wa" - ] - }, - "compact": { - "type": "string", - "enum": [ - "socw" + "vi", + "wa", + "wv", + "wi", + "wy" ] }, - "jurisdiction": { + "type": { "type": "string", "enum": [ - "al", - "az", - "co", - "ks", - "ky", - "md", - "oh", - "tn", - "va", - "wa" + "privilege" ] }, - "investigations": { + "adverseActions": { "type": "array", "items": { "required": [ + "actionAgainst", + "adverseActionId", "compact", "creationDate", "dateOfUpdate", - "investigationId", + "effectiveStartDate", "jurisdiction", "licenseScope", "licenseType", + "licenseTypeAbbreviation", "providerId", - "submittingUser", "type" ], "type": "object", "properties": { - "licenseType": { - "type": "string" - }, - "investigationId": { - "type": "string" - }, "compact": { "type": "string", "enum": [ "socw" ] }, - "providerId": { - "pattern": "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab]{1}[0-9a-f]{3}-[0-9a-f]{12}", - "type": "string" - }, "jurisdiction": { "type": "string", "enum": [ "al", + "ak", "az", + "ar", + "ca", "co", + "ct", + "de", + "dc", + "fl", + "ga", + "gu", + "hi", + "id", + "il", + "in", + "ia", "ks", "ky", + "la", + "me", "md", + "ma", + "mi", + "mn", + "ms", + "mo", + "mt", + "mp", + "ne", + "nv", + "nh", + "nj", + "nm", + "ny", + "nc", + "nd", "oh", + "ok", + "or", + "pa", + "ri", + "sc", + "sd", "tn", + "tx", + "ut", + "vt", "va", - "wa" + "vi", + "wa", + "wv", + "wi", + "wy" ] }, - "submittingUser": { + "licenseTypeAbbreviation": { "type": "string" }, "type": { "type": "string", "enum": [ - "investigation" + "adverseAction" ] }, "creationDate": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", "type": "string", - "format": "date-time" + "format": "date" + }, + "actionAgainst": { + "type": "string" + }, + "licenseType": { + "type": "string" + }, + "providerId": { + "pattern": "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab]{1}[0-9a-f]{3}-[0-9a-f]{12}", + "type": "string" + }, + "effectiveStartDate": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" + }, + "adverseActionId": { + "type": "string" + }, + "effectiveLiftDate": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" }, "licenseScope": { "type": "string", @@ -2275,387 +3162,276 @@ } } }, - "history": { + "status": { + "type": "string", + "enum": [ + "active", + "inactive" + ] + } + } + } + }, + "licenses": { + "type": "array", + "description": "Sanitized home-state license rows (LicensePublicResponseSchema)", + "items": { + "required": [ + "compact", + "compactEligibility", + "dateOfExpiration", + "jurisdiction", + "licenseNumber", + "licenseScope", + "licenseStatus", + "licenseType", + "type" + ], + "type": "object", + "properties": { + "licenseType": { + "type": "string", + "enum": [ + "licensed clinical social worker", + "licensed master social worker", + "licensed bachelors social worker" + ] + }, + "dateOfExpiration": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" + }, + "compact": { + "type": "string", + "enum": [ + "socw" + ] + }, + "licenseStatus": { + "type": "string", + "enum": [ + "active", + "inactive" + ] + }, + "jurisdiction": { + "type": "string", + "enum": [ + "al", + "ak", + "az", + "ar", + "ca", + "co", + "ct", + "de", + "dc", + "fl", + "ga", + "gu", + "hi", + "id", + "il", + "in", + "ia", + "ks", + "ky", + "la", + "me", + "md", + "ma", + "mi", + "mn", + "ms", + "mo", + "mt", + "mp", + "ne", + "nv", + "nh", + "nj", + "nm", + "ny", + "nc", + "nd", + "oh", + "ok", + "or", + "pa", + "ri", + "sc", + "sd", + "tn", + "tx", + "ut", + "vt", + "va", + "vi", + "wa", + "wv", + "wi", + "wy" + ] + }, + "compactEligibility": { + "type": "string", + "enum": [ + "eligible", + "ineligible" + ] + }, + "licenseNumber": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "license" + ] + }, + "adverseActions": { "type": "array", "items": { "required": [ + "actionAgainst", + "adverseActionId", "compact", + "creationDate", "dateOfUpdate", + "effectiveStartDate", "jurisdiction", - "previous", - "type", - "updateType" + "licenseScope", + "licenseType", + "licenseTypeAbbreviation", + "providerId", + "type" ], "type": "object", "properties": { - "removedValues": { - "type": "array", - "description": "List of field names that were present in the previous record but removed in the update", - "items": { - "type": "string" - } - }, - "licenseType": { - "type": "string", - "enum": [ - "licensed clinical social worker", - "licensed master social worker", - "licensed bachelors social worker" - ] - }, - "compact": { + "compact": { "type": "string", "enum": [ "socw" ] }, - "previous": { - "required": [ - "administratorSetStatus", - "compactTransactionId", - "dateOfExpiration", - "licenseJurisdiction" - ], - "type": "object", - "properties": { - "administratorSetStatus": { - "type": "string", - "enum": [ - "active", - "inactive" - ] - }, - "dateOfExpiration": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" - }, - "licenseJurisdiction": { - "type": "string", - "enum": [ - "al", - "az", - "co", - "ks", - "ky", - "md", - "oh", - "tn", - "va", - "wa" - ] - }, - "compact": { - "type": "string", - "enum": [ - "socw" - ] - }, - "providerId": { - "pattern": "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab]{1}[0-9a-f]{3}-[0-9a-f]{12}", - "type": "string" - }, - "jurisdiction": { - "type": "string", - "enum": [ - "al", - "az", - "co", - "ks", - "ky", - "md", - "oh", - "tn", - "va", - "wa" - ] - }, - "type": { - "type": "string", - "enum": [ - "privilege" - ] - }, - "compactTransactionId": { - "type": "string" - }, - "status": { - "type": "string", - "enum": [ - "active", - "inactive" - ] - } - } - }, "jurisdiction": { "type": "string", "enum": [ "al", + "ak", "az", + "ar", + "ca", "co", + "ct", + "de", + "dc", + "fl", + "ga", + "gu", + "hi", + "id", + "il", + "in", + "ia", "ks", "ky", + "la", + "me", "md", + "ma", + "mi", + "mn", + "ms", + "mo", + "mt", + "mp", + "ne", + "nv", + "nh", + "nj", + "nm", + "ny", + "nc", + "nd", "oh", + "ok", + "or", + "pa", + "ri", + "sc", + "sd", "tn", + "tx", + "ut", + "vt", "va", - "wa" + "vi", + "wa", + "wv", + "wi", + "wy" ] }, - "updatedValues": { - "type": "object", - "properties": { - "administratorSetStatus": { - "type": "string", - "enum": [ - "active", - "inactive" - ] - }, - "dateOfExpiration": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" - }, - "licenseJurisdiction": { - "type": "string", - "enum": [ - "al", - "az", - "co", - "ks", - "ky", - "md", - "oh", - "tn", - "va", - "wa" - ] - }, - "compact": { - "type": "string", - "enum": [ - "socw" - ] - }, - "providerId": { - "pattern": "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab]{1}[0-9a-f]{3}-[0-9a-f]{12}", - "type": "string" - }, - "jurisdiction": { - "type": "string", - "enum": [ - "al", - "az", - "co", - "ks", - "ky", - "md", - "oh", - "tn", - "va", - "wa" - ] - }, - "type": { - "type": "string", - "enum": [ - "privilege" - ] - }, - "compactTransactionId": { - "type": "string" - }, - "status": { - "type": "string", - "enum": [ - "active", - "inactive" - ] - } - } + "licenseTypeAbbreviation": { + "type": "string" }, "type": { "type": "string", "enum": [ - "privilegeUpdate" + "adverseAction" ] }, - "dateOfUpdate": { + "creationDate": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", "type": "string", - "format": "date-time" + "format": "date" }, - "updateType": { + "actionAgainst": { + "type": "string" + }, + "licenseType": { + "type": "string" + }, + "providerId": { + "pattern": "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab]{1}[0-9a-f]{3}-[0-9a-f]{12}", + "type": "string" + }, + "effectiveStartDate": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" + }, + "adverseActionId": { + "type": "string" + }, + "effectiveLiftDate": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" + }, + "licenseScope": { "type": "string", "enum": [ - "deactivation", - "expiration", - "issuance", - "other", - "renewal", - "encumbrance", - "lifting_encumbrance", - "licenseDeactivation" + "single-state", + "multi-state" ] + }, + "dateOfUpdate": { + "type": "string", + "format": "date-time" } } } }, - "type": { - "type": "string", - "enum": [ - "privilege" - ] - }, - "compactTransactionId": { - "type": "string" - }, - "licenseType": { - "type": "string", - "enum": [ - "licensed clinical social worker", - "licensed master social worker", - "licensed bachelors social worker" - ] - }, - "administratorSetStatus": { + "licenseScope": { "type": "string", "enum": [ - "active", - "inactive" - ] - }, - "dateOfExpiration": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" - }, - "providerId": { - "pattern": "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab]{1}[0-9a-f]{3}-[0-9a-f]{12}", - "type": "string" - }, - "adverseActions": { - "type": "array", - "items": { - "required": [ - "actionAgainst", - "adverseActionId", - "compact", - "creationDate", - "dateOfUpdate", - "effectiveStartDate", - "encumbranceType", - "jurisdiction", - "licenseScope", - "licenseType", - "licenseTypeAbbreviation", - "providerId", - "type" - ], - "type": "object", - "properties": { - "clinicalPrivilegeActionCategories": { - "type": "array", - "description": "The categories of clinical privilege action", - "items": { - "type": "string", - "enum": [ - "Non-Compliance With Requirements", - "Conflict of Interest", - "Substandard Care or Patient Neglect/Abuse", - "Criminal Conviction or Adjudication", - "Confidentiality, Consent or Disclosure Violations", - "Fraud, Deception, or Misrepresentation", - "Improper Supervision or Allowing Unlicensed Practice", - "Improper Prescribing, Dispensing, Administering Medication/Drug Violation", - "Other" - ] - } - }, - "compact": { - "type": "string", - "enum": [ - "socw" - ] - }, - "jurisdiction": { - "type": "string", - "enum": [ - "al", - "az", - "co", - "ks", - "ky", - "md", - "oh", - "tn", - "va", - "wa" - ] - }, - "licenseTypeAbbreviation": { - "type": "string" - }, - "type": { - "type": "string", - "enum": [ - "adverseAction" - ] - }, - "creationDate": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" - }, - "actionAgainst": { - "type": "string" - }, - "licenseType": { - "type": "string" - }, - "providerId": { - "pattern": "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab]{1}[0-9a-f]{3}-[0-9a-f]{12}", - "type": "string" - }, - "effectiveStartDate": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" - }, - "adverseActionId": { - "type": "string" - }, - "effectiveLiftDate": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" - }, - "encumbranceType": { - "type": "string" - }, - "liftingUser": { - "type": "string" - }, - "licenseScope": { - "type": "string", - "enum": [ - "single-state", - "multi-state" - ] - }, - "dateOfUpdate": { - "type": "string", - "format": "date-time" - } - } - } - }, - "status": { - "type": "string", - "enum": [ - "active", - "inactive" + "single-state", + "multi-state" ] } } @@ -2665,15 +3441,59 @@ "type": "string", "enum": [ "al", + "ak", "az", + "ar", + "ca", "co", + "ct", + "de", + "dc", + "fl", + "ga", + "gu", + "hi", + "id", + "il", + "in", + "ia", "ks", "ky", + "la", + "me", "md", + "ma", + "mi", + "mn", + "ms", + "mo", + "mt", + "mp", + "ne", + "nv", + "nh", + "nj", + "nm", + "ny", + "nc", + "nd", "oh", + "ok", + "or", + "pa", + "ri", + "sc", + "sd", "tn", + "tx", + "ut", + "vt", "va", - "wa" + "vi", + "wa", + "wv", + "wi", + "wy" ] }, "compact": { @@ -2682,36 +3502,24 @@ "socw" ] }, + "providerId": { + "pattern": "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab]{1}[0-9a-f]{3}-[0-9a-f]{12}", + "type": "string" + }, "givenName": { "maxLength": 100, "minLength": 1, "type": "string" }, - "compactEligibility": { - "type": "string", - "enum": [ - "eligible", - "ineligible" - ] - }, - "jurisdictionUploadedCompactEligibility": { - "type": "string", - "enum": [ - "eligible", - "ineligible" - ] - }, - "dateOfBirth": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" + "familyName": { + "maxLength": 100, + "minLength": 1, + "type": "string" }, - "jurisdictionUploadedLicenseStatus": { - "type": "string", - "enum": [ - "active", - "inactive" - ] + "middleName": { + "maxLength": 100, + "minLength": 1, + "type": "string" }, "type": { "type": "string", @@ -2724,198 +3532,423 @@ "minLength": 1, "type": "string" }, - "licenses": { - "type": "array", - "items": { - "required": [ - "compact", - "compactEligibility", - "dateOfExpiration", - "dateOfIssuance", - "dateOfRenewal", - "dateOfUpdate", - "familyName", - "givenName", - "history", - "homeAddressCity", - "homeAddressPostalCode", - "homeAddressState", - "homeAddressStreet1", - "jurisdiction", - "jurisdictionUploadedCompactEligibility", - "jurisdictionUploadedLicenseStatus", - "licenseScope", - "licenseStatus", - "licenseType", - "middleName", - "providerId", - "type" - ], - "type": "object", - "properties": { - "compact": { - "type": "string", - "enum": [ - "socw" - ] - }, - "homeAddressStreet2": { - "maxLength": 100, - "minLength": 1, - "type": "string" - }, - "jurisdiction": { - "type": "string", - "enum": [ - "al", - "az", - "co", - "ks", - "ky", - "md", - "oh", - "tn", - "va", - "wa" - ] - }, - "homeAddressStreet1": { - "maxLength": 100, - "minLength": 2, - "type": "string" - }, - "investigations": { - "type": "array", - "items": { - "required": [ - "compact", - "creationDate", - "dateOfUpdate", - "investigationId", - "jurisdiction", - "licenseScope", - "licenseType", - "providerId", - "submittingUser", - "type" - ], - "type": "object", - "properties": { - "licenseType": { - "type": "string" - }, - "investigationId": { - "type": "string" - }, - "compact": { - "type": "string", - "enum": [ - "socw" - ] - }, - "providerId": { - "pattern": "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab]{1}[0-9a-f]{3}-[0-9a-f]{12}", - "type": "string" - }, - "jurisdiction": { - "type": "string", - "enum": [ - "al", - "az", - "co", - "ks", - "ky", - "md", - "oh", - "tn", - "va", - "wa" - ] - }, - "submittingUser": { - "type": "string" - }, - "type": { - "type": "string", - "enum": [ - "investigation" - ] - }, - "creationDate": { - "type": "string", - "format": "date-time" - }, - "licenseScope": { - "type": "string", - "enum": [ - "single-state", - "multi-state" - ] - }, - "dateOfUpdate": { - "type": "string", - "format": "date-time" - } - } - } + "publicCompactIdentifier": { + "pattern": "^[Ss][Ww][Cc]-[0-9]{4}-[1-9][0-9]*$", + "type": "string" + }, + "dateOfUpdate": { + "type": "string", + "format": "date-time" + } + } + }, + "TestALicenqy3W03pETQIx": { + "required": [ + "query" + ], + "type": "object", + "properties": { + "pagination": { + "type": "object", + "properties": { + "lastKey": { + "maxLength": 1024, + "minLength": 1, + "type": "string" + }, + "pageSize": { + "maximum": 100, + "minimum": 5, + "type": "integer" + } + }, + "additionalProperties": false + }, + "query": { + "type": "object", + "properties": { + "licenseType": { + "type": "string", + "description": "Filter for licenses of a specific license type", + "enum": [ + "licensed clinical social worker", + "licensed master social worker", + "licensed bachelors social worker" + ] + }, + "cuid": { + "maxLength": 64, + "pattern": "^[Ss][Ww][Cc]-[0-9]{4}-[1-9][0-9]*$", + "type": "string", + "description": "Filter for the provider with a specific Compact Unique Identifier" + }, + "jurisdiction": { + "type": "string", + "description": "Filter for providers with license in a jurisdiction", + "enum": [ + "al", + "ak", + "az", + "ar", + "ca", + "co", + "ct", + "de", + "dc", + "fl", + "ga", + "gu", + "hi", + "id", + "il", + "in", + "ia", + "ks", + "ky", + "la", + "me", + "md", + "ma", + "mi", + "mn", + "ms", + "mo", + "mt", + "mp", + "ne", + "nv", + "nh", + "nj", + "nm", + "ny", + "nc", + "nd", + "oh", + "ok", + "or", + "pa", + "ri", + "sc", + "sd", + "tn", + "tx", + "ut", + "vt", + "va", + "vi", + "wa", + "wv", + "wi", + "wy" + ] + }, + "givenName": { + "maxLength": 100, + "type": "string", + "description": "Filter for providers with a given name (familyName is required if givenName is provided)" + }, + "familyName": { + "maxLength": 100, + "type": "string", + "description": "Filter for providers with a family name" + }, + "licenseNumber": { + "maxLength": 100, + "minLength": 1, + "type": "string", + "description": "Filter for licenses with a specific license number" + } + }, + "additionalProperties": false, + "description": "The query parameters" + }, + "sorting": { + "required": [ + "key" + ], + "type": "object", + "properties": { + "key": { + "type": "string", + "description": "The key to sort results by", + "enum": [ + "dateOfUpdate", + "familyName" + ] + }, + "direction": { + "type": "string", + "description": "Direction to sort results by", + "enum": [ + "ascending", + "descending" + ] + } + }, + "description": "How to sort results" + } + }, + "additionalProperties": false + }, + "TestALicenFHq7OPw0O1oA": { + "required": [ + "pagination", + "providers" + ], + "type": "object", + "properties": { + "pagination": { + "type": "object", + "properties": { + "prevLastKey": { + "maxLength": 1024, + "minLength": 1, + "type": "object" + }, + "lastKey": { + "maxLength": 1024, + "minLength": 1, + "type": "object" + }, + "pageSize": { + "maximum": 100, + "minimum": 5, + "type": "integer" + } + } + }, + "query": { + "type": "object", + "properties": { + "licenseType": { + "type": "string", + "description": "Filter for licenses of a specific license type", + "enum": [ + "licensed clinical social worker", + "licensed master social worker", + "licensed bachelors social worker" + ] + }, + "cuid": { + "maxLength": 64, + "pattern": "^[Ss][Ww][Cc]-[0-9]{4}-[1-9][0-9]*$", + "type": "string", + "description": "Filter for the provider with a specific Compact Unique Identifier" + }, + "providerId": { + "pattern": "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab]{1}[0-9a-f]{3}-[0-9a-f]{12}", + "type": "string", + "description": "Internal UUID for the provider" + }, + "jurisdiction": { + "type": "string", + "description": "Filter for providers with privilege/license in a jurisdiction", + "enum": [ + "al", + "ak", + "az", + "ar", + "ca", + "co", + "ct", + "de", + "dc", + "fl", + "ga", + "gu", + "hi", + "id", + "il", + "in", + "ia", + "ks", + "ky", + "la", + "me", + "md", + "ma", + "mi", + "mn", + "ms", + "mo", + "mt", + "mp", + "ne", + "nv", + "nh", + "nj", + "nm", + "ny", + "nc", + "nd", + "oh", + "ok", + "or", + "pa", + "ri", + "sc", + "sd", + "tn", + "tx", + "ut", + "vt", + "va", + "vi", + "wa", + "wv", + "wi", + "wy" + ] + }, + "givenName": { + "maxLength": 100, + "type": "string", + "description": "Filter for providers with a given name" + }, + "familyName": { + "maxLength": 100, + "type": "string", + "description": "Filter for providers with a family name" + }, + "licenseNumber": { + "maxLength": 100, + "minLength": 1, + "type": "string", + "description": "Filter for licenses with a specific license number" + } + } + }, + "sorting": { + "required": [ + "key" + ], + "type": "object", + "properties": { + "key": { + "type": "string", + "description": "The key to sort results by", + "enum": [ + "dateOfUpdate", + "familyName" + ] + }, + "direction": { + "type": "string", + "description": "Direction to sort results by", + "enum": [ + "ascending", + "descending" + ] + } + }, + "description": "How to sort results" + }, + "providers": { + "maxLength": 100, + "type": "array", + "items": { + "required": [ + "compact", + "familyName", + "givenName", + "licenseEligibility", + "licenseJurisdiction", + "licenseNumber", + "licenseScope", + "licenseType", + "providerId" + ], + "type": "object", + "properties": { + "licenseType": { + "type": "string", + "description": "License type or profession designation for this license row" }, - "type": { + "licenseEligibility": { "type": "string", + "description": "Whether the license is eligible for compact participation in public search results", "enum": [ - "license-home" + "eligible", + "ineligible" ] }, - "suffix": { - "maxLength": 100, - "minLength": 1, - "type": "string" - }, - "dateOfIssuance": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" - }, - "licenseType": { + "licenseJurisdiction": { "type": "string", "enum": [ - "licensed clinical social worker", - "licensed master social worker", - "licensed bachelors social worker" - ] - }, - "emailAddress": { - "maxLength": 100, - "minLength": 5, - "type": "string", - "format": "email" + "al", + "ak", + "az", + "ar", + "ca", + "co", + "ct", + "de", + "dc", + "fl", + "ga", + "gu", + "hi", + "id", + "il", + "in", + "ia", + "ks", + "ky", + "la", + "me", + "md", + "ma", + "mi", + "mn", + "ms", + "mo", + "mt", + "mp", + "ne", + "nv", + "nh", + "nj", + "nm", + "ny", + "nc", + "nd", + "oh", + "ok", + "or", + "pa", + "ri", + "sc", + "sd", + "tn", + "tx", + "ut", + "vt", + "va", + "vi", + "wa", + "wv", + "wi", + "wy" + ] }, - "dateOfExpiration": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "compact": { "type": "string", - "format": "date" - }, - "homeAddressState": { - "maxLength": 100, - "minLength": 2, - "type": "string" + "enum": [ + "socw" + ] }, "providerId": { "pattern": "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab]{1}[0-9a-f]{3}-[0-9a-f]{12}", "type": "string" }, - "dateOfRenewal": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" - }, - "familyName": { + "givenName": { "maxLength": 100, "minLength": 1, "type": "string" }, - "homeAddressCity": { + "familyName": { "maxLength": 100, - "minLength": 2, + "minLength": 1, "type": "string" }, "licenseNumber": { @@ -2923,23 +3956,158 @@ "minLength": 1, "type": "string" }, - "investigationStatus": { + "publicCompactIdentifier": { + "pattern": "^[Ss][Ww][Cc]-[0-9]{4}-[1-9][0-9]*$", + "type": "string" + }, + "licenseScope": { "type": "string", - "description": "Status indicating if the license is under investigation", "enum": [ - "underInvestigation" + "single-state", + "multi-state" + ] + } + } + } + } + } + }, + "TestALicenFZgseK5NUIOL": { + "required": [ + "pagination", + "providers" + ], + "type": "object", + "properties": { + "pagination": { + "type": "object", + "properties": { + "prevLastKey": { + "maxLength": 1024, + "minLength": 1, + "type": "object" + }, + "lastKey": { + "maxLength": 1024, + "minLength": 1, + "type": "object" + }, + "pageSize": { + "maximum": 100, + "minimum": 5, + "type": "integer" + } + } + }, + "sorting": { + "required": [ + "key" + ], + "type": "object", + "properties": { + "key": { + "type": "string", + "description": "The key to sort results by", + "enum": [ + "dateOfUpdate", + "familyName" + ] + }, + "direction": { + "type": "string", + "description": "Direction to sort results by", + "enum": [ + "ascending", + "descending" + ] + } + }, + "description": "How to sort results" + }, + "providers": { + "maxLength": 100, + "type": "array", + "items": { + "required": [ + "birthMonthDay", + "compact", + "compactEligibility", + "dateOfExpiration", + "dateOfUpdate", + "familyName", + "givenName", + "jurisdictionUploadedCompactEligibility", + "jurisdictionUploadedLicenseStatus", + "licenseJurisdiction", + "licenseStatus", + "providerId", + "type" + ], + "type": "object", + "properties": { + "licenseJurisdiction": { + "type": "string", + "enum": [ + "al", + "ak", + "az", + "ar", + "ca", + "co", + "ct", + "de", + "dc", + "fl", + "ga", + "gu", + "hi", + "id", + "il", + "in", + "ia", + "ks", + "ky", + "la", + "me", + "md", + "ma", + "mi", + "mn", + "ms", + "mo", + "mt", + "mp", + "ne", + "nv", + "nh", + "nj", + "nm", + "ny", + "nc", + "nd", + "oh", + "ok", + "or", + "pa", + "ri", + "sc", + "sd", + "tn", + "tx", + "ut", + "vt", + "va", + "vi", + "wa", + "wv", + "wi", + "wy" ] }, - "homeAddressPostalCode": { - "maxLength": 7, - "minLength": 5, - "type": "string" - }, - "compactEligibility": { + "compact": { "type": "string", "enum": [ - "eligible", - "ineligible" + "socw" ] }, "givenName": { @@ -2947,6 +4115,13 @@ "minLength": 1, "type": "string" }, + "compactEligibility": { + "type": "string", + "enum": [ + "eligible", + "ineligible" + ] + }, "jurisdictionUploadedCompactEligibility": { "type": "string", "enum": [ @@ -2966,351 +4141,32 @@ "inactive" ] }, - "history": { - "type": "array", - "items": { - "required": [ - "compact", - "dateOfUpdate", - "jurisdiction", - "previous", - "type", - "updateType" - ], - "type": "object", - "properties": { - "removedValues": { - "type": "array", - "description": "List of field names that were present in the previous record but removed in the update", - "items": { - "type": "string" - } - }, - "licenseType": { - "type": "string", - "enum": [ - "licensed clinical social worker", - "licensed master social worker", - "licensed bachelors social worker" - ] - }, - "compact": { - "type": "string", - "enum": [ - "socw" - ] - }, - "previous": { - "required": [ - "dateOfExpiration", - "dateOfIssuance", - "dateOfRenewal", - "familyName", - "givenName", - "homeAddressCity", - "homeAddressPostalCode", - "homeAddressState", - "homeAddressStreet1", - "jurisdictionUploadedCompactEligibility", - "jurisdictionUploadedLicenseStatus", - "middleName" - ], - "type": "object", - "properties": { - "homeAddressStreet2": { - "maxLength": 100, - "minLength": 1, - "type": "string" - }, - "homeAddressPostalCode": { - "maxLength": 7, - "minLength": 5, - "type": "string" - }, - "givenName": { - "maxLength": 100, - "minLength": 1, - "type": "string" - }, - "homeAddressStreet1": { - "maxLength": 100, - "minLength": 2, - "type": "string" - }, - "compactEligibility": { - "type": "string", - "enum": [ - "eligible", - "ineligible" - ] - }, - "jurisdictionUploadedCompactEligibility": { - "type": "string", - "enum": [ - "eligible", - "ineligible" - ] - }, - "dateOfBirth": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" - }, - "jurisdictionUploadedLicenseStatus": { - "type": "string", - "enum": [ - "active", - "inactive" - ] - }, - "suffix": { - "maxLength": 100, - "minLength": 1, - "type": "string" - }, - "dateOfIssuance": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" - }, - "emailAddress": { - "maxLength": 100, - "minLength": 5, - "type": "string", - "format": "email" - }, - "dateOfExpiration": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" - }, - "phoneNumber": { - "pattern": "^\\+[0-9]{8,15}$", - "type": "string" - }, - "homeAddressState": { - "maxLength": 100, - "minLength": 2, - "type": "string" - }, - "dateOfRenewal": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" - }, - "licenseStatus": { - "type": "string", - "enum": [ - "active", - "inactive" - ] - }, - "familyName": { - "maxLength": 100, - "minLength": 1, - "type": "string" - }, - "homeAddressCity": { - "maxLength": 100, - "minLength": 2, - "type": "string" - }, - "licenseNumber": { - "maxLength": 100, - "minLength": 1, - "type": "string" - }, - "middleName": { - "maxLength": 100, - "minLength": 1, - "type": "string" - }, - "licenseStatusName": { - "maxLength": 100, - "minLength": 1, - "type": "string" - }, - "licenseScope": { - "type": "string", - "enum": [ - "single-state", - "multi-state" - ] - } - } - }, - "jurisdiction": { - "type": "string", - "enum": [ - "al", - "az", - "co", - "ks", - "ky", - "md", - "oh", - "tn", - "va", - "wa" - ] - }, - "updatedValues": { - "type": "object", - "properties": { - "homeAddressStreet2": { - "maxLength": 100, - "minLength": 1, - "type": "string" - }, - "homeAddressPostalCode": { - "maxLength": 7, - "minLength": 5, - "type": "string" - }, - "givenName": { - "maxLength": 100, - "minLength": 1, - "type": "string" - }, - "homeAddressStreet1": { - "maxLength": 100, - "minLength": 2, - "type": "string" - }, - "compactEligibility": { - "type": "string", - "enum": [ - "eligible", - "ineligible" - ] - }, - "jurisdictionUploadedCompactEligibility": { - "type": "string", - "enum": [ - "eligible", - "ineligible" - ] - }, - "dateOfBirth": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" - }, - "jurisdictionUploadedLicenseStatus": { - "type": "string", - "enum": [ - "active", - "inactive" - ] - }, - "suffix": { - "maxLength": 100, - "minLength": 1, - "type": "string" - }, - "dateOfIssuance": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" - }, - "emailAddress": { - "maxLength": 100, - "minLength": 5, - "type": "string", - "format": "email" - }, - "dateOfExpiration": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" - }, - "phoneNumber": { - "pattern": "^\\+[0-9]{8,15}$", - "type": "string" - }, - "homeAddressState": { - "maxLength": 100, - "minLength": 2, - "type": "string" - }, - "dateOfRenewal": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" - }, - "licenseStatus": { - "type": "string", - "enum": [ - "active", - "inactive" - ] - }, - "familyName": { - "maxLength": 100, - "minLength": 1, - "type": "string" - }, - "homeAddressCity": { - "maxLength": 100, - "minLength": 2, - "type": "string" - }, - "licenseNumber": { - "maxLength": 100, - "minLength": 1, - "type": "string" - }, - "middleName": { - "maxLength": 100, - "minLength": 1, - "type": "string" - }, - "licenseStatusName": { - "maxLength": 100, - "minLength": 1, - "type": "string" - }, - "licenseScope": { - "type": "string", - "enum": [ - "single-state", - "multi-state" - ] - } - } - }, - "type": { - "type": "string", - "enum": [ - "licenseUpdate" - ] - }, - "dateOfUpdate": { - "type": "string", - "format": "date-time" - }, - "updateType": { - "type": "string", - "enum": [ - "deactivation", - "expiration", - "issuance", - "other", - "renewal", - "encumbrance", - "lifting_encumbrance", - "licenseDeactivation" - ] - } - } - } + "type": { + "type": "string", + "enum": [ + "provider" + ] + }, + "suffix": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "publicCompactIdentifier": { + "pattern": "^[Ss][Ww][Cc]-[0-9]{4}-[1-9][0-9]*$", + "type": "string" }, "ssnLastFour": { "pattern": "^[0-9]{4}$", "type": "string" }, - "phoneNumber": { - "pattern": "^\\+[0-9]{8,15}$", + "dateOfExpiration": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" + }, + "providerId": { + "pattern": "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab]{1}[0-9a-f]{3}-[0-9a-f]{12}", "type": "string" }, "licenseStatus": { @@ -3320,453 +4176,688 @@ "inactive" ] }, - "middleName": { + "familyName": { "maxLength": 100, "minLength": 1, "type": "string" }, - "licenseStatusName": { + "middleName": { "maxLength": 100, "minLength": 1, "type": "string" }, - "adverseActions": { - "type": "array", - "items": { - "required": [ - "actionAgainst", - "adverseActionId", - "compact", - "creationDate", - "dateOfUpdate", - "effectiveStartDate", - "encumbranceType", - "jurisdiction", - "licenseScope", - "licenseType", - "licenseTypeAbbreviation", - "providerId", - "type" - ], - "type": "object", - "properties": { - "clinicalPrivilegeActionCategories": { - "type": "array", - "description": "The categories of clinical privilege action", - "items": { - "type": "string", - "enum": [ - "Non-Compliance With Requirements", - "Conflict of Interest", - "Substandard Care or Patient Neglect/Abuse", - "Criminal Conviction or Adjudication", - "Confidentiality, Consent or Disclosure Violations", - "Fraud, Deception, or Misrepresentation", - "Improper Supervision or Allowing Unlicensed Practice", - "Improper Prescribing, Dispensing, Administering Medication/Drug Violation", - "Other" - ] - } - }, - "compact": { - "type": "string", - "enum": [ - "socw" - ] - }, - "jurisdiction": { - "type": "string", - "enum": [ - "al", - "az", - "co", - "ks", - "ky", - "md", - "oh", - "tn", - "va", - "wa" - ] - }, - "licenseTypeAbbreviation": { - "type": "string" - }, - "type": { - "type": "string", - "enum": [ - "adverseAction" - ] - }, - "creationDate": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" - }, - "actionAgainst": { - "type": "string" - }, - "licenseType": { - "type": "string" - }, - "providerId": { - "pattern": "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab]{1}[0-9a-f]{3}-[0-9a-f]{12}", - "type": "string" - }, - "effectiveStartDate": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" - }, - "adverseActionId": { - "type": "string" - }, - "effectiveLiftDate": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" - }, - "encumbranceType": { - "type": "string" - }, - "liftingUser": { - "type": "string" - }, - "licenseScope": { - "type": "string", - "enum": [ - "single-state", - "multi-state" - ] - }, - "dateOfUpdate": { - "type": "string", - "format": "date-time" - } - } - } + "birthMonthDay": { + "pattern": "^[01]{1}[0-9]{1}-[0-3]{1}[0-9]{1}$", + "type": "string", + "format": "date" }, "dateOfUpdate": { "type": "string", "format": "date-time" - }, - "licenseScope": { - "type": "string", - "enum": [ - "single-state", - "multi-state" - ] } } } - }, - "ssnLastFour": { - "pattern": "^[0-9]{4}$", - "type": "string" - }, - "dateOfExpiration": { + } + } + }, + "TestALicenQPmgNWK0HpLx": { + "required": [ + "effectiveLiftDate", + "licenseScope" + ], + "type": "object", + "properties": { + "effectiveLiftDate": { "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", "type": "string", + "description": "The effective date when the encumbrance will be lifted", "format": "date" }, - "providerId": { - "pattern": "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab]{1}[0-9a-f]{3}-[0-9a-f]{12}", - "type": "string" - }, - "licenseStatus": { + "licenseScope": { "type": "string", "enum": [ - "active", - "inactive" + "single-state", + "multi-state" ] - }, - "familyName": { - "maxLength": 100, - "minLength": 1, - "type": "string" - }, - "middleName": { - "maxLength": 100, - "minLength": 1, - "type": "string" - }, - "birthMonthDay": { - "pattern": "^[01]{1}[0-9]{1}-[0-3]{1}[0-9]{1}$", - "type": "string", - "format": "date" - }, - "adverseActions": { + } + }, + "additionalProperties": false + }, + "TestALicenEZKeRGLs4K99": { + "required": [ + "compactAdverseActionsNotificationEmails", + "compactOperationsTeamEmails", + "configuredStates", + "licenseeRegistrationEnabled" + ], + "type": "object", + "properties": { + "configuredStates": { "type": "array", + "description": "List of states that have submitted configurations and their live status", "items": { "required": [ - "actionAgainst", - "adverseActionId", - "compact", - "creationDate", - "dateOfUpdate", - "effectiveStartDate", - "encumbranceType", - "jurisdiction", - "licenseScope", - "licenseType", - "licenseTypeAbbreviation", - "providerId", - "type" + "isLive", + "postalAbbreviation" ], "type": "object", "properties": { - "clinicalPrivilegeActionCategories": { - "type": "array", - "description": "The categories of clinical privilege action", - "items": { - "type": "string", - "enum": [ - "Non-Compliance With Requirements", - "Conflict of Interest", - "Substandard Care or Patient Neglect/Abuse", - "Criminal Conviction or Adjudication", - "Confidentiality, Consent or Disclosure Violations", - "Fraud, Deception, or Misrepresentation", - "Improper Supervision or Allowing Unlicensed Practice", - "Improper Prescribing, Dispensing, Administering Medication/Drug Violation", - "Other" - ] - } - }, - "compact": { - "type": "string", - "enum": [ - "socw" - ] - }, - "jurisdiction": { + "postalAbbreviation": { "type": "string", + "description": "The postal abbreviation of the jurisdiction", "enum": [ "al", + "ak", "az", + "ar", + "ca", "co", + "ct", + "de", + "dc", + "fl", + "ga", + "gu", + "hi", + "id", + "il", + "in", + "ia", "ks", "ky", + "la", + "me", "md", + "ma", + "mi", + "mn", + "ms", + "mo", + "mt", + "mp", + "ne", + "nv", + "nh", + "nj", + "nm", + "ny", + "nc", + "nd", "oh", + "ok", + "or", + "pa", + "ri", + "sc", + "sd", "tn", + "tx", + "ut", + "vt", "va", - "wa" - ] - }, - "licenseTypeAbbreviation": { - "type": "string" - }, - "type": { - "type": "string", - "enum": [ - "adverseAction" + "vi", + "wa", + "wv", + "wi", + "wy" ] }, - "creationDate": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" - }, - "actionAgainst": { - "type": "string" - }, - "licenseType": { - "type": "string" - }, - "providerId": { - "pattern": "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab]{1}[0-9a-f]{3}-[0-9a-f]{12}", - "type": "string" - }, - "effectiveStartDate": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" - }, - "adverseActionId": { - "type": "string" - }, - "effectiveLiftDate": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" - }, - "encumbranceType": { - "type": "string" - }, - "liftingUser": { - "type": "string" - }, - "licenseScope": { + "isLive": { + "type": "boolean", + "description": "Whether the state is live and available for registrations." + } + }, + "additionalProperties": false + } + }, + "compactAdverseActionsNotificationEmails": { + "maxItems": 10, + "minItems": 1, + "uniqueItems": true, + "type": "array", + "description": "List of email addresses for adverse actions notifications", + "items": { + "type": "string", + "format": "email" + } + }, + "licenseeRegistrationEnabled": { + "type": "boolean", + "description": "Denotes whether licensee registration is enabled" + }, + "compactOperationsTeamEmails": { + "maxItems": 10, + "minItems": 1, + "uniqueItems": true, + "type": "array", + "description": "List of email addresses for operations team notifications", + "items": { + "type": "string", + "format": "email" + } + } + }, + "additionalProperties": false + }, + "TestALicenfa1lTrBaH6fm": { + "required": [ + "compactAbbr", + "compactAdverseActionsNotificationEmails", + "compactName", + "compactOperationsTeamEmails", + "configuredStates", + "licenseeRegistrationEnabled" + ], + "type": "object", + "properties": { + "configuredStates": { + "type": "array", + "description": "List of states that have submitted configurations and their live status", + "items": { + "required": [ + "isLive", + "postalAbbreviation" + ], + "type": "object", + "properties": { + "postalAbbreviation": { "type": "string", + "description": "The postal abbreviation of the jurisdiction", "enum": [ - "single-state", - "multi-state" + "al", + "ak", + "az", + "ar", + "ca", + "co", + "ct", + "de", + "dc", + "fl", + "ga", + "gu", + "hi", + "id", + "il", + "in", + "ia", + "ks", + "ky", + "la", + "me", + "md", + "ma", + "mi", + "mn", + "ms", + "mo", + "mt", + "mp", + "ne", + "nv", + "nh", + "nj", + "nm", + "ny", + "nc", + "nd", + "oh", + "ok", + "or", + "pa", + "ri", + "sc", + "sd", + "tn", + "tx", + "ut", + "vt", + "va", + "vi", + "wa", + "wv", + "wi", + "wy" ] }, - "dateOfUpdate": { - "type": "string", - "format": "date-time" + "isLive": { + "type": "boolean", + "description": "Whether the state is live and available for registrations." } } } }, - "dateOfUpdate": { + "compactAdverseActionsNotificationEmails": { + "type": "array", + "description": "List of email addresses for adverse actions notifications", + "items": { + "type": "string", + "format": "email" + } + }, + "licenseeRegistrationEnabled": { + "type": "boolean", + "description": "Denotes whether licensee registration is enabled" + }, + "compactAbbr": { "type": "string", - "format": "date-time" + "description": "The abbreviation of the compact" + }, + "compactName": { + "type": "string", + "description": "The full name of the compact" + }, + "compactOperationsTeamEmails": { + "type": "array", + "description": "List of email addresses for operations team notifications", + "items": { + "type": "string", + "format": "email" + } } } }, - "TestALicenuf1t1AOWtbEj": { + "TestALicen02H1rt7iqxOE": { + "required": [ + "jurisdictionAdverseActionsNotificationEmails", + "jurisdictionOperationsTeamEmails", + "licenseeRegistrationEnabled" + ], "type": "object", "properties": { - "context": { - "type": "object", - "properties": { - "userId": { - "maxLength": 100, - "minLength": 1, - "type": "string", - "description": "Optional user ID for feature flag evaluation" - }, - "customAttributes": { - "type": "object", - "additionalProperties": { - "type": "string" - }, - "description": "Optional custom attributes for feature flag evaluation" - } - }, - "additionalProperties": false, - "description": "Optional context for feature flag evaluation" + "jurisdictionAdverseActionsNotificationEmails": { + "maxItems": 10, + "minItems": 1, + "uniqueItems": true, + "type": "array", + "description": "List of email addresses for adverse actions notifications", + "items": { + "type": "string", + "format": "email" + } + }, + "jurisdictionOperationsTeamEmails": { + "maxItems": 10, + "minItems": 1, + "uniqueItems": true, + "type": "array", + "description": "List of email addresses for operations team notifications", + "items": { + "type": "string", + "format": "email" + } + }, + "licenseeRegistrationEnabled": { + "type": "boolean", + "description": "Denotes whether licensee registration is enabled" } }, "additionalProperties": false }, - "TestALicenTcO3YVaPklzG": { + "TestALicendpUtSr8g3DI4": { "required": [ - "enabled" + "clinicalPrivilegeActionCategories", + "encumbranceEffectiveDate", + "encumbranceType", + "licenseScope" ], "type": "object", "properties": { - "enabled": { - "type": "boolean", - "description": "Whether the feature flag is enabled" + "encumbranceEffectiveDate": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "description": "The effective date of the encumbrance", + "format": "date" + }, + "clinicalPrivilegeActionCategories": { + "minItems": 1, + "type": "array", + "description": "The categories of clinical privilege action", + "items": { + "type": "string", + "enum": [ + "Non-Compliance With Requirements", + "Conflict of Interest", + "Substandard Care or Patient Neglect/Abuse", + "Criminal Conviction or Adjudication", + "Confidentiality, Consent or Disclosure Violations", + "Fraud, Deception, or Misrepresentation", + "Improper Supervision or Allowing Unlicensed Practice", + "Improper Prescribing, Dispensing, Administering Medication/Drug Violation", + "Other" + ] + } + }, + "encumbranceType": { + "type": "string", + "description": "The type of encumbrance", + "enum": [ + "fine", + "reprimand", + "required supervision", + "completion of continuing education", + "public reprimand", + "probation", + "injunctive action", + "suspension", + "revocation", + "denial", + "surrender of license", + "modification of previous action-extension", + "modification of previous action-reduction", + "other monitoring", + "other adjudicated action not listed" + ] + }, + "licenseScope": { + "type": "string", + "enum": [ + "single-state", + "multi-state" + ] } - } + }, + "additionalProperties": false, + "description": "Encumbrance data to create" }, - "TestALicent8Cwee4tcVEF": { + "TestALicenuiANnWJRg2CR": { "required": [ - "upload" + "message" ], "type": "object", "properties": { - "upload": { - "required": [ - "fields", - "url" - ], - "type": "object", - "properties": { - "fields": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "url": { - "type": "string" - } - } + "message": { + "type": "string", + "description": "A message about the request" } } }, - "TestALicenNEZjECY0EIXB": { - "required": [ - "pagination", - "providers" - ], + "TestALicenpkpZkk1id4nC": { "type": "object", "properties": { - "pagination": { + "attributes": { "type": "object", "properties": { - "prevLastKey": { - "maxLength": 1024, + "givenName": { + "maxLength": 100, "minLength": 1, - "type": "object" + "type": "string" }, - "lastKey": { - "maxLength": 1024, + "familyName": { + "maxLength": 100, "minLength": 1, - "type": "object" - }, - "pageSize": { - "maximum": 100, - "minimum": 5, - "type": "integer" + "type": "string" } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "TestALicen8l1lLxH6cpVc": { + "type": "array", + "items": { + "required": [ + "compact", + "jurisdictionName", + "postalAbbreviation" + ], + "type": "object", + "properties": { + "postalAbbreviation": { + "type": "string", + "description": "The postal abbreviation of the jurisdiction" + }, + "compact": { + "type": "string" + }, + "jurisdictionName": { + "type": "string", + "description": "The name of the jurisdiction" } + } + } + }, + "TestALicenmQla1snuDbv9": { + "required": [ + "action", + "licenseScope" + ], + "type": "object", + "properties": { + "action": { + "type": "string", + "enum": [ + "close" + ] }, - "sorting": { + "licenseScope": { + "type": "string", + "enum": [ + "single-state", + "multi-state" + ] + }, + "encumbrance": { "required": [ - "key" + "clinicalPrivilegeActionCategories", + "encumbranceEffectiveDate", + "encumbranceType", + "licenseScope" ], "type": "object", "properties": { - "key": { + "encumbranceEffectiveDate": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", "type": "string", - "description": "The key to sort results by", + "description": "The effective date of the encumbrance", + "format": "date" + }, + "clinicalPrivilegeActionCategories": { + "minItems": 1, + "type": "array", + "description": "The categories of clinical privilege action", + "items": { + "type": "string", + "enum": [ + "Non-Compliance With Requirements", + "Conflict of Interest", + "Substandard Care or Patient Neglect/Abuse", + "Criminal Conviction or Adjudication", + "Confidentiality, Consent or Disclosure Violations", + "Fraud, Deception, or Misrepresentation", + "Improper Supervision or Allowing Unlicensed Practice", + "Improper Prescribing, Dispensing, Administering Medication/Drug Violation", + "Other" + ] + } + }, + "encumbranceType": { + "type": "string", + "description": "The type of encumbrance", "enum": [ - "dateOfUpdate", - "familyName" + "fine", + "reprimand", + "required supervision", + "completion of continuing education", + "public reprimand", + "probation", + "injunctive action", + "suspension", + "revocation", + "denial", + "surrender of license", + "modification of previous action-extension", + "modification of previous action-reduction", + "other monitoring", + "other adjudicated action not listed" ] }, - "direction": { + "licenseScope": { "type": "string", - "description": "Direction to sort results by", "enum": [ - "ascending", - "descending" + "single-state", + "multi-state" ] } }, - "description": "How to sort results" - }, - "providers": { - "maxLength": 100, + "additionalProperties": false, + "description": "Encumbrance data to create" + } + } + }, + "TestALicenMm93S3Be4KsL": { + "required": [ + "enabled" + ], + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "Whether the feature flag is enabled" + } + } + }, + "TestALicen5hRh5X7Ztohi": { + "required": [ + "upload" + ], + "type": "object", + "properties": { + "upload": { + "required": [ + "fields", + "url" + ], + "type": "object", + "properties": { + "fields": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "url": { + "type": "string" + } + } + } + } + }, + "TestALicenNuwYXlN2vJVp": { + "required": [ + "effectiveLiftDate" + ], + "type": "object", + "properties": { + "effectiveLiftDate": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "description": "The effective date when the encumbrance will be lifted", + "format": "date" + } + }, + "additionalProperties": false + }, + "TestALicen50rcrMXljKAv": { + "required": [ + "adverseActions", + "birthMonthDay", + "compact", + "dateOfExpiration", + "dateOfUpdate", + "familyName", + "givenName", + "licenseJurisdiction", + "licenses", + "privileges", + "providerId", + "type" + ], + "type": "object", + "properties": { + "privileges": { "type": "array", "items": { "required": [ - "birthMonthDay", + "administratorSetStatus", "compact", - "compactEligibility", + "compactTransactionId", "dateOfExpiration", - "dateOfUpdate", - "familyName", - "givenName", - "jurisdictionUploadedCompactEligibility", - "jurisdictionUploadedLicenseStatus", + "history", + "jurisdiction", "licenseJurisdiction", - "licenseStatus", + "licenseType", "providerId", + "status", "type" ], "type": "object", "properties": { + "investigationStatus": { + "type": "string", + "description": "Status indicating if the privilege is under investigation", + "enum": [ + "underInvestigation" + ] + }, "licenseJurisdiction": { "type": "string", "enum": [ "al", + "ak", "az", + "ar", + "ca", "co", + "ct", + "de", + "dc", + "fl", + "ga", + "gu", + "hi", + "id", + "il", + "in", + "ia", "ks", "ky", + "la", + "me", "md", + "ma", + "mi", + "mn", + "ms", + "mo", + "mt", + "mp", + "ne", + "nv", + "nh", + "nj", + "nm", + "ny", + "nc", + "nd", "oh", + "ok", + "or", + "pa", + "ri", + "sc", + "sd", "tn", + "tx", + "ut", + "vt", "va", - "wa" + "vi", + "wa", + "wv", + "wi", + "wy" ] }, "compact": { @@ -3775,569 +4866,2064 @@ "socw" ] }, - "givenName": { - "maxLength": 100, - "minLength": 1, - "type": "string" - }, - "compactEligibility": { - "type": "string", - "enum": [ - "eligible", - "ineligible" - ] - }, - "jurisdictionUploadedCompactEligibility": { + "jurisdiction": { "type": "string", "enum": [ - "eligible", - "ineligible" + "al", + "ak", + "az", + "ar", + "ca", + "co", + "ct", + "de", + "dc", + "fl", + "ga", + "gu", + "hi", + "id", + "il", + "in", + "ia", + "ks", + "ky", + "la", + "me", + "md", + "ma", + "mi", + "mn", + "ms", + "mo", + "mt", + "mp", + "ne", + "nv", + "nh", + "nj", + "nm", + "ny", + "nc", + "nd", + "oh", + "ok", + "or", + "pa", + "ri", + "sc", + "sd", + "tn", + "tx", + "ut", + "vt", + "va", + "vi", + "wa", + "wv", + "wi", + "wy" ] }, - "dateOfBirth": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" - }, - "jurisdictionUploadedLicenseStatus": { - "type": "string", - "enum": [ - "active", - "inactive" - ] - }, - "type": { - "type": "string", - "enum": [ - "provider" - ] - }, - "suffix": { - "maxLength": 100, - "minLength": 1, - "type": "string" - }, - "ssnLastFour": { - "pattern": "^[0-9]{4}$", - "type": "string" - }, - "dateOfExpiration": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" - }, - "providerId": { - "pattern": "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab]{1}[0-9a-f]{3}-[0-9a-f]{12}", - "type": "string" - }, - "licenseStatus": { - "type": "string", - "enum": [ - "active", - "inactive" - ] - }, - "familyName": { - "maxLength": 100, - "minLength": 1, - "type": "string" - }, - "middleName": { - "maxLength": 100, - "minLength": 1, - "type": "string" - }, - "birthMonthDay": { - "pattern": "^[01]{1}[0-9]{1}-[0-3]{1}[0-9]{1}$", - "type": "string", - "format": "date" - }, - "dateOfUpdate": { - "type": "string", - "format": "date-time" - } - } - } - } - } - }, - "TestALicenwtaAzkUzgvMz": { - "required": [ - "attributes", - "permissions", - "status", - "userId" - ], - "type": "object", - "properties": { - "permissions": { - "type": "object", - "additionalProperties": { - "type": "object", - "properties": { - "actions": { - "type": "object", - "properties": { - "readPrivate": { - "type": "boolean" - }, - "admin": { - "type": "boolean" + "investigations": { + "type": "array", + "items": { + "required": [ + "compact", + "creationDate", + "dateOfUpdate", + "investigationId", + "jurisdiction", + "licenseScope", + "licenseType", + "providerId", + "submittingUser", + "type" + ], + "type": "object", + "properties": { + "licenseType": { + "type": "string" + }, + "investigationId": { + "type": "string" + }, + "compact": { + "type": "string", + "enum": [ + "socw" + ] + }, + "providerId": { + "pattern": "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab]{1}[0-9a-f]{3}-[0-9a-f]{12}", + "type": "string" + }, + "jurisdiction": { + "type": "string", + "enum": [ + "al", + "ak", + "az", + "ar", + "ca", + "co", + "ct", + "de", + "dc", + "fl", + "ga", + "gu", + "hi", + "id", + "il", + "in", + "ia", + "ks", + "ky", + "la", + "me", + "md", + "ma", + "mi", + "mn", + "ms", + "mo", + "mt", + "mp", + "ne", + "nv", + "nh", + "nj", + "nm", + "ny", + "nc", + "nd", + "oh", + "ok", + "or", + "pa", + "ri", + "sc", + "sd", + "tn", + "tx", + "ut", + "vt", + "va", + "vi", + "wa", + "wv", + "wi", + "wy" + ] + }, + "submittingUser": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "investigation" + ] + }, + "creationDate": { + "type": "string", + "format": "date-time" + }, + "licenseScope": { + "type": "string", + "enum": [ + "single-state", + "multi-state" + ] + }, + "dateOfUpdate": { + "type": "string", + "format": "date-time" + } } } }, - "jurisdictions": { - "type": "object", - "additionalProperties": { + "history": { + "type": "array", + "items": { + "required": [ + "compact", + "dateOfUpdate", + "jurisdiction", + "previous", + "type", + "updateType" + ], "type": "object", "properties": { - "actions": { + "removedValues": { + "type": "array", + "description": "List of field names that were present in the previous record but removed in the update", + "items": { + "type": "string" + } + }, + "licenseType": { + "type": "string", + "enum": [ + "licensed clinical social worker", + "licensed master social worker", + "licensed bachelors social worker" + ] + }, + "compact": { + "type": "string", + "enum": [ + "socw" + ] + }, + "previous": { + "required": [ + "administratorSetStatus", + "compactTransactionId", + "dateOfExpiration", + "licenseJurisdiction" + ], "type": "object", "properties": { - "readPrivate": { - "type": "boolean" + "administratorSetStatus": { + "type": "string", + "enum": [ + "active", + "inactive" + ] }, - "admin": { - "type": "boolean" + "dateOfExpiration": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" }, - "write": { - "type": "boolean" - } - }, - "additionalProperties": false - } - } - } - } - }, - "additionalProperties": false - } - }, - "attributes": { - "required": [ - "email", - "familyName", - "givenName" - ], - "type": "object", - "properties": { - "givenName": { - "maxLength": 100, - "minLength": 1, - "type": "string" - }, - "familyName": { - "maxLength": 100, - "minLength": 1, - "type": "string" - }, - "email": { - "maxLength": 100, - "minLength": 5, - "type": "string" - } - }, - "additionalProperties": false - }, - "userId": { - "type": "string" - }, - "status": { - "type": "string", - "enum": [ - "active", - "inactive" - ] - } - }, - "additionalProperties": false - }, - "TestALicenz60E9GxgxvRT": { - "required": [ - "compactAbbr", - "compactAdverseActionsNotificationEmails", - "compactName", - "compactOperationsTeamEmails", - "configuredStates", - "licenseeRegistrationEnabled" - ], - "type": "object", - "properties": { - "configuredStates": { + "licenseJurisdiction": { + "type": "string", + "enum": [ + "al", + "ak", + "az", + "ar", + "ca", + "co", + "ct", + "de", + "dc", + "fl", + "ga", + "gu", + "hi", + "id", + "il", + "in", + "ia", + "ks", + "ky", + "la", + "me", + "md", + "ma", + "mi", + "mn", + "ms", + "mo", + "mt", + "mp", + "ne", + "nv", + "nh", + "nj", + "nm", + "ny", + "nc", + "nd", + "oh", + "ok", + "or", + "pa", + "ri", + "sc", + "sd", + "tn", + "tx", + "ut", + "vt", + "va", + "vi", + "wa", + "wv", + "wi", + "wy" + ] + }, + "compact": { + "type": "string", + "enum": [ + "socw" + ] + }, + "providerId": { + "pattern": "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab]{1}[0-9a-f]{3}-[0-9a-f]{12}", + "type": "string" + }, + "jurisdiction": { + "type": "string", + "enum": [ + "al", + "ak", + "az", + "ar", + "ca", + "co", + "ct", + "de", + "dc", + "fl", + "ga", + "gu", + "hi", + "id", + "il", + "in", + "ia", + "ks", + "ky", + "la", + "me", + "md", + "ma", + "mi", + "mn", + "ms", + "mo", + "mt", + "mp", + "ne", + "nv", + "nh", + "nj", + "nm", + "ny", + "nc", + "nd", + "oh", + "ok", + "or", + "pa", + "ri", + "sc", + "sd", + "tn", + "tx", + "ut", + "vt", + "va", + "vi", + "wa", + "wv", + "wi", + "wy" + ] + }, + "type": { + "type": "string", + "enum": [ + "privilege" + ] + }, + "compactTransactionId": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "inactive" + ] + } + } + }, + "jurisdiction": { + "type": "string", + "enum": [ + "al", + "ak", + "az", + "ar", + "ca", + "co", + "ct", + "de", + "dc", + "fl", + "ga", + "gu", + "hi", + "id", + "il", + "in", + "ia", + "ks", + "ky", + "la", + "me", + "md", + "ma", + "mi", + "mn", + "ms", + "mo", + "mt", + "mp", + "ne", + "nv", + "nh", + "nj", + "nm", + "ny", + "nc", + "nd", + "oh", + "ok", + "or", + "pa", + "ri", + "sc", + "sd", + "tn", + "tx", + "ut", + "vt", + "va", + "vi", + "wa", + "wv", + "wi", + "wy" + ] + }, + "updatedValues": { + "type": "object", + "properties": { + "administratorSetStatus": { + "type": "string", + "enum": [ + "active", + "inactive" + ] + }, + "dateOfExpiration": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" + }, + "licenseJurisdiction": { + "type": "string", + "enum": [ + "al", + "ak", + "az", + "ar", + "ca", + "co", + "ct", + "de", + "dc", + "fl", + "ga", + "gu", + "hi", + "id", + "il", + "in", + "ia", + "ks", + "ky", + "la", + "me", + "md", + "ma", + "mi", + "mn", + "ms", + "mo", + "mt", + "mp", + "ne", + "nv", + "nh", + "nj", + "nm", + "ny", + "nc", + "nd", + "oh", + "ok", + "or", + "pa", + "ri", + "sc", + "sd", + "tn", + "tx", + "ut", + "vt", + "va", + "vi", + "wa", + "wv", + "wi", + "wy" + ] + }, + "compact": { + "type": "string", + "enum": [ + "socw" + ] + }, + "providerId": { + "pattern": "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab]{1}[0-9a-f]{3}-[0-9a-f]{12}", + "type": "string" + }, + "jurisdiction": { + "type": "string", + "enum": [ + "al", + "ak", + "az", + "ar", + "ca", + "co", + "ct", + "de", + "dc", + "fl", + "ga", + "gu", + "hi", + "id", + "il", + "in", + "ia", + "ks", + "ky", + "la", + "me", + "md", + "ma", + "mi", + "mn", + "ms", + "mo", + "mt", + "mp", + "ne", + "nv", + "nh", + "nj", + "nm", + "ny", + "nc", + "nd", + "oh", + "ok", + "or", + "pa", + "ri", + "sc", + "sd", + "tn", + "tx", + "ut", + "vt", + "va", + "vi", + "wa", + "wv", + "wi", + "wy" + ] + }, + "type": { + "type": "string", + "enum": [ + "privilege" + ] + }, + "compactTransactionId": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "inactive" + ] + } + } + }, + "type": { + "type": "string", + "enum": [ + "privilegeUpdate" + ] + }, + "dateOfUpdate": { + "type": "string", + "format": "date-time" + }, + "updateType": { + "type": "string", + "enum": [ + "deactivation", + "expiration", + "issuance", + "other", + "renewal", + "encumbrance", + "lifting_encumbrance", + "licenseDeactivation" + ] + } + } + } + }, + "type": { + "type": "string", + "enum": [ + "privilege" + ] + }, + "compactTransactionId": { + "type": "string" + }, + "licenseType": { + "type": "string", + "enum": [ + "licensed clinical social worker", + "licensed master social worker", + "licensed bachelors social worker" + ] + }, + "administratorSetStatus": { + "type": "string", + "enum": [ + "active", + "inactive" + ] + }, + "dateOfExpiration": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" + }, + "providerId": { + "pattern": "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab]{1}[0-9a-f]{3}-[0-9a-f]{12}", + "type": "string" + }, + "adverseActions": { + "type": "array", + "items": { + "required": [ + "actionAgainst", + "adverseActionId", + "compact", + "creationDate", + "dateOfUpdate", + "effectiveStartDate", + "encumbranceType", + "jurisdiction", + "licenseScope", + "licenseType", + "licenseTypeAbbreviation", + "providerId", + "type" + ], + "type": "object", + "properties": { + "clinicalPrivilegeActionCategories": { + "minItems": 1, + "type": "array", + "description": "The categories of clinical privilege action", + "items": { + "type": "string", + "enum": [ + "Non-Compliance With Requirements", + "Conflict of Interest", + "Substandard Care or Patient Neglect/Abuse", + "Criminal Conviction or Adjudication", + "Confidentiality, Consent or Disclosure Violations", + "Fraud, Deception, or Misrepresentation", + "Improper Supervision or Allowing Unlicensed Practice", + "Improper Prescribing, Dispensing, Administering Medication/Drug Violation", + "Other" + ] + } + }, + "compact": { + "type": "string", + "enum": [ + "socw" + ] + }, + "jurisdiction": { + "type": "string", + "enum": [ + "al", + "ak", + "az", + "ar", + "ca", + "co", + "ct", + "de", + "dc", + "fl", + "ga", + "gu", + "hi", + "id", + "il", + "in", + "ia", + "ks", + "ky", + "la", + "me", + "md", + "ma", + "mi", + "mn", + "ms", + "mo", + "mt", + "mp", + "ne", + "nv", + "nh", + "nj", + "nm", + "ny", + "nc", + "nd", + "oh", + "ok", + "or", + "pa", + "ri", + "sc", + "sd", + "tn", + "tx", + "ut", + "vt", + "va", + "vi", + "wa", + "wv", + "wi", + "wy" + ] + }, + "licenseTypeAbbreviation": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "adverseAction" + ] + }, + "creationDate": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" + }, + "actionAgainst": { + "type": "string" + }, + "licenseType": { + "type": "string" + }, + "providerId": { + "pattern": "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab]{1}[0-9a-f]{3}-[0-9a-f]{12}", + "type": "string" + }, + "effectiveStartDate": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" + }, + "adverseActionId": { + "type": "string" + }, + "effectiveLiftDate": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" + }, + "encumbranceType": { + "type": "string" + }, + "liftingUser": { + "type": "string" + }, + "licenseScope": { + "type": "string", + "enum": [ + "single-state", + "multi-state" + ] + }, + "dateOfUpdate": { + "type": "string", + "format": "date-time" + } + } + } + }, + "status": { + "type": "string", + "enum": [ + "active", + "inactive" + ] + } + } + } + }, + "licenseJurisdiction": { + "type": "string", + "enum": [ + "al", + "ak", + "az", + "ar", + "ca", + "co", + "ct", + "de", + "dc", + "fl", + "ga", + "gu", + "hi", + "id", + "il", + "in", + "ia", + "ks", + "ky", + "la", + "me", + "md", + "ma", + "mi", + "mn", + "ms", + "mo", + "mt", + "mp", + "ne", + "nv", + "nh", + "nj", + "nm", + "ny", + "nc", + "nd", + "oh", + "ok", + "or", + "pa", + "ri", + "sc", + "sd", + "tn", + "tx", + "ut", + "vt", + "va", + "vi", + "wa", + "wv", + "wi", + "wy" + ] + }, + "compact": { + "type": "string", + "enum": [ + "socw" + ] + }, + "givenName": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "compactEligibility": { + "type": "string", + "enum": [ + "eligible", + "ineligible" + ] + }, + "jurisdictionUploadedCompactEligibility": { + "type": "string", + "enum": [ + "eligible", + "ineligible" + ] + }, + "dateOfBirth": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" + }, + "jurisdictionUploadedLicenseStatus": { + "type": "string", + "enum": [ + "active", + "inactive" + ] + }, + "type": { + "type": "string", + "enum": [ + "provider" + ] + }, + "suffix": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "publicCompactIdentifier": { + "pattern": "^[Ss][Ww][Cc]-[0-9]{4}-[1-9][0-9]*$", + "type": "string" + }, + "licenses": { "type": "array", - "description": "List of states that have submitted configurations and their live status", "items": { "required": [ - "isLive", - "postalAbbreviation" + "compact", + "compactEligibility", + "dateOfExpiration", + "dateOfIssuance", + "dateOfRenewal", + "dateOfUpdate", + "familyName", + "givenName", + "history", + "homeAddressCity", + "homeAddressPostalCode", + "homeAddressState", + "homeAddressStreet1", + "jurisdiction", + "jurisdictionUploadedCompactEligibility", + "jurisdictionUploadedLicenseStatus", + "licenseScope", + "licenseStatus", + "licenseType", + "middleName", + "providerId", + "type" ], "type": "object", "properties": { - "postalAbbreviation": { + "compact": { + "type": "string", + "enum": [ + "socw" + ] + }, + "homeAddressStreet2": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "jurisdiction": { + "type": "string", + "enum": [ + "al", + "ak", + "az", + "ar", + "ca", + "co", + "ct", + "de", + "dc", + "fl", + "ga", + "gu", + "hi", + "id", + "il", + "in", + "ia", + "ks", + "ky", + "la", + "me", + "md", + "ma", + "mi", + "mn", + "ms", + "mo", + "mt", + "mp", + "ne", + "nv", + "nh", + "nj", + "nm", + "ny", + "nc", + "nd", + "oh", + "ok", + "or", + "pa", + "ri", + "sc", + "sd", + "tn", + "tx", + "ut", + "vt", + "va", + "vi", + "wa", + "wv", + "wi", + "wy" + ] + }, + "homeAddressStreet1": { + "maxLength": 100, + "minLength": 2, + "type": "string" + }, + "investigations": { + "type": "array", + "items": { + "required": [ + "compact", + "creationDate", + "dateOfUpdate", + "investigationId", + "jurisdiction", + "licenseScope", + "licenseType", + "providerId", + "submittingUser", + "type" + ], + "type": "object", + "properties": { + "licenseType": { + "type": "string" + }, + "investigationId": { + "type": "string" + }, + "compact": { + "type": "string", + "enum": [ + "socw" + ] + }, + "providerId": { + "pattern": "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab]{1}[0-9a-f]{3}-[0-9a-f]{12}", + "type": "string" + }, + "jurisdiction": { + "type": "string", + "enum": [ + "al", + "ak", + "az", + "ar", + "ca", + "co", + "ct", + "de", + "dc", + "fl", + "ga", + "gu", + "hi", + "id", + "il", + "in", + "ia", + "ks", + "ky", + "la", + "me", + "md", + "ma", + "mi", + "mn", + "ms", + "mo", + "mt", + "mp", + "ne", + "nv", + "nh", + "nj", + "nm", + "ny", + "nc", + "nd", + "oh", + "ok", + "or", + "pa", + "ri", + "sc", + "sd", + "tn", + "tx", + "ut", + "vt", + "va", + "vi", + "wa", + "wv", + "wi", + "wy" + ] + }, + "submittingUser": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "investigation" + ] + }, + "creationDate": { + "type": "string", + "format": "date-time" + }, + "licenseScope": { + "type": "string", + "enum": [ + "single-state", + "multi-state" + ] + }, + "dateOfUpdate": { + "type": "string", + "format": "date-time" + } + } + } + }, + "type": { + "type": "string", + "enum": [ + "license-home" + ] + }, + "suffix": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "dateOfIssuance": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" + }, + "licenseType": { + "type": "string", + "enum": [ + "licensed clinical social worker", + "licensed master social worker", + "licensed bachelors social worker" + ] + }, + "emailAddress": { + "maxLength": 100, + "minLength": 5, + "type": "string", + "format": "email" + }, + "dateOfExpiration": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" + }, + "homeAddressState": { + "maxLength": 100, + "minLength": 2, + "type": "string" + }, + "providerId": { + "pattern": "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab]{1}[0-9a-f]{3}-[0-9a-f]{12}", + "type": "string" + }, + "dateOfRenewal": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" + }, + "familyName": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "homeAddressCity": { + "maxLength": 100, + "minLength": 2, + "type": "string" + }, + "licenseNumber": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "investigationStatus": { + "type": "string", + "description": "Status indicating if the license is under investigation", + "enum": [ + "underInvestigation" + ] + }, + "homeAddressPostalCode": { + "maxLength": 7, + "minLength": 5, + "type": "string" + }, + "compactEligibility": { + "type": "string", + "enum": [ + "eligible", + "ineligible" + ] + }, + "givenName": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "jurisdictionUploadedCompactEligibility": { + "type": "string", + "enum": [ + "eligible", + "ineligible" + ] + }, + "dateOfBirth": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" + }, + "jurisdictionUploadedLicenseStatus": { + "type": "string", + "enum": [ + "active", + "inactive" + ] + }, + "history": { + "type": "array", + "items": { + "required": [ + "compact", + "dateOfUpdate", + "jurisdiction", + "previous", + "type", + "updateType" + ], + "type": "object", + "properties": { + "removedValues": { + "type": "array", + "description": "List of field names that were present in the previous record but removed in the update", + "items": { + "type": "string" + } + }, + "licenseType": { + "type": "string", + "enum": [ + "licensed clinical social worker", + "licensed master social worker", + "licensed bachelors social worker" + ] + }, + "compact": { + "type": "string", + "enum": [ + "socw" + ] + }, + "previous": { + "required": [ + "dateOfExpiration", + "dateOfIssuance", + "dateOfRenewal", + "familyName", + "givenName", + "homeAddressCity", + "homeAddressPostalCode", + "homeAddressState", + "homeAddressStreet1", + "jurisdictionUploadedCompactEligibility", + "jurisdictionUploadedLicenseStatus", + "middleName" + ], + "type": "object", + "properties": { + "homeAddressStreet2": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "homeAddressPostalCode": { + "maxLength": 7, + "minLength": 5, + "type": "string" + }, + "givenName": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "homeAddressStreet1": { + "maxLength": 100, + "minLength": 2, + "type": "string" + }, + "compactEligibility": { + "type": "string", + "enum": [ + "eligible", + "ineligible" + ] + }, + "jurisdictionUploadedCompactEligibility": { + "type": "string", + "enum": [ + "eligible", + "ineligible" + ] + }, + "dateOfBirth": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" + }, + "jurisdictionUploadedLicenseStatus": { + "type": "string", + "enum": [ + "active", + "inactive" + ] + }, + "suffix": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "dateOfIssuance": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" + }, + "emailAddress": { + "maxLength": 100, + "minLength": 5, + "type": "string", + "format": "email" + }, + "dateOfExpiration": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" + }, + "phoneNumber": { + "pattern": "^\\+[0-9]{8,15}$", + "type": "string" + }, + "homeAddressState": { + "maxLength": 100, + "minLength": 2, + "type": "string" + }, + "dateOfRenewal": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" + }, + "licenseStatus": { + "type": "string", + "enum": [ + "active", + "inactive" + ] + }, + "familyName": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "homeAddressCity": { + "maxLength": 100, + "minLength": 2, + "type": "string" + }, + "licenseNumber": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "middleName": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "licenseStatusName": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "licenseScope": { + "type": "string", + "enum": [ + "single-state", + "multi-state" + ] + } + } + }, + "jurisdiction": { + "type": "string", + "enum": [ + "al", + "ak", + "az", + "ar", + "ca", + "co", + "ct", + "de", + "dc", + "fl", + "ga", + "gu", + "hi", + "id", + "il", + "in", + "ia", + "ks", + "ky", + "la", + "me", + "md", + "ma", + "mi", + "mn", + "ms", + "mo", + "mt", + "mp", + "ne", + "nv", + "nh", + "nj", + "nm", + "ny", + "nc", + "nd", + "oh", + "ok", + "or", + "pa", + "ri", + "sc", + "sd", + "tn", + "tx", + "ut", + "vt", + "va", + "vi", + "wa", + "wv", + "wi", + "wy" + ] + }, + "updatedValues": { + "type": "object", + "properties": { + "homeAddressStreet2": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "homeAddressPostalCode": { + "maxLength": 7, + "minLength": 5, + "type": "string" + }, + "givenName": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "homeAddressStreet1": { + "maxLength": 100, + "minLength": 2, + "type": "string" + }, + "compactEligibility": { + "type": "string", + "enum": [ + "eligible", + "ineligible" + ] + }, + "jurisdictionUploadedCompactEligibility": { + "type": "string", + "enum": [ + "eligible", + "ineligible" + ] + }, + "dateOfBirth": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" + }, + "jurisdictionUploadedLicenseStatus": { + "type": "string", + "enum": [ + "active", + "inactive" + ] + }, + "suffix": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "dateOfIssuance": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" + }, + "emailAddress": { + "maxLength": 100, + "minLength": 5, + "type": "string", + "format": "email" + }, + "dateOfExpiration": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" + }, + "phoneNumber": { + "pattern": "^\\+[0-9]{8,15}$", + "type": "string" + }, + "homeAddressState": { + "maxLength": 100, + "minLength": 2, + "type": "string" + }, + "dateOfRenewal": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" + }, + "licenseStatus": { + "type": "string", + "enum": [ + "active", + "inactive" + ] + }, + "familyName": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "homeAddressCity": { + "maxLength": 100, + "minLength": 2, + "type": "string" + }, + "licenseNumber": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "middleName": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "licenseStatusName": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "licenseScope": { + "type": "string", + "enum": [ + "single-state", + "multi-state" + ] + } + } + }, + "type": { + "type": "string", + "enum": [ + "licenseUpdate" + ] + }, + "dateOfUpdate": { + "type": "string", + "format": "date-time" + }, + "updateType": { + "type": "string", + "enum": [ + "deactivation", + "expiration", + "issuance", + "other", + "renewal", + "encumbrance", + "lifting_encumbrance", + "licenseDeactivation" + ] + } + } + } + }, + "ssnLastFour": { + "pattern": "^[0-9]{4}$", + "type": "string" + }, + "phoneNumber": { + "pattern": "^\\+[0-9]{8,15}$", + "type": "string" + }, + "licenseStatus": { "type": "string", - "description": "The postal abbreviation of the jurisdiction", "enum": [ - "al", - "az", - "co", - "ks", - "ky", - "md", - "oh", - "tn", - "va", - "wa" + "active", + "inactive" ] }, - "isLive": { - "type": "boolean", - "description": "Whether the state is live and available for registrations." - } - } - } - }, - "compactAdverseActionsNotificationEmails": { - "type": "array", - "description": "List of email addresses for adverse actions notifications", - "items": { - "type": "string", - "format": "email" - } - }, - "licenseeRegistrationEnabled": { - "type": "boolean", - "description": "Denotes whether licensee registration is enabled" - }, - "compactAbbr": { - "type": "string", - "description": "The abbreviation of the compact" - }, - "compactName": { - "type": "string", - "description": "The full name of the compact" - }, - "compactOperationsTeamEmails": { - "type": "array", - "description": "List of email addresses for operations team notifications", - "items": { - "type": "string", - "format": "email" - } - } - } - }, - "TestALicenpht9Tj1tkyAU": { - "required": [ - "effectiveLiftDate", - "licenseScope" - ], - "type": "object", - "properties": { - "effectiveLiftDate": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "description": "The effective date when the encumbrance will be lifted", - "format": "date" - }, - "licenseScope": { - "type": "string", - "enum": [ - "single-state", - "multi-state" - ] - } - }, - "additionalProperties": false - }, - "TestALicenPU78UBpLgMaP": { - "required": [ - "action" - ], - "type": "object", - "properties": { - "action": { - "type": "string", - "enum": [ - "close" - ] - }, - "encumbrance": { - "required": [ - "clinicalPrivilegeActionCategories", - "encumbranceEffectiveDate", - "encumbranceType" - ], - "type": "object", - "properties": { - "encumbranceEffectiveDate": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "description": "The effective date of the encumbrance", - "format": "date" - }, - "clinicalPrivilegeActionCategories": { - "type": "array", - "description": "The categories of clinical privilege action", - "items": { + "middleName": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "licenseStatusName": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "adverseActions": { + "type": "array", + "items": { + "required": [ + "actionAgainst", + "adverseActionId", + "compact", + "creationDate", + "dateOfUpdate", + "effectiveStartDate", + "encumbranceType", + "jurisdiction", + "licenseScope", + "licenseType", + "licenseTypeAbbreviation", + "providerId", + "type" + ], + "type": "object", + "properties": { + "clinicalPrivilegeActionCategories": { + "minItems": 1, + "type": "array", + "description": "The categories of clinical privilege action", + "items": { + "type": "string", + "enum": [ + "Non-Compliance With Requirements", + "Conflict of Interest", + "Substandard Care or Patient Neglect/Abuse", + "Criminal Conviction or Adjudication", + "Confidentiality, Consent or Disclosure Violations", + "Fraud, Deception, or Misrepresentation", + "Improper Supervision or Allowing Unlicensed Practice", + "Improper Prescribing, Dispensing, Administering Medication/Drug Violation", + "Other" + ] + } + }, + "compact": { + "type": "string", + "enum": [ + "socw" + ] + }, + "jurisdiction": { + "type": "string", + "enum": [ + "al", + "ak", + "az", + "ar", + "ca", + "co", + "ct", + "de", + "dc", + "fl", + "ga", + "gu", + "hi", + "id", + "il", + "in", + "ia", + "ks", + "ky", + "la", + "me", + "md", + "ma", + "mi", + "mn", + "ms", + "mo", + "mt", + "mp", + "ne", + "nv", + "nh", + "nj", + "nm", + "ny", + "nc", + "nd", + "oh", + "ok", + "or", + "pa", + "ri", + "sc", + "sd", + "tn", + "tx", + "ut", + "vt", + "va", + "vi", + "wa", + "wv", + "wi", + "wy" + ] + }, + "licenseTypeAbbreviation": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "adverseAction" + ] + }, + "creationDate": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" + }, + "actionAgainst": { + "type": "string" + }, + "licenseType": { + "type": "string" + }, + "providerId": { + "pattern": "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab]{1}[0-9a-f]{3}-[0-9a-f]{12}", + "type": "string" + }, + "effectiveStartDate": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" + }, + "adverseActionId": { + "type": "string" + }, + "effectiveLiftDate": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" + }, + "encumbranceType": { + "type": "string" + }, + "liftingUser": { + "type": "string" + }, + "licenseScope": { + "type": "string", + "enum": [ + "single-state", + "multi-state" + ] + }, + "dateOfUpdate": { + "type": "string", + "format": "date-time" + } + } + } + }, + "dateOfUpdate": { + "type": "string", + "format": "date-time" + }, + "licenseScope": { "type": "string", "enum": [ - "Non-Compliance With Requirements", - "Conflict of Interest", - "Substandard Care or Patient Neglect/Abuse", - "Criminal Conviction or Adjudication", - "Confidentiality, Consent or Disclosure Violations", - "Fraud, Deception, or Misrepresentation", - "Improper Supervision or Allowing Unlicensed Practice", - "Improper Prescribing, Dispensing, Administering Medication/Drug Violation", - "Other" + "single-state", + "multi-state" ] } - }, - "encumbranceType": { - "type": "string", - "description": "The type of encumbrance", - "enum": [ - "fine", - "reprimand", - "required supervision", - "completion of continuing education", - "public reprimand", - "probation", - "injunctive action", - "suspension", - "revocation", - "denial", - "surrender of license", - "modification of previous action-extension", - "modification of previous action-reduction", - "other monitoring", - "other adjudicated action not listed" - ] } - }, - "additionalProperties": false, - "description": "Encumbrance data to create" - } - } - }, - "TestALicenEVsrIP1Cbe0h": { - "type": "object", - "properties": {} - }, - "TestALicenT0BWB0XcccEO": { - "type": "array", - "items": { - "required": [ - "compact", - "jurisdictionName", - "postalAbbreviation" - ], - "type": "object", - "properties": { - "postalAbbreviation": { - "type": "string", - "description": "The postal abbreviation of the jurisdiction" - }, - "compact": { - "type": "string" - }, - "jurisdictionName": { - "type": "string", - "description": "The name of the jurisdiction" - } - } - } - }, - "TestALicenmi91EHypaiFi": { - "required": [ - "jurisdictionAdverseActionsNotificationEmails", - "jurisdictionOperationsTeamEmails", - "licenseeRegistrationEnabled" - ], - "type": "object", - "properties": { - "jurisdictionAdverseActionsNotificationEmails": { - "maxItems": 10, - "minItems": 1, - "uniqueItems": true, - "type": "array", - "description": "List of email addresses for adverse actions notifications", - "items": { - "type": "string", - "format": "email" } }, - "jurisdictionOperationsTeamEmails": { - "maxItems": 10, - "minItems": 1, - "uniqueItems": true, - "type": "array", - "description": "List of email addresses for operations team notifications", - "items": { - "type": "string", - "format": "email" - } + "ssnLastFour": { + "pattern": "^[0-9]{4}$", + "type": "string" }, - "licenseeRegistrationEnabled": { - "type": "boolean", - "description": "Denotes whether licensee registration is enabled" - } - }, - "additionalProperties": false - }, - "TestALicennE6IeKjO4KkD": { - "required": [ - "message" - ], - "type": "object", - "properties": { - "message": { - "type": "string", - "description": "A message about the request" - } - } - }, - "TestALicennkQ1XbRgr9l0": { - "required": [ - "clinicalPrivilegeActionCategories", - "encumbranceEffectiveDate", - "encumbranceType", - "licenseScope" - ], - "type": "object", - "properties": { - "encumbranceEffectiveDate": { + "dateOfExpiration": { "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", "type": "string", - "description": "The effective date of the encumbrance", "format": "date" }, - "clinicalPrivilegeActionCategories": { - "type": "array", - "description": "The categories of clinical privilege action", - "items": { - "type": "string", - "enum": [ - "Non-Compliance With Requirements", - "Conflict of Interest", - "Substandard Care or Patient Neglect/Abuse", - "Criminal Conviction or Adjudication", - "Confidentiality, Consent or Disclosure Violations", - "Fraud, Deception, or Misrepresentation", - "Improper Supervision or Allowing Unlicensed Practice", - "Improper Prescribing, Dispensing, Administering Medication/Drug Violation", - "Other" - ] - } + "providerId": { + "pattern": "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab]{1}[0-9a-f]{3}-[0-9a-f]{12}", + "type": "string" }, - "encumbranceType": { + "licenseStatus": { "type": "string", - "description": "The type of encumbrance", "enum": [ - "fine", - "reprimand", - "required supervision", - "completion of continuing education", - "public reprimand", - "probation", - "injunctive action", - "suspension", - "revocation", - "denial", - "surrender of license", - "modification of previous action-extension", - "modification of previous action-reduction", - "other monitoring", - "other adjudicated action not listed" + "active", + "inactive" ] }, - "licenseScope": { + "familyName": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "middleName": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "birthMonthDay": { + "pattern": "^[01]{1}[0-9]{1}-[0-3]{1}[0-9]{1}$", "type": "string", - "enum": [ - "single-state", - "multi-state" - ] - } - }, - "additionalProperties": false, - "description": "Encumbrance data to create" - }, - "TestALicenyPPmB5QK4Mwi": { - "required": [ - "compactAdverseActionsNotificationEmails", - "compactOperationsTeamEmails", - "configuredStates", - "licenseeRegistrationEnabled" - ], - "type": "object", - "properties": { - "configuredStates": { + "format": "date" + }, + "adverseActions": { "type": "array", - "description": "List of states that have submitted configurations and their live status", "items": { "required": [ - "isLive", - "postalAbbreviation" + "actionAgainst", + "adverseActionId", + "compact", + "creationDate", + "dateOfUpdate", + "effectiveStartDate", + "encumbranceType", + "jurisdiction", + "licenseScope", + "licenseType", + "licenseTypeAbbreviation", + "providerId", + "type" ], "type": "object", "properties": { - "postalAbbreviation": { + "clinicalPrivilegeActionCategories": { + "minItems": 1, + "type": "array", + "description": "The categories of clinical privilege action", + "items": { + "type": "string", + "enum": [ + "Non-Compliance With Requirements", + "Conflict of Interest", + "Substandard Care or Patient Neglect/Abuse", + "Criminal Conviction or Adjudication", + "Confidentiality, Consent or Disclosure Violations", + "Fraud, Deception, or Misrepresentation", + "Improper Supervision or Allowing Unlicensed Practice", + "Improper Prescribing, Dispensing, Administering Medication/Drug Violation", + "Other" + ] + } + }, + "compact": { + "type": "string", + "enum": [ + "socw" + ] + }, + "jurisdiction": { "type": "string", - "description": "The postal abbreviation of the jurisdiction", "enum": [ "al", + "ak", "az", + "ar", + "ca", "co", + "ct", + "de", + "dc", + "fl", + "ga", + "gu", + "hi", + "id", + "il", + "in", + "ia", "ks", "ky", + "la", + "me", "md", + "ma", + "mi", + "mn", + "ms", + "mo", + "mt", + "mp", + "ne", + "nv", + "nh", + "nj", + "nm", + "ny", + "nc", + "nd", "oh", + "ok", + "or", + "pa", + "ri", + "sc", + "sd", "tn", + "tx", + "ut", + "vt", "va", - "wa" + "vi", + "wa", + "wv", + "wi", + "wy" ] }, - "isLive": { - "type": "boolean", - "description": "Whether the state is live and available for registrations." + "licenseTypeAbbreviation": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "adverseAction" + ] + }, + "creationDate": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" + }, + "actionAgainst": { + "type": "string" + }, + "licenseType": { + "type": "string" + }, + "providerId": { + "pattern": "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab]{1}[0-9a-f]{3}-[0-9a-f]{12}", + "type": "string" + }, + "effectiveStartDate": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" + }, + "adverseActionId": { + "type": "string" + }, + "effectiveLiftDate": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" + }, + "encumbranceType": { + "type": "string" + }, + "liftingUser": { + "type": "string" + }, + "licenseScope": { + "type": "string", + "enum": [ + "single-state", + "multi-state" + ] + }, + "dateOfUpdate": { + "type": "string", + "format": "date-time" } - }, - "additionalProperties": false - } - }, - "compactAdverseActionsNotificationEmails": { - "maxItems": 10, - "minItems": 1, - "uniqueItems": true, - "type": "array", - "description": "List of email addresses for adverse actions notifications", - "items": { - "type": "string", - "format": "email" + } } }, - "licenseeRegistrationEnabled": { - "type": "boolean", - "description": "Denotes whether licensee registration is enabled" - }, - "compactOperationsTeamEmails": { - "maxItems": 10, - "minItems": 1, - "uniqueItems": true, - "type": "array", - "description": "List of email addresses for operations team notifications", - "items": { - "type": "string", - "format": "email" - } + "dateOfUpdate": { + "type": "string", + "format": "date-time" } - }, - "additionalProperties": false + } }, - "TestALicenbkno2SNuWuvY": { + "TestALicenv73HP1pQkT4T": { "required": [ - "action", - "licenseScope" + "action" ], "type": "object", "properties": { @@ -4347,19 +6933,11 @@ "close" ] }, - "licenseScope": { - "type": "string", - "enum": [ - "single-state", - "multi-state" - ] - }, "encumbrance": { "required": [ "clinicalPrivilegeActionCategories", "encumbranceEffectiveDate", - "encumbranceType", - "licenseScope" + "encumbranceType" ], "type": "object", "properties": { @@ -4370,6 +6948,7 @@ "format": "date" }, "clinicalPrivilegeActionCategories": { + "minItems": 1, "type": "array", "description": "The categories of clinical privilege action", "items": { @@ -4407,13 +6986,6 @@ "other monitoring", "other adjudicated action not listed" ] - }, - "licenseScope": { - "type": "string", - "enum": [ - "single-state", - "multi-state" - ] } }, "additionalProperties": false, @@ -4421,158 +6993,166 @@ } } }, - "TestALicenssEUeQmNqrM6": { + "TestALicenMgJ48N3eFq37": { + "type": "object", + "properties": {} + }, + "TestALicenkIOuhcYR4oy0": { "required": [ - "clinicalPrivilegeActionCategories", - "encumbranceEffectiveDate", - "encumbranceType" + "licenseScope" ], "type": "object", "properties": { - "encumbranceEffectiveDate": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "licenseScope": { "type": "string", - "description": "The effective date of the encumbrance", - "format": "date" + "enum": [ + "single-state", + "multi-state" + ] + } + }, + "additionalProperties": false + }, + "TestALicenBLppmGZCgMCA": { + "required": [ + "compact", + "jurisdictionAdverseActionsNotificationEmails", + "jurisdictionName", + "jurisdictionOperationsTeamEmails", + "licenseeRegistrationEnabled", + "postalAbbreviation" + ], + "type": "object", + "properties": { + "postalAbbreviation": { + "type": "string", + "description": "The postal abbreviation of the jurisdiction" }, - "clinicalPrivilegeActionCategories": { + "jurisdictionAdverseActionsNotificationEmails": { "type": "array", - "description": "The categories of clinical privilege action", + "description": "List of email addresses for adverse actions notifications", "items": { "type": "string", - "enum": [ - "Non-Compliance With Requirements", - "Conflict of Interest", - "Substandard Care or Patient Neglect/Abuse", - "Criminal Conviction or Adjudication", - "Confidentiality, Consent or Disclosure Violations", - "Fraud, Deception, or Misrepresentation", - "Improper Supervision or Allowing Unlicensed Practice", - "Improper Prescribing, Dispensing, Administering Medication/Drug Violation", - "Other" - ] + "format": "email" } }, - "encumbranceType": { + "jurisdictionOperationsTeamEmails": { + "type": "array", + "description": "List of email addresses for operations team notifications", + "items": { + "type": "string", + "format": "email" + } + }, + "compact": { "type": "string", - "description": "The type of encumbrance", + "description": "The compact this jurisdiction configuration belongs to", "enum": [ - "fine", - "reprimand", - "required supervision", - "completion of continuing education", - "public reprimand", - "probation", - "injunctive action", - "suspension", - "revocation", - "denial", - "surrender of license", - "modification of previous action-extension", - "modification of previous action-reduction", - "other monitoring", - "other adjudicated action not listed" + "socw" ] + }, + "licenseeRegistrationEnabled": { + "type": "boolean", + "description": "Denotes whether licensee registration is enabled" + }, + "jurisdictionName": { + "type": "string", + "description": "The name of the jurisdiction" } - }, - "additionalProperties": false, - "description": "Encumbrance data to create" + } }, - "TestALicen4oBA2oRHRolJ": { + "TestALicenipCN6Fzx8gkn": { "required": [ - "query" + "attributes", + "permissions", + "status", + "userId" ], "type": "object", "properties": { - "pagination": { + "permissions": { "type": "object", - "properties": { - "lastKey": { - "maxLength": 1024, - "minLength": 1, - "type": "string" + "additionalProperties": { + "type": "object", + "properties": { + "actions": { + "type": "object", + "properties": { + "readPrivate": { + "type": "boolean" + }, + "admin": { + "type": "boolean" + } + } + }, + "jurisdictions": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "actions": { + "type": "object", + "properties": { + "readPrivate": { + "type": "boolean" + }, + "admin": { + "type": "boolean" + }, + "write": { + "type": "boolean" + } + }, + "additionalProperties": false + } + } + } + } }, - "pageSize": { - "maximum": 100, - "minimum": 5, - "type": "integer" - } - }, - "additionalProperties": false + "additionalProperties": false + } }, - "query": { + "attributes": { + "required": [ + "email", + "familyName", + "givenName" + ], "type": "object", "properties": { - "providerId": { - "pattern": "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab]{1}[0-9a-f]{3}-[0-9a-f]{12}", - "type": "string", - "description": "Internal UUID for the provider" - }, - "jurisdiction": { - "type": "string", - "description": "Filter for providers with license in a jurisdiction", - "enum": [ - "al", - "az", - "co", - "ks", - "ky", - "md", - "oh", - "tn", - "va", - "wa" - ] - }, "givenName": { "maxLength": 100, - "type": "string", - "description": "Filter for providers with a given name (familyName is required if givenName is provided)" + "minLength": 1, + "type": "string" }, "familyName": { "maxLength": 100, - "type": "string", - "description": "Filter for providers with a family name" + "minLength": 1, + "type": "string" }, - "licenseNumber": { + "email": { "maxLength": 100, - "minLength": 1, - "type": "string", - "description": "Filter for licenses with a specific license number" + "minLength": 5, + "type": "string" } }, - "additionalProperties": false, - "description": "The query parameters" + "additionalProperties": false }, - "sorting": { - "required": [ - "key" - ], - "type": "object", - "properties": { - "key": { - "type": "string", - "description": "The key to sort results by", - "enum": [ - "dateOfUpdate", - "familyName" - ] - }, - "direction": { - "type": "string", - "description": "Direction to sort results by", - "enum": [ - "ascending", - "descending" - ] - } - }, - "description": "How to sort results" + "userId": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "inactive" + ] } }, "additionalProperties": false }, - "TestALicenSnbehaHnpXHv": { + "TestALicen1p96CBusH3Yd": { "type": "object", "properties": { "pagination": { @@ -4649,206 +7229,34 @@ "additionalProperties": false } }, - "attributes": { - "required": [ - "email", - "familyName", - "givenName" - ], - "type": "object", - "properties": { - "givenName": { - "maxLength": 100, - "minLength": 1, - "type": "string" - }, - "familyName": { - "maxLength": 100, - "minLength": 1, - "type": "string" - }, - "email": { - "maxLength": 100, - "minLength": 5, - "type": "string" - } - }, - "additionalProperties": false - }, - "userId": { - "type": "string" - }, - "status": { - "type": "string", - "enum": [ - "active", - "inactive" - ] - } - }, - "additionalProperties": false - } - } - }, - "additionalProperties": false - }, - "TestALicen49toJAATzdfY": { - "required": [ - "compact", - "jurisdictionAdverseActionsNotificationEmails", - "jurisdictionName", - "jurisdictionOperationsTeamEmails", - "licenseeRegistrationEnabled", - "postalAbbreviation" - ], - "type": "object", - "properties": { - "postalAbbreviation": { - "type": "string", - "description": "The postal abbreviation of the jurisdiction" - }, - "jurisdictionAdverseActionsNotificationEmails": { - "type": "array", - "description": "List of email addresses for adverse actions notifications", - "items": { - "type": "string", - "format": "email" - } - }, - "jurisdictionOperationsTeamEmails": { - "type": "array", - "description": "List of email addresses for operations team notifications", - "items": { - "type": "string", - "format": "email" - } - }, - "compact": { - "type": "string", - "description": "The compact this jurisdiction configuration belongs to", - "enum": [ - "socw" - ] - }, - "licenseeRegistrationEnabled": { - "type": "boolean", - "description": "Denotes whether licensee registration is enabled" - }, - "jurisdictionName": { - "type": "string", - "description": "The name of the jurisdiction" - } - } - }, - "TestALicenmyF8U9gFO2mb": { - "required": [ - "licenseScope" - ], - "type": "object", - "properties": { - "licenseScope": { - "type": "string", - "enum": [ - "single-state", - "multi-state" - ] - } - }, - "additionalProperties": false - }, - "TestALicenI8ObIK8aJtpE": { - "required": [ - "compact", - "dateOfUpdate", - "familyName", - "givenName", - "licenseJurisdiction", - "providerId", - "type" - ], - "type": "object", - "properties": { - "privileges": { - "type": "array", - "items": { - "required": [ - "administratorSetStatus", - "compact", - "dateOfExpiration", - "jurisdiction", - "licenseJurisdiction", - "licenseType", - "providerId", - "status", - "type" - ], - "type": "object", - "properties": { - "licenseType": { - "type": "string", - "enum": [ - "licensed clinical social worker", - "licensed master social worker", - "licensed bachelors social worker" - ] - }, - "administratorSetStatus": { - "type": "string", - "enum": [ - "active", - "inactive" - ] - }, - "licenseJurisdiction": { - "type": "string", - "enum": [ - "al", - "az", - "co", - "ks", - "ky", - "md", - "oh", - "tn", - "va", - "wa" - ] - }, - "dateOfExpiration": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" - }, - "compact": { - "type": "string", - "enum": [ - "socw" - ] - }, - "providerId": { - "pattern": "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab]{1}[0-9a-f]{3}-[0-9a-f]{12}", - "type": "string" - }, - "jurisdiction": { - "type": "string", - "enum": [ - "al", - "az", - "co", - "ks", - "ky", - "md", - "oh", - "tn", - "va", - "wa" - ] + "attributes": { + "required": [ + "email", + "familyName", + "givenName" + ], + "type": "object", + "properties": { + "givenName": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "familyName": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "email": { + "maxLength": 100, + "minLength": 5, + "type": "string" + } + }, + "additionalProperties": false }, - "type": { - "type": "string", - "enum": [ - "privilege" - ] + "userId": { + "type": "string" }, "status": { "type": "string", @@ -4857,151 +7265,91 @@ "inactive" ] } - } + }, + "additionalProperties": false } - }, - "licenses": { - "type": "array", - "description": "Sanitized home-state license rows (LicensePublicResponseSchema)", - "items": { - "required": [ - "compact", - "compactEligibility", - "dateOfExpiration", - "jurisdiction", - "licenseNumber", - "licenseScope", - "licenseStatus", - "licenseType", - "type" - ], + } + }, + "additionalProperties": false + }, + "TestALicen3IyOh208lr9M": { + "required": [ + "attributes", + "permissions" + ], + "type": "object", + "properties": { + "permissions": { + "type": "object", + "additionalProperties": { "type": "object", "properties": { - "licenseType": { - "type": "string", - "enum": [ - "licensed clinical social worker", - "licensed master social worker", - "licensed bachelors social worker" - ] - }, - "dateOfExpiration": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" - }, - "compact": { - "type": "string", - "enum": [ - "socw" - ] - }, - "licenseStatus": { - "type": "string", - "enum": [ - "active", - "inactive" - ] - }, - "jurisdiction": { - "type": "string", - "enum": [ - "al", - "az", - "co", - "ks", - "ky", - "md", - "oh", - "tn", - "va", - "wa" - ] - }, - "compactEligibility": { - "type": "string", - "enum": [ - "eligible", - "ineligible" - ] - }, - "licenseNumber": { - "maxLength": 100, - "minLength": 1, - "type": "string" - }, - "type": { - "type": "string", - "enum": [ - "license" - ] + "actions": { + "type": "object", + "properties": { + "readPrivate": { + "type": "boolean" + }, + "admin": { + "type": "boolean" + } + } }, - "licenseScope": { - "type": "string", - "enum": [ - "single-state", - "multi-state" - ] + "jurisdictions": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "actions": { + "type": "object", + "properties": { + "readPrivate": { + "type": "boolean" + }, + "admin": { + "type": "boolean" + }, + "write": { + "type": "boolean" + } + }, + "additionalProperties": false + } + } + } } - } + }, + "additionalProperties": false } }, - "licenseJurisdiction": { - "type": "string", - "enum": [ - "al", - "az", - "co", - "ks", - "ky", - "md", - "oh", - "tn", - "va", - "wa" - ] - }, - "compact": { - "type": "string", - "enum": [ - "socw" - ] - }, - "providerId": { - "pattern": "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab]{1}[0-9a-f]{3}-[0-9a-f]{12}", - "type": "string" - }, - "givenName": { - "maxLength": 100, - "minLength": 1, - "type": "string" - }, - "familyName": { - "maxLength": 100, - "minLength": 1, - "type": "string" - }, - "middleName": { - "maxLength": 100, - "minLength": 1, - "type": "string" - }, - "type": { - "type": "string", - "enum": [ - "provider" - ] - }, - "suffix": { - "maxLength": 100, - "minLength": 1, - "type": "string" - }, - "dateOfUpdate": { - "type": "string", - "format": "date-time" + "attributes": { + "required": [ + "email", + "familyName", + "givenName" + ], + "type": "object", + "properties": { + "givenName": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "familyName": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "email": { + "maxLength": 100, + "minLength": 5, + "type": "string" + } + }, + "additionalProperties": false } - } + }, + "additionalProperties": false } }, "securitySchemes": { diff --git a/backend/social-work-app/docs/internal/postman/postman-collection.json b/backend/social-work-app/docs/internal/postman/postman-collection.json index acd68a7bd6..9214d95881 100644 --- a/backend/social-work-app/docs/internal/postman/postman-collection.json +++ b/backend/social-work-app/docs/internal/postman/postman-collection.json @@ -10,7 +10,7 @@ "type": "bearer" }, "info": { - "_postman_id": "3888a8a2-222f-482b-b1c5-c21783b2aa3e", + "_postman_id": "782899de-5e17-4559-889a-cb9311eb6ea0", "description": { "content": "", "type": "text/plain" @@ -401,7 +401,7 @@ "item": [ { "event": [], - "id": "04610af3-2603-4eb6-ab7b-42684c141f41", + "id": "618edc0c-06c7-42e3-b245-1ddad8cf1845", "name": "/v1/compacts/:compact", "protocolProfileBehavior": { "disableBodyPruning": true @@ -444,7 +444,7 @@ "response": [ { "_postman_previewlanguage": "json", - "body": "{\n \"compactAbbr\": \"\",\n \"compactAdverseActionsNotificationEmails\": [\n \"\",\n \"\"\n ],\n \"compactName\": \"\",\n \"compactOperationsTeamEmails\": [\n \"\",\n \"\"\n ],\n \"configuredStates\": [\n {\n \"isLive\": \"\",\n \"postalAbbreviation\": \"ky\"\n },\n {\n \"isLive\": \"\",\n \"postalAbbreviation\": \"wa\"\n }\n ],\n \"licenseeRegistrationEnabled\": \"\"\n}", + "body": "{\n \"compactAbbr\": \"\",\n \"compactAdverseActionsNotificationEmails\": [\n \"\",\n \"\"\n ],\n \"compactName\": \"\",\n \"compactOperationsTeamEmails\": [\n \"\",\n \"\"\n ],\n \"configuredStates\": [\n {\n \"isLive\": \"\",\n \"postalAbbreviation\": \"sd\"\n },\n {\n \"isLive\": \"\",\n \"postalAbbreviation\": \"sc\"\n }\n ],\n \"licenseeRegistrationEnabled\": \"\"\n}", "code": 200, "cookie": [], "header": [ @@ -453,7 +453,7 @@ "value": "application/json" } ], - "id": "0d68bb9f-e6b4-42fa-a450-74b473a4add1", + "id": "2ed67d59-77e2-4c95-92f0-7c19722e158c", "name": "200 response", "originalRequest": { "body": {}, @@ -491,7 +491,7 @@ }, { "event": [], - "id": "e80c810c-38f3-4bab-950d-a518d3272d26", + "id": "3d6d08ea-587e-465c-a017-7f5d1620a69f", "name": "/v1/compacts/:compact", "protocolProfileBehavior": { "disableBodyPruning": true @@ -505,7 +505,7 @@ "language": "json" } }, - "raw": "{\n \"compactAdverseActionsNotificationEmails\": [\n \"\"\n ],\n \"compactOperationsTeamEmails\": [\n \"\"\n ],\n \"configuredStates\": [\n {\n \"isLive\": \"\",\n \"postalAbbreviation\": \"ky\"\n },\n {\n \"isLive\": \"\",\n \"postalAbbreviation\": \"al\"\n }\n ],\n \"licenseeRegistrationEnabled\": \"\"\n}" + "raw": "{\n \"compactAdverseActionsNotificationEmails\": [\n \"\"\n ],\n \"compactOperationsTeamEmails\": [\n \"\"\n ],\n \"configuredStates\": [\n {\n \"isLive\": \"\",\n \"postalAbbreviation\": \"in\"\n },\n {\n \"isLive\": \"\",\n \"postalAbbreviation\": \"md\"\n }\n ],\n \"licenseeRegistrationEnabled\": \"\"\n}" }, "description": {}, "header": [ @@ -556,7 +556,7 @@ "value": "application/json" } ], - "id": "7d91f959-d43d-4110-bf05-63dc6115e453", + "id": "d47f166e-98aa-49a7-b213-69dc23c4e00d", "name": "200 response", "originalRequest": { "body": { @@ -567,7 +567,7 @@ "language": "json" } }, - "raw": "{\n \"compactAdverseActionsNotificationEmails\": [\n \"\"\n ],\n \"compactOperationsTeamEmails\": [\n \"\"\n ],\n \"configuredStates\": [\n {\n \"isLive\": \"\",\n \"postalAbbreviation\": \"ky\"\n },\n {\n \"isLive\": \"\",\n \"postalAbbreviation\": \"al\"\n }\n ],\n \"licenseeRegistrationEnabled\": \"\"\n}" + "raw": "{\n \"compactAdverseActionsNotificationEmails\": [\n \"\"\n ],\n \"compactOperationsTeamEmails\": [\n \"\"\n ],\n \"configuredStates\": [\n {\n \"isLive\": \"\",\n \"postalAbbreviation\": \"in\"\n },\n {\n \"isLive\": \"\",\n \"postalAbbreviation\": \"md\"\n }\n ],\n \"licenseeRegistrationEnabled\": \"\"\n}" }, "header": [ { @@ -610,7 +610,7 @@ "item": [ { "event": [], - "id": "a87558bd-4ed0-4082-a381-8fbe17a19282", + "id": "35b4809e-eca0-4ebe-a3f1-45c7369fd1ab", "name": "/v1/compacts/:compact/jurisdictions", "protocolProfileBehavior": { "disableBodyPruning": true @@ -663,7 +663,7 @@ "value": "application/json" } ], - "id": "7115a32f-ed11-45ce-89a3-06c94e99e692", + "id": "31796c8c-aabc-4441-95ae-b950f782fbc5", "name": "200 response", "originalRequest": { "body": {}, @@ -705,7 +705,7 @@ "item": [ { "event": [], - "id": "cda39061-19a0-49eb-b603-eec340a2cf80", + "id": "70f3f6b0-2bab-437f-8eae-1c7cde67f15f", "name": "/v1/compacts/:compact/jurisdictions/:jurisdiction", "protocolProfileBehavior": { "disableBodyPruning": true @@ -769,7 +769,7 @@ "value": "application/json" } ], - "id": "4d6f3211-0360-49e9-81eb-2c527f1c5f15", + "id": "fb09c070-fd10-4da1-bf67-72ffa81822be", "name": "200 response", "originalRequest": { "body": {}, @@ -809,7 +809,7 @@ }, { "event": [], - "id": "de420ce7-72f1-4f63-80a5-2938cc64639a", + "id": "ba9b67e2-7de3-47d5-84d1-61660ffe5f66", "name": "/v1/compacts/:compact/jurisdictions/:jurisdiction", "protocolProfileBehavior": { "disableBodyPruning": true @@ -886,7 +886,7 @@ "value": "application/json" } ], - "id": "30aa9447-2663-43e4-83eb-31d269d02779", + "id": "4be84932-ed35-4f22-b191-c53402b7a769", "name": "200 response", "originalRequest": { "body": { @@ -970,7 +970,7 @@ } } ], - "id": "3bd215e7-701b-463d-b28d-2288effe0e0e", + "id": "4b738b71-8cdd-4079-aa24-66551bfb0efb", "name": "/v1/compacts/:compact/jurisdictions/:jurisdiction/licenses/bulk-upload", "protocolProfileBehavior": { "disableBodyPruning": true @@ -1027,7 +1027,7 @@ "response": [ { "_postman_previewlanguage": "json", - "body": "{\n \"upload\": {\n \"fields\": {\n \"deserunt_d\": \"\",\n \"ete\": \"\",\n \"aliquab3\": \"\",\n \"elit_d\": \"\"\n },\n \"url\": \"\"\n }\n}", + "body": "{\n \"upload\": {\n \"fields\": {\n \"sed2\": \"\",\n \"Ut__\": \"\"\n },\n \"url\": \"\"\n }\n}", "code": 200, "cookie": [], "header": [ @@ -1036,7 +1036,7 @@ "value": "application/json" } ], - "id": "a66eedb6-e258-46ff-8c4f-d3a41139a67c", + "id": "11b23897-eb03-4af2-8ec8-db9a70522791", "name": "200 response", "originalRequest": { "body": {}, @@ -1096,7 +1096,7 @@ "item": [ { "event": [], - "id": "f23d13c0-25b4-484b-af04-9311a538c663", + "id": "e048f7d7-4356-4ad7-90f4-4e13debe78fa", "name": "/v1/compacts/:compact/providers/query", "protocolProfileBehavior": { "disableBodyPruning": true @@ -1110,7 +1110,7 @@ "language": "json" } }, - "raw": "{\n \"query\": {\n \"providerId\": \"e28ca435-11f7-4f20-acdd-0e7e284b86d5\",\n \"jurisdiction\": \"wa\",\n \"givenName\": \"\",\n \"familyName\": \"\",\n \"licenseNumber\": \"\"\n },\n \"pagination\": {\n \"lastKey\": \"\",\n \"pageSize\": \"\"\n },\n \"sorting\": {\n \"key\": \"familyName\",\n \"direction\": \"ascending\"\n }\n}" + "raw": "{\n \"query\": {\n \"providerId\": \"2123ae8e-1aa7-414c-bab8-bea55a1755d3\",\n \"jurisdiction\": \"wa\",\n \"givenName\": \"\",\n \"familyName\": \"\",\n \"licenseNumber\": \"\"\n },\n \"pagination\": {\n \"lastKey\": \"\",\n \"pageSize\": \"\"\n },\n \"sorting\": {\n \"key\": \"familyName\",\n \"direction\": \"ascending\"\n }\n}" }, "description": {}, "header": [ @@ -1154,7 +1154,7 @@ "response": [ { "_postman_previewlanguage": "json", - "body": "{\n \"pagination\": {\n \"prevLastKey\": {},\n \"lastKey\": {},\n \"pageSize\": \"\"\n },\n \"providers\": [\n {\n \"birthMonthDay\": \"01-23\",\n \"compact\": \"socw\",\n \"compactEligibility\": \"eligible\",\n \"dateOfExpiration\": \"2170-01-24\",\n \"dateOfUpdate\": \"\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"jurisdictionUploadedCompactEligibility\": \"ineligible\",\n \"jurisdictionUploadedLicenseStatus\": \"inactive\",\n \"licenseJurisdiction\": \"al\",\n \"licenseStatus\": \"inactive\",\n \"providerId\": \"9256cefc-0fc4-4719-a2c7-810549795078\",\n \"type\": \"provider\",\n \"dateOfBirth\": \"1446-06-31\",\n \"suffix\": \"\",\n \"ssnLastFour\": \"3662\",\n \"middleName\": \"\"\n },\n {\n \"birthMonthDay\": \"05-28\",\n \"compact\": \"socw\",\n \"compactEligibility\": \"eligible\",\n \"dateOfExpiration\": \"1908-04-30\",\n \"dateOfUpdate\": \"\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"jurisdictionUploadedCompactEligibility\": \"ineligible\",\n \"jurisdictionUploadedLicenseStatus\": \"active\",\n \"licenseJurisdiction\": \"md\",\n \"licenseStatus\": \"active\",\n \"providerId\": \"49fc53f5-f060-49c2-ac0c-184adca6de42\",\n \"type\": \"provider\",\n \"dateOfBirth\": \"2727-02-10\",\n \"suffix\": \"\",\n \"ssnLastFour\": \"0383\",\n \"middleName\": \"\"\n }\n ],\n \"sorting\": {\n \"key\": \"dateOfUpdate\",\n \"direction\": \"descending\"\n }\n}", + "body": "{\n \"pagination\": {\n \"prevLastKey\": {},\n \"lastKey\": {},\n \"pageSize\": \"\"\n },\n \"providers\": [\n {\n \"birthMonthDay\": \"05-38\",\n \"compact\": \"socw\",\n \"compactEligibility\": \"ineligible\",\n \"dateOfExpiration\": \"2879-12-07\",\n \"dateOfUpdate\": \"\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"jurisdictionUploadedCompactEligibility\": \"ineligible\",\n \"jurisdictionUploadedLicenseStatus\": \"inactive\",\n \"licenseJurisdiction\": \"mt\",\n \"licenseStatus\": \"inactive\",\n \"providerId\": \"37baf51c-e3fc-4de2-b898-05284a12b7d3\",\n \"type\": \"provider\",\n \"dateOfBirth\": \"2298-10-16\",\n \"suffix\": \"\",\n \"publicCompactIdentifier\": \"SWc-7469-63\",\n \"ssnLastFour\": \"3762\",\n \"middleName\": \"\"\n },\n {\n \"birthMonthDay\": \"16-09\",\n \"compact\": \"socw\",\n \"compactEligibility\": \"eligible\",\n \"dateOfExpiration\": \"2030-12-08\",\n \"dateOfUpdate\": \"\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"jurisdictionUploadedCompactEligibility\": \"ineligible\",\n \"jurisdictionUploadedLicenseStatus\": \"active\",\n \"licenseJurisdiction\": \"nc\",\n \"licenseStatus\": \"inactive\",\n \"providerId\": \"6320d044-de8c-4bdd-8ee9-4a64cd61195a\",\n \"type\": \"provider\",\n \"dateOfBirth\": \"1773-12-21\",\n \"suffix\": \"\",\n \"publicCompactIdentifier\": \"swc-9711-1708890835\",\n \"ssnLastFour\": \"3631\",\n \"middleName\": \"\"\n }\n ],\n \"sorting\": {\n \"key\": \"familyName\",\n \"direction\": \"ascending\"\n }\n}", "code": 200, "cookie": [], "header": [ @@ -1163,7 +1163,7 @@ "value": "application/json" } ], - "id": "5d81d303-7c05-4b45-b518-91cf5fee77f9", + "id": "824d7eef-9453-4815-a7d1-b51ec616b305", "name": "200 response", "originalRequest": { "body": { @@ -1174,7 +1174,7 @@ "language": "json" } }, - "raw": "{\n \"query\": {\n \"providerId\": \"e28ca435-11f7-4f20-acdd-0e7e284b86d5\",\n \"jurisdiction\": \"wa\",\n \"givenName\": \"\",\n \"familyName\": \"\",\n \"licenseNumber\": \"\"\n },\n \"pagination\": {\n \"lastKey\": \"\",\n \"pageSize\": \"\"\n },\n \"sorting\": {\n \"key\": \"familyName\",\n \"direction\": \"ascending\"\n }\n}" + "raw": "{\n \"query\": {\n \"providerId\": \"2123ae8e-1aa7-414c-bab8-bea55a1755d3\",\n \"jurisdiction\": \"wa\",\n \"givenName\": \"\",\n \"familyName\": \"\",\n \"licenseNumber\": \"\"\n },\n \"pagination\": {\n \"lastKey\": \"\",\n \"pageSize\": \"\"\n },\n \"sorting\": {\n \"key\": \"familyName\",\n \"direction\": \"ascending\"\n }\n}" }, "header": [ { @@ -1222,7 +1222,7 @@ "item": [ { "event": [], - "id": "2b04dbac-a803-44dd-991d-0d597e3f25e6", + "id": "6d66b991-3f37-4977-887e-be8f4bb1263c", "name": "/v1/compacts/:compact/providers/:providerId", "protocolProfileBehavior": { "disableBodyPruning": true @@ -1277,7 +1277,7 @@ "response": [ { "_postman_previewlanguage": "json", - "body": "{\n \"adverseActions\": [\n {\n \"actionAgainst\": \"\",\n \"adverseActionId\": \"\",\n \"compact\": \"socw\",\n \"creationDate\": \"1160-06-23\",\n \"dateOfUpdate\": \"\",\n \"effectiveStartDate\": \"2718-12-08\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"al\",\n \"licenseScope\": \"single-state\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"00e7bb0f-4370-4dce-9d4b-6ab3154fc5f2\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"Substandard Care or Patient Neglect/Abuse\",\n \"Other\"\n ],\n \"effectiveLiftDate\": \"1342-10-02\",\n \"liftingUser\": \"\"\n },\n {\n \"actionAgainst\": \"\",\n \"adverseActionId\": \"\",\n \"compact\": \"socw\",\n \"creationDate\": \"2156-10-30\",\n \"dateOfUpdate\": \"\",\n \"effectiveStartDate\": \"2461-12-19\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"va\",\n \"licenseScope\": \"multi-state\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"4a9cf136-1478-4d7d-920a-bf79858455ad\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"Other\",\n \"Other\"\n ],\n \"effectiveLiftDate\": \"1881-07-29\",\n \"liftingUser\": \"\"\n }\n ],\n \"birthMonthDay\": \"08-18\",\n \"compact\": \"socw\",\n \"dateOfExpiration\": \"1280-12-09\",\n \"dateOfUpdate\": \"\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"licenseJurisdiction\": \"ks\",\n \"licenses\": [\n {\n \"compact\": \"socw\",\n \"compactEligibility\": \"ineligible\",\n \"dateOfExpiration\": \"2045-08-30\",\n \"dateOfIssuance\": \"2823-08-03\",\n \"dateOfRenewal\": \"1292-05-30\",\n \"dateOfUpdate\": \"\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"history\": [\n {\n \"compact\": \"socw\",\n \"dateOfUpdate\": \"\",\n \"jurisdiction\": \"ks\",\n \"previous\": {\n \"dateOfExpiration\": \"2221-01-07\",\n \"dateOfIssuance\": \"1280-06-02\",\n \"dateOfRenewal\": \"1825-12-01\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"homeAddressCity\": \"\",\n \"homeAddressPostalCode\": \"\",\n \"homeAddressState\": \"\",\n \"homeAddressStreet1\": \"\",\n \"jurisdictionUploadedCompactEligibility\": \"ineligible\",\n \"jurisdictionUploadedLicenseStatus\": \"inactive\",\n \"middleName\": \"\",\n \"homeAddressStreet2\": \"\",\n \"compactEligibility\": \"eligible\",\n \"dateOfBirth\": \"1880-03-08\",\n \"suffix\": \"\",\n \"emailAddress\": \"\",\n \"phoneNumber\": \"+228872964\",\n \"licenseStatus\": \"inactive\",\n \"licenseNumber\": \"\",\n \"licenseStatusName\": \"\",\n \"licenseScope\": \"single-state\"\n },\n \"type\": \"licenseUpdate\",\n \"updateType\": \"renewal\",\n \"removedValues\": [\n \"\",\n \"\"\n ],\n \"licenseType\": \"licensed bachelors social worker\",\n \"updatedValues\": {\n \"homeAddressStreet2\": \"\",\n \"homeAddressPostalCode\": \"\",\n \"givenName\": \"\",\n \"homeAddressStreet1\": \"\",\n \"compactEligibility\": \"ineligible\",\n \"jurisdictionUploadedCompactEligibility\": \"eligible\",\n \"dateOfBirth\": \"2261-04-11\",\n \"jurisdictionUploadedLicenseStatus\": \"active\",\n \"suffix\": \"\",\n \"dateOfIssuance\": \"1652-11-05\",\n \"emailAddress\": \"\",\n \"dateOfExpiration\": \"1702-09-06\",\n \"phoneNumber\": \"+699958009619\",\n \"homeAddressState\": \"\",\n \"dateOfRenewal\": \"1064-11-30\",\n \"licenseStatus\": \"inactive\",\n \"familyName\": \"\",\n \"homeAddressCity\": \"\",\n \"licenseNumber\": \"\",\n \"middleName\": \"\",\n \"licenseStatusName\": \"\",\n \"licenseScope\": \"multi-state\"\n }\n },\n {\n \"compact\": \"socw\",\n \"dateOfUpdate\": \"\",\n \"jurisdiction\": \"tn\",\n \"previous\": {\n \"dateOfExpiration\": \"1297-10-30\",\n \"dateOfIssuance\": \"2426-05-08\",\n \"dateOfRenewal\": \"2524-05-03\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"homeAddressCity\": \"\",\n \"homeAddressPostalCode\": \"\",\n \"homeAddressState\": \"\",\n \"homeAddressStreet1\": \"\",\n \"jurisdictionUploadedCompactEligibility\": \"ineligible\",\n \"jurisdictionUploadedLicenseStatus\": \"inactive\",\n \"middleName\": \"\",\n \"homeAddressStreet2\": \"\",\n \"compactEligibility\": \"ineligible\",\n \"dateOfBirth\": \"2726-11-27\",\n \"suffix\": \"\",\n \"emailAddress\": \"\",\n \"phoneNumber\": \"+05907510297\",\n \"licenseStatus\": \"active\",\n \"licenseNumber\": \"\",\n \"licenseStatusName\": \"\",\n \"licenseScope\": \"multi-state\"\n },\n \"type\": \"licenseUpdate\",\n \"updateType\": \"lifting_encumbrance\",\n \"removedValues\": [\n \"\",\n \"\"\n ],\n \"licenseType\": \"licensed master social worker\",\n \"updatedValues\": {\n \"homeAddressStreet2\": \"\",\n \"homeAddressPostalCode\": \"\",\n \"givenName\": \"\",\n \"homeAddressStreet1\": \"\",\n \"compactEligibility\": \"ineligible\",\n \"jurisdictionUploadedCompactEligibility\": \"eligible\",\n \"dateOfBirth\": \"1349-08-17\",\n \"jurisdictionUploadedLicenseStatus\": \"inactive\",\n \"suffix\": \"\",\n \"dateOfIssuance\": \"1335-04-18\",\n \"emailAddress\": \"\",\n \"dateOfExpiration\": \"1014-01-31\",\n \"phoneNumber\": \"+828199787\",\n \"homeAddressState\": \"\",\n \"dateOfRenewal\": \"2382-12-21\",\n \"licenseStatus\": \"inactive\",\n \"familyName\": \"\",\n \"homeAddressCity\": \"\",\n \"licenseNumber\": \"\",\n \"middleName\": \"\",\n \"licenseStatusName\": \"\",\n \"licenseScope\": \"single-state\"\n }\n }\n ],\n \"homeAddressCity\": \"\",\n \"homeAddressPostalCode\": \"\",\n \"homeAddressState\": \"\",\n \"homeAddressStreet1\": \"\",\n \"jurisdiction\": \"wa\",\n \"jurisdictionUploadedCompactEligibility\": \"ineligible\",\n \"jurisdictionUploadedLicenseStatus\": \"active\",\n \"licenseScope\": \"single-state\",\n \"licenseStatus\": \"inactive\",\n \"licenseType\": \"licensed clinical social worker\",\n \"middleName\": \"\",\n \"providerId\": \"f60391ec-df56-438b-8874-358e7cd3fdf5\",\n \"type\": \"license-home\",\n \"homeAddressStreet2\": \"\",\n \"investigations\": [\n {\n \"compact\": \"socw\",\n \"creationDate\": \"\",\n \"dateOfUpdate\": \"\",\n \"investigationId\": \"\",\n \"jurisdiction\": \"ky\",\n \"licenseScope\": \"multi-state\",\n \"licenseType\": \"\",\n \"providerId\": \"1f8185a1-dd31-4bc1-be55-aa89287807b8\",\n \"submittingUser\": \"\",\n \"type\": \"investigation\"\n },\n {\n \"compact\": \"socw\",\n \"creationDate\": \"\",\n \"dateOfUpdate\": \"\",\n \"investigationId\": \"\",\n \"jurisdiction\": \"ks\",\n \"licenseScope\": \"single-state\",\n \"licenseType\": \"\",\n \"providerId\": \"bea3edae-8af3-4397-a5e6-d5326cac5206\",\n \"submittingUser\": \"\",\n \"type\": \"investigation\"\n }\n ],\n \"suffix\": \"\",\n \"emailAddress\": \"\",\n \"licenseNumber\": \"\",\n \"investigationStatus\": \"underInvestigation\",\n \"dateOfBirth\": \"2589-10-13\",\n \"ssnLastFour\": \"6996\",\n \"phoneNumber\": \"+76733502344701\",\n \"licenseStatusName\": \"\",\n \"adverseActions\": [\n {\n \"actionAgainst\": \"\",\n \"adverseActionId\": \"\",\n \"compact\": \"socw\",\n \"creationDate\": \"1922-07-21\",\n \"dateOfUpdate\": \"\",\n \"effectiveStartDate\": \"2512-11-04\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"ks\",\n \"licenseScope\": \"multi-state\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"35c884b5-ec90-485e-9864-3e75215f588c\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"Substandard Care or Patient Neglect/Abuse\",\n \"Substandard Care or Patient Neglect/Abuse\"\n ],\n \"effectiveLiftDate\": \"1170-11-31\",\n \"liftingUser\": \"\"\n },\n {\n \"actionAgainst\": \"\",\n \"adverseActionId\": \"\",\n \"compact\": \"socw\",\n \"creationDate\": \"2880-10-31\",\n \"dateOfUpdate\": \"\",\n \"effectiveStartDate\": \"1320-03-30\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"oh\",\n \"licenseScope\": \"multi-state\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"9ad53881-e267-44b2-b339-d50a6abef047\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"Substandard Care or Patient Neglect/Abuse\",\n \"Fraud, Deception, or Misrepresentation\"\n ],\n \"effectiveLiftDate\": \"1422-03-03\",\n \"liftingUser\": \"\"\n }\n ]\n },\n {\n \"compact\": \"socw\",\n \"compactEligibility\": \"eligible\",\n \"dateOfExpiration\": \"1250-09-04\",\n \"dateOfIssuance\": \"2658-08-01\",\n \"dateOfRenewal\": \"2869-10-30\",\n \"dateOfUpdate\": \"\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"history\": [\n {\n \"compact\": \"socw\",\n \"dateOfUpdate\": \"\",\n \"jurisdiction\": \"md\",\n \"previous\": {\n \"dateOfExpiration\": \"1482-08-15\",\n \"dateOfIssuance\": \"2490-11-14\",\n \"dateOfRenewal\": \"1433-07-31\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"homeAddressCity\": \"\",\n \"homeAddressPostalCode\": \"\",\n \"homeAddressState\": \"\",\n \"homeAddressStreet1\": \"\",\n \"jurisdictionUploadedCompactEligibility\": \"eligible\",\n \"jurisdictionUploadedLicenseStatus\": \"inactive\",\n \"middleName\": \"\",\n \"homeAddressStreet2\": \"\",\n \"compactEligibility\": \"ineligible\",\n \"dateOfBirth\": \"1303-10-05\",\n \"suffix\": \"\",\n \"emailAddress\": \"\",\n \"phoneNumber\": \"+0018548593\",\n \"licenseStatus\": \"inactive\",\n \"licenseNumber\": \"\",\n \"licenseStatusName\": \"\",\n \"licenseScope\": \"single-state\"\n },\n \"type\": \"licenseUpdate\",\n \"updateType\": \"renewal\",\n \"removedValues\": [\n \"\",\n \"\"\n ],\n \"licenseType\": \"licensed master social worker\",\n \"updatedValues\": {\n \"homeAddressStreet2\": \"\",\n \"homeAddressPostalCode\": \"\",\n \"givenName\": \"\",\n \"homeAddressStreet1\": \"\",\n \"compactEligibility\": \"ineligible\",\n \"jurisdictionUploadedCompactEligibility\": \"eligible\",\n \"dateOfBirth\": \"2352-12-31\",\n \"jurisdictionUploadedLicenseStatus\": \"inactive\",\n \"suffix\": \"\",\n \"dateOfIssuance\": \"2225-10-01\",\n \"emailAddress\": \"\",\n \"dateOfExpiration\": \"1081-12-14\",\n \"phoneNumber\": \"+81818666589\",\n \"homeAddressState\": \"\",\n \"dateOfRenewal\": \"2510-11-20\",\n \"licenseStatus\": \"active\",\n \"familyName\": \"\",\n \"homeAddressCity\": \"\",\n \"licenseNumber\": \"\",\n \"middleName\": \"\",\n \"licenseStatusName\": \"\",\n \"licenseScope\": \"single-state\"\n }\n },\n {\n \"compact\": \"socw\",\n \"dateOfUpdate\": \"\",\n \"jurisdiction\": \"ks\",\n \"previous\": {\n \"dateOfExpiration\": \"2336-10-02\",\n \"dateOfIssuance\": \"1736-04-30\",\n \"dateOfRenewal\": \"1854-12-20\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"homeAddressCity\": \"\",\n \"homeAddressPostalCode\": \"\",\n \"homeAddressState\": \"\",\n \"homeAddressStreet1\": \"\",\n \"jurisdictionUploadedCompactEligibility\": \"ineligible\",\n \"jurisdictionUploadedLicenseStatus\": \"inactive\",\n \"middleName\": \"\",\n \"homeAddressStreet2\": \"\",\n \"compactEligibility\": \"ineligible\",\n \"dateOfBirth\": \"2950-07-01\",\n \"suffix\": \"\",\n \"emailAddress\": \"\",\n \"phoneNumber\": \"+8914586016122\",\n \"licenseStatus\": \"active\",\n \"licenseNumber\": \"\",\n \"licenseStatusName\": \"\",\n \"licenseScope\": \"multi-state\"\n },\n \"type\": \"licenseUpdate\",\n \"updateType\": \"renewal\",\n \"removedValues\": [\n \"\",\n \"\"\n ],\n \"licenseType\": \"licensed bachelors social worker\",\n \"updatedValues\": {\n \"homeAddressStreet2\": \"\",\n \"homeAddressPostalCode\": \"\",\n \"givenName\": \"\",\n \"homeAddressStreet1\": \"\",\n \"compactEligibility\": \"eligible\",\n \"jurisdictionUploadedCompactEligibility\": \"eligible\",\n \"dateOfBirth\": \"2885-10-12\",\n \"jurisdictionUploadedLicenseStatus\": \"active\",\n \"suffix\": \"\",\n \"dateOfIssuance\": \"1258-12-18\",\n \"emailAddress\": \"\",\n \"dateOfExpiration\": \"1363-09-16\",\n \"phoneNumber\": \"+2051028595\",\n \"homeAddressState\": \"\",\n \"dateOfRenewal\": \"1464-10-30\",\n \"licenseStatus\": \"inactive\",\n \"familyName\": \"\",\n \"homeAddressCity\": \"\",\n \"licenseNumber\": \"\",\n \"middleName\": \"\",\n \"licenseStatusName\": \"\",\n \"licenseScope\": \"single-state\"\n }\n }\n ],\n \"homeAddressCity\": \"\",\n \"homeAddressPostalCode\": \"\",\n \"homeAddressState\": \"\",\n \"homeAddressStreet1\": \"\",\n \"jurisdiction\": \"co\",\n \"jurisdictionUploadedCompactEligibility\": \"eligible\",\n \"jurisdictionUploadedLicenseStatus\": \"active\",\n \"licenseScope\": \"multi-state\",\n \"licenseStatus\": \"inactive\",\n \"licenseType\": \"licensed clinical social worker\",\n \"middleName\": \"\",\n \"providerId\": \"7e09156b-f8c2-4862-b7f3-5157b2f07f43\",\n \"type\": \"license-home\",\n \"homeAddressStreet2\": \"\",\n \"investigations\": [\n {\n \"compact\": \"socw\",\n \"creationDate\": \"\",\n \"dateOfUpdate\": \"\",\n \"investigationId\": \"\",\n \"jurisdiction\": \"wa\",\n \"licenseScope\": \"single-state\",\n \"licenseType\": \"\",\n \"providerId\": \"3320bcaa-9f82-4e08-8f9a-50dc28935980\",\n \"submittingUser\": \"\",\n \"type\": \"investigation\"\n },\n {\n \"compact\": \"socw\",\n \"creationDate\": \"\",\n \"dateOfUpdate\": \"\",\n \"investigationId\": \"\",\n \"jurisdiction\": \"wa\",\n \"licenseScope\": \"single-state\",\n \"licenseType\": \"\",\n \"providerId\": \"3699cad2-8687-4190-92a4-cb3f64a0d460\",\n \"submittingUser\": \"\",\n \"type\": \"investigation\"\n }\n ],\n \"suffix\": \"\",\n \"emailAddress\": \"\",\n \"licenseNumber\": \"\",\n \"investigationStatus\": \"underInvestigation\",\n \"dateOfBirth\": \"2804-09-08\",\n \"ssnLastFour\": \"2627\",\n \"phoneNumber\": \"+164734901773\",\n \"licenseStatusName\": \"\",\n \"adverseActions\": [\n {\n \"actionAgainst\": \"\",\n \"adverseActionId\": \"\",\n \"compact\": \"socw\",\n \"creationDate\": \"1138-12-01\",\n \"dateOfUpdate\": \"\",\n \"effectiveStartDate\": \"2164-03-30\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"tn\",\n \"licenseScope\": \"single-state\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"f5f102da-48d5-4b54-8cae-3f4d3a671fcf\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"Substandard Care or Patient Neglect/Abuse\",\n \"Fraud, Deception, or Misrepresentation\"\n ],\n \"effectiveLiftDate\": \"2205-04-01\",\n \"liftingUser\": \"\"\n },\n {\n \"actionAgainst\": \"\",\n \"adverseActionId\": \"\",\n \"compact\": \"socw\",\n \"creationDate\": \"2512-12-15\",\n \"dateOfUpdate\": \"\",\n \"effectiveStartDate\": \"2684-10-10\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"az\",\n \"licenseScope\": \"single-state\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"89fd3a63-b365-4d4b-8f12-dd87e5edc6ef\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"Substandard Care or Patient Neglect/Abuse\",\n \"Other\"\n ],\n \"effectiveLiftDate\": \"2897-11-27\",\n \"liftingUser\": \"\"\n }\n ]\n }\n ],\n \"privileges\": [\n {\n \"administratorSetStatus\": \"active\",\n \"compact\": \"socw\",\n \"compactTransactionId\": \"\",\n \"dateOfExpiration\": \"2576-11-31\",\n \"history\": [\n {\n \"compact\": \"socw\",\n \"dateOfUpdate\": \"\",\n \"jurisdiction\": \"md\",\n \"previous\": {\n \"administratorSetStatus\": \"inactive\",\n \"compactTransactionId\": \"\",\n \"dateOfExpiration\": \"1718-08-14\",\n \"licenseJurisdiction\": \"tn\",\n \"compact\": \"socw\",\n \"providerId\": \"641b6c40-e394-4a04-8223-0a01311702a9\",\n \"jurisdiction\": \"az\",\n \"type\": \"privilege\",\n \"status\": \"active\"\n },\n \"type\": \"privilegeUpdate\",\n \"updateType\": \"renewal\",\n \"removedValues\": [\n \"\",\n \"\"\n ],\n \"licenseType\": \"licensed clinical social worker\",\n \"updatedValues\": {\n \"administratorSetStatus\": \"inactive\",\n \"dateOfExpiration\": \"2929-11-07\",\n \"licenseJurisdiction\": \"tn\",\n \"compact\": \"socw\",\n \"providerId\": \"8fb4643f-8c7d-4081-829c-6494a12be670\",\n \"jurisdiction\": \"co\",\n \"type\": \"privilege\",\n \"compactTransactionId\": \"\",\n \"status\": \"inactive\"\n }\n },\n {\n \"compact\": \"socw\",\n \"dateOfUpdate\": \"\",\n \"jurisdiction\": \"al\",\n \"previous\": {\n \"administratorSetStatus\": \"active\",\n \"compactTransactionId\": \"\",\n \"dateOfExpiration\": \"2462-02-07\",\n \"licenseJurisdiction\": \"ks\",\n \"compact\": \"socw\",\n \"providerId\": \"db84b60a-eb5a-4837-a460-7bb61b89589b\",\n \"jurisdiction\": \"al\",\n \"type\": \"privilege\",\n \"status\": \"inactive\"\n },\n \"type\": \"privilegeUpdate\",\n \"updateType\": \"renewal\",\n \"removedValues\": [\n \"\",\n \"\"\n ],\n \"licenseType\": \"licensed master social worker\",\n \"updatedValues\": {\n \"administratorSetStatus\": \"active\",\n \"dateOfExpiration\": \"1175-01-31\",\n \"licenseJurisdiction\": \"az\",\n \"compact\": \"socw\",\n \"providerId\": \"5dc09001-1e59-4959-a599-8c1f0fec06e5\",\n \"jurisdiction\": \"tn\",\n \"type\": \"privilege\",\n \"compactTransactionId\": \"\",\n \"status\": \"active\"\n }\n }\n ],\n \"jurisdiction\": \"az\",\n \"licenseJurisdiction\": \"az\",\n \"licenseType\": \"licensed bachelors social worker\",\n \"providerId\": \"f329b4ce-e452-46a7-8934-8155538cd716\",\n \"status\": \"active\",\n \"type\": \"privilege\",\n \"investigationStatus\": \"underInvestigation\",\n \"investigations\": [\n {\n \"compact\": \"socw\",\n \"creationDate\": \"\",\n \"dateOfUpdate\": \"\",\n \"investigationId\": \"\",\n \"jurisdiction\": \"ks\",\n \"licenseScope\": \"single-state\",\n \"licenseType\": \"\",\n \"providerId\": \"c4d3fec4-5881-48ce-b19e-0b74edb8bbd4\",\n \"submittingUser\": \"\",\n \"type\": \"investigation\"\n },\n {\n \"compact\": \"socw\",\n \"creationDate\": \"\",\n \"dateOfUpdate\": \"\",\n \"investigationId\": \"\",\n \"jurisdiction\": \"oh\",\n \"licenseScope\": \"single-state\",\n \"licenseType\": \"\",\n \"providerId\": \"9cd326d9-3bd9-404c-a86f-7e2c0528306c\",\n \"submittingUser\": \"\",\n \"type\": \"investigation\"\n }\n ],\n \"adverseActions\": [\n {\n \"actionAgainst\": \"\",\n \"adverseActionId\": \"\",\n \"compact\": \"socw\",\n \"creationDate\": \"1463-01-05\",\n \"dateOfUpdate\": \"\",\n \"effectiveStartDate\": \"2747-10-16\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"va\",\n \"licenseScope\": \"single-state\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"d68dfee6-451a-4e1a-9a9d-f26af02e9b2f\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"Fraud, Deception, or Misrepresentation\",\n \"Other\"\n ],\n \"effectiveLiftDate\": \"2386-10-05\",\n \"liftingUser\": \"\"\n },\n {\n \"actionAgainst\": \"\",\n \"adverseActionId\": \"\",\n \"compact\": \"socw\",\n \"creationDate\": \"2449-08-06\",\n \"dateOfUpdate\": \"\",\n \"effectiveStartDate\": \"2471-11-17\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"ks\",\n \"licenseScope\": \"single-state\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"9b830bdd-3b8d-42eb-b00b-d7165d32d537\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"Other\",\n \"Substandard Care or Patient Neglect/Abuse\"\n ],\n \"effectiveLiftDate\": \"2820-09-03\",\n \"liftingUser\": \"\"\n }\n ]\n },\n {\n \"administratorSetStatus\": \"active\",\n \"compact\": \"socw\",\n \"compactTransactionId\": \"\",\n \"dateOfExpiration\": \"2640-03-22\",\n \"history\": [\n {\n \"compact\": \"socw\",\n \"dateOfUpdate\": \"\",\n \"jurisdiction\": \"az\",\n \"previous\": {\n \"administratorSetStatus\": \"active\",\n \"compactTransactionId\": \"\",\n \"dateOfExpiration\": \"1072-10-30\",\n \"licenseJurisdiction\": \"oh\",\n \"compact\": \"socw\",\n \"providerId\": \"dc7ee300-0359-4b89-9cae-f926fee0d44e\",\n \"jurisdiction\": \"oh\",\n \"type\": \"privilege\",\n \"status\": \"inactive\"\n },\n \"type\": \"privilegeUpdate\",\n \"updateType\": \"expiration\",\n \"removedValues\": [\n \"\",\n \"\"\n ],\n \"licenseType\": \"licensed bachelors social worker\",\n \"updatedValues\": {\n \"administratorSetStatus\": \"active\",\n \"dateOfExpiration\": \"1216-03-01\",\n \"licenseJurisdiction\": \"ks\",\n \"compact\": \"socw\",\n \"providerId\": \"291a80ea-0792-498c-91dc-ca05c99a5c42\",\n \"jurisdiction\": \"oh\",\n \"type\": \"privilege\",\n \"compactTransactionId\": \"\",\n \"status\": \"active\"\n }\n },\n {\n \"compact\": \"socw\",\n \"dateOfUpdate\": \"\",\n \"jurisdiction\": \"ky\",\n \"previous\": {\n \"administratorSetStatus\": \"active\",\n \"compactTransactionId\": \"\",\n \"dateOfExpiration\": \"2251-08-05\",\n \"licenseJurisdiction\": \"wa\",\n \"compact\": \"socw\",\n \"providerId\": \"5842b9b4-06a7-49b5-9de8-ea0a610d1bba\",\n \"jurisdiction\": \"md\",\n \"type\": \"privilege\",\n \"status\": \"active\"\n },\n \"type\": \"privilegeUpdate\",\n \"updateType\": \"renewal\",\n \"removedValues\": [\n \"\",\n \"\"\n ],\n \"licenseType\": \"licensed master social worker\",\n \"updatedValues\": {\n \"administratorSetStatus\": \"inactive\",\n \"dateOfExpiration\": \"1510-08-31\",\n \"licenseJurisdiction\": \"co\",\n \"compact\": \"socw\",\n \"providerId\": \"83c3146f-6831-4c4d-ba4c-76cce2a8d777\",\n \"jurisdiction\": \"al\",\n \"type\": \"privilege\",\n \"compactTransactionId\": \"\",\n \"status\": \"inactive\"\n }\n }\n ],\n \"jurisdiction\": \"oh\",\n \"licenseJurisdiction\": \"ky\",\n \"licenseType\": \"licensed bachelors social worker\",\n \"providerId\": \"b93e01ad-7dcc-4c22-abcb-650166be90db\",\n \"status\": \"active\",\n \"type\": \"privilege\",\n \"investigationStatus\": \"underInvestigation\",\n \"investigations\": [\n {\n \"compact\": \"socw\",\n \"creationDate\": \"\",\n \"dateOfUpdate\": \"\",\n \"investigationId\": \"\",\n \"jurisdiction\": \"md\",\n \"licenseScope\": \"multi-state\",\n \"licenseType\": \"\",\n \"providerId\": \"e42f9896-f31a-43ea-a6b9-4eb835ba91ad\",\n \"submittingUser\": \"\",\n \"type\": \"investigation\"\n },\n {\n \"compact\": \"socw\",\n \"creationDate\": \"\",\n \"dateOfUpdate\": \"\",\n \"investigationId\": \"\",\n \"jurisdiction\": \"oh\",\n \"licenseScope\": \"single-state\",\n \"licenseType\": \"\",\n \"providerId\": \"5f20c301-3ab6-48c0-aea5-2f60f5940b7a\",\n \"submittingUser\": \"\",\n \"type\": \"investigation\"\n }\n ],\n \"adverseActions\": [\n {\n \"actionAgainst\": \"\",\n \"adverseActionId\": \"\",\n \"compact\": \"socw\",\n \"creationDate\": \"1627-10-12\",\n \"dateOfUpdate\": \"\",\n \"effectiveStartDate\": \"1337-10-04\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"tn\",\n \"licenseScope\": \"single-state\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"c283f6e9-bf3d-49a7-a64a-e175389cd173\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"Substandard Care or Patient Neglect/Abuse\",\n \"Other\"\n ],\n \"effectiveLiftDate\": \"1675-12-05\",\n \"liftingUser\": \"\"\n },\n {\n \"actionAgainst\": \"\",\n \"adverseActionId\": \"\",\n \"compact\": \"socw\",\n \"creationDate\": \"2072-10-31\",\n \"dateOfUpdate\": \"\",\n \"effectiveStartDate\": \"1382-11-30\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"va\",\n \"licenseScope\": \"single-state\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"4fbfa1ab-7422-41ac-bc80-cf2ee6255dd5\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"Fraud, Deception, or Misrepresentation\",\n \"Other\"\n ],\n \"effectiveLiftDate\": \"2776-04-20\",\n \"liftingUser\": \"\"\n }\n ]\n }\n ],\n \"providerId\": \"4487f21b-62b6-435b-ab99-27cdd19f5dd2\",\n \"type\": \"provider\",\n \"compactEligibility\": \"ineligible\",\n \"jurisdictionUploadedCompactEligibility\": \"eligible\",\n \"dateOfBirth\": \"2328-12-30\",\n \"jurisdictionUploadedLicenseStatus\": \"active\",\n \"suffix\": \"\",\n \"ssnLastFour\": \"8801\",\n \"licenseStatus\": \"inactive\",\n \"middleName\": \"\"\n}", + "body": "{\n \"adverseActions\": [\n {\n \"actionAgainst\": \"\",\n \"adverseActionId\": \"\",\n \"compact\": \"socw\",\n \"creationDate\": \"1869-12-31\",\n \"dateOfUpdate\": \"\",\n \"effectiveStartDate\": \"1943-10-01\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"al\",\n \"licenseScope\": \"single-state\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"efc455d4-718e-4d8f-aa68-b4b73e93a737\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"Criminal Conviction or Adjudication\"\n ],\n \"effectiveLiftDate\": \"1293-12-30\",\n \"liftingUser\": \"\"\n },\n {\n \"actionAgainst\": \"\",\n \"adverseActionId\": \"\",\n \"compact\": \"socw\",\n \"creationDate\": \"2827-04-30\",\n \"dateOfUpdate\": \"\",\n \"effectiveStartDate\": \"2027-12-05\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"ca\",\n \"licenseScope\": \"multi-state\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"a78d1925-802c-4c3d-b178-f91e355eb3ec\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"Improper Prescribing, Dispensing, Administering Medication/Drug Violation\"\n ],\n \"effectiveLiftDate\": \"2682-10-07\",\n \"liftingUser\": \"\"\n }\n ],\n \"birthMonthDay\": \"02-29\",\n \"compact\": \"socw\",\n \"dateOfExpiration\": \"1813-04-31\",\n \"dateOfUpdate\": \"\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"licenseJurisdiction\": \"pa\",\n \"licenses\": [\n {\n \"compact\": \"socw\",\n \"compactEligibility\": \"eligible\",\n \"dateOfExpiration\": \"2431-12-28\",\n \"dateOfIssuance\": \"2356-11-31\",\n \"dateOfRenewal\": \"1267-06-23\",\n \"dateOfUpdate\": \"\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"history\": [\n {\n \"compact\": \"socw\",\n \"dateOfUpdate\": \"\",\n \"jurisdiction\": \"ak\",\n \"previous\": {\n \"dateOfExpiration\": \"1225-02-05\",\n \"dateOfIssuance\": \"2218-02-31\",\n \"dateOfRenewal\": \"2362-12-08\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"homeAddressCity\": \"\",\n \"homeAddressPostalCode\": \"\",\n \"homeAddressState\": \"\",\n \"homeAddressStreet1\": \"\",\n \"jurisdictionUploadedCompactEligibility\": \"eligible\",\n \"jurisdictionUploadedLicenseStatus\": \"active\",\n \"middleName\": \"\",\n \"homeAddressStreet2\": \"\",\n \"compactEligibility\": \"ineligible\",\n \"dateOfBirth\": \"2790-12-31\",\n \"suffix\": \"\",\n \"emailAddress\": \"\",\n \"phoneNumber\": \"+05705496\",\n \"licenseStatus\": \"active\",\n \"licenseNumber\": \"\",\n \"licenseStatusName\": \"\",\n \"licenseScope\": \"multi-state\"\n },\n \"type\": \"licenseUpdate\",\n \"updateType\": \"other\",\n \"removedValues\": [\n \"\",\n \"\"\n ],\n \"licenseType\": \"licensed bachelors social worker\",\n \"updatedValues\": {\n \"homeAddressStreet2\": \"\",\n \"homeAddressPostalCode\": \"\",\n \"givenName\": \"\",\n \"homeAddressStreet1\": \"\",\n \"compactEligibility\": \"ineligible\",\n \"jurisdictionUploadedCompactEligibility\": \"ineligible\",\n \"dateOfBirth\": \"1449-04-06\",\n \"jurisdictionUploadedLicenseStatus\": \"active\",\n \"suffix\": \"\",\n \"dateOfIssuance\": \"2521-11-31\",\n \"emailAddress\": \"\",\n \"dateOfExpiration\": \"2986-06-31\",\n \"phoneNumber\": \"+09362330511164\",\n \"homeAddressState\": \"\",\n \"dateOfRenewal\": \"2087-02-31\",\n \"licenseStatus\": \"active\",\n \"familyName\": \"\",\n \"homeAddressCity\": \"\",\n \"licenseNumber\": \"\",\n \"middleName\": \"\",\n \"licenseStatusName\": \"\",\n \"licenseScope\": \"single-state\"\n }\n },\n {\n \"compact\": \"socw\",\n \"dateOfUpdate\": \"\",\n \"jurisdiction\": \"fl\",\n \"previous\": {\n \"dateOfExpiration\": \"1072-04-30\",\n \"dateOfIssuance\": \"2582-12-18\",\n \"dateOfRenewal\": \"2519-10-31\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"homeAddressCity\": \"\",\n \"homeAddressPostalCode\": \"\",\n \"homeAddressState\": \"\",\n \"homeAddressStreet1\": \"\",\n \"jurisdictionUploadedCompactEligibility\": \"ineligible\",\n \"jurisdictionUploadedLicenseStatus\": \"active\",\n \"middleName\": \"\",\n \"homeAddressStreet2\": \"\",\n \"compactEligibility\": \"eligible\",\n \"dateOfBirth\": \"1242-07-09\",\n \"suffix\": \"\",\n \"emailAddress\": \"\",\n \"phoneNumber\": \"+914880825026435\",\n \"licenseStatus\": \"inactive\",\n \"licenseNumber\": \"\",\n \"licenseStatusName\": \"\",\n \"licenseScope\": \"multi-state\"\n },\n \"type\": \"licenseUpdate\",\n \"updateType\": \"expiration\",\n \"removedValues\": [\n \"\",\n \"\"\n ],\n \"licenseType\": \"licensed clinical social worker\",\n \"updatedValues\": {\n \"homeAddressStreet2\": \"\",\n \"homeAddressPostalCode\": \"\",\n \"givenName\": \"\",\n \"homeAddressStreet1\": \"\",\n \"compactEligibility\": \"eligible\",\n \"jurisdictionUploadedCompactEligibility\": \"ineligible\",\n \"dateOfBirth\": \"2584-12-03\",\n \"jurisdictionUploadedLicenseStatus\": \"active\",\n \"suffix\": \"\",\n \"dateOfIssuance\": \"2432-11-09\",\n \"emailAddress\": \"\",\n \"dateOfExpiration\": \"2556-11-02\",\n \"phoneNumber\": \"+7412821694\",\n \"homeAddressState\": \"\",\n \"dateOfRenewal\": \"1544-12-13\",\n \"licenseStatus\": \"active\",\n \"familyName\": \"\",\n \"homeAddressCity\": \"\",\n \"licenseNumber\": \"\",\n \"middleName\": \"\",\n \"licenseStatusName\": \"\",\n \"licenseScope\": \"multi-state\"\n }\n }\n ],\n \"homeAddressCity\": \"\",\n \"homeAddressPostalCode\": \"\",\n \"homeAddressState\": \"\",\n \"homeAddressStreet1\": \"\",\n \"jurisdiction\": \"mn\",\n \"jurisdictionUploadedCompactEligibility\": \"ineligible\",\n \"jurisdictionUploadedLicenseStatus\": \"inactive\",\n \"licenseScope\": \"single-state\",\n \"licenseStatus\": \"active\",\n \"licenseType\": \"licensed master social worker\",\n \"middleName\": \"\",\n \"providerId\": \"667105b7-1e98-4a5c-a440-1a5b7fd3d2b3\",\n \"type\": \"license-home\",\n \"homeAddressStreet2\": \"\",\n \"investigations\": [\n {\n \"compact\": \"socw\",\n \"creationDate\": \"\",\n \"dateOfUpdate\": \"\",\n \"investigationId\": \"\",\n \"jurisdiction\": \"nc\",\n \"licenseScope\": \"multi-state\",\n \"licenseType\": \"\",\n \"providerId\": \"8c51402a-1770-4a73-a770-420fb68fa4be\",\n \"submittingUser\": \"\",\n \"type\": \"investigation\"\n },\n {\n \"compact\": \"socw\",\n \"creationDate\": \"\",\n \"dateOfUpdate\": \"\",\n \"investigationId\": \"\",\n \"jurisdiction\": \"ar\",\n \"licenseScope\": \"single-state\",\n \"licenseType\": \"\",\n \"providerId\": \"b1ee157f-3e2e-4172-af3d-4f0fc2e40895\",\n \"submittingUser\": \"\",\n \"type\": \"investigation\"\n }\n ],\n \"suffix\": \"\",\n \"emailAddress\": \"\",\n \"licenseNumber\": \"\",\n \"investigationStatus\": \"underInvestigation\",\n \"dateOfBirth\": \"1334-02-26\",\n \"ssnLastFour\": \"6387\",\n \"phoneNumber\": \"+1696518778418\",\n \"licenseStatusName\": \"\",\n \"adverseActions\": [\n {\n \"actionAgainst\": \"\",\n \"adverseActionId\": \"\",\n \"compact\": \"socw\",\n \"creationDate\": \"1355-03-20\",\n \"dateOfUpdate\": \"\",\n \"effectiveStartDate\": \"1992-02-03\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"mi\",\n \"licenseScope\": \"multi-state\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"716b8f79-ad53-4e05-a51d-40d2a584e573\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"Confidentiality, Consent or Disclosure Violations\"\n ],\n \"effectiveLiftDate\": \"2353-06-09\",\n \"liftingUser\": \"\"\n },\n {\n \"actionAgainst\": \"\",\n \"adverseActionId\": \"\",\n \"compact\": \"socw\",\n \"creationDate\": \"2598-03-05\",\n \"dateOfUpdate\": \"\",\n \"effectiveStartDate\": \"1824-11-03\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"mi\",\n \"licenseScope\": \"multi-state\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"cb31c040-9f4c-45f1-a2f5-d090468888b5\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"Improper Supervision or Allowing Unlicensed Practice\"\n ],\n \"effectiveLiftDate\": \"2333-10-04\",\n \"liftingUser\": \"\"\n }\n ]\n },\n {\n \"compact\": \"socw\",\n \"compactEligibility\": \"eligible\",\n \"dateOfExpiration\": \"2291-06-31\",\n \"dateOfIssuance\": \"2823-08-13\",\n \"dateOfRenewal\": \"1962-10-12\",\n \"dateOfUpdate\": \"\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"history\": [\n {\n \"compact\": \"socw\",\n \"dateOfUpdate\": \"\",\n \"jurisdiction\": \"nj\",\n \"previous\": {\n \"dateOfExpiration\": \"1539-12-30\",\n \"dateOfIssuance\": \"1581-10-07\",\n \"dateOfRenewal\": \"1853-09-31\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"homeAddressCity\": \"\",\n \"homeAddressPostalCode\": \"\",\n \"homeAddressState\": \"\",\n \"homeAddressStreet1\": \"\",\n \"jurisdictionUploadedCompactEligibility\": \"ineligible\",\n \"jurisdictionUploadedLicenseStatus\": \"inactive\",\n \"middleName\": \"\",\n \"homeAddressStreet2\": \"\",\n \"compactEligibility\": \"ineligible\",\n \"dateOfBirth\": \"2084-12-09\",\n \"suffix\": \"\",\n \"emailAddress\": \"\",\n \"phoneNumber\": \"+663013892274\",\n \"licenseStatus\": \"inactive\",\n \"licenseNumber\": \"\",\n \"licenseStatusName\": \"\",\n \"licenseScope\": \"single-state\"\n },\n \"type\": \"licenseUpdate\",\n \"updateType\": \"expiration\",\n \"removedValues\": [\n \"\",\n \"\"\n ],\n \"licenseType\": \"licensed master social worker\",\n \"updatedValues\": {\n \"homeAddressStreet2\": \"\",\n \"homeAddressPostalCode\": \"\",\n \"givenName\": \"\",\n \"homeAddressStreet1\": \"\",\n \"compactEligibility\": \"ineligible\",\n \"jurisdictionUploadedCompactEligibility\": \"eligible\",\n \"dateOfBirth\": \"1718-01-31\",\n \"jurisdictionUploadedLicenseStatus\": \"inactive\",\n \"suffix\": \"\",\n \"dateOfIssuance\": \"1904-11-24\",\n \"emailAddress\": \"\",\n \"dateOfExpiration\": \"1312-12-30\",\n \"phoneNumber\": \"+1052854884625\",\n \"homeAddressState\": \"\",\n \"dateOfRenewal\": \"1937-01-01\",\n \"licenseStatus\": \"inactive\",\n \"familyName\": \"\",\n \"homeAddressCity\": \"\",\n \"licenseNumber\": \"\",\n \"middleName\": \"\",\n \"licenseStatusName\": \"\",\n \"licenseScope\": \"single-state\"\n }\n },\n {\n \"compact\": \"socw\",\n \"dateOfUpdate\": \"\",\n \"jurisdiction\": \"nm\",\n \"previous\": {\n \"dateOfExpiration\": \"1842-11-31\",\n \"dateOfIssuance\": \"1198-11-02\",\n \"dateOfRenewal\": \"2756-03-08\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"homeAddressCity\": \"\",\n \"homeAddressPostalCode\": \"\",\n \"homeAddressState\": \"\",\n \"homeAddressStreet1\": \"\",\n \"jurisdictionUploadedCompactEligibility\": \"ineligible\",\n \"jurisdictionUploadedLicenseStatus\": \"active\",\n \"middleName\": \"\",\n \"homeAddressStreet2\": \"\",\n \"compactEligibility\": \"ineligible\",\n \"dateOfBirth\": \"2579-05-30\",\n \"suffix\": \"\",\n \"emailAddress\": \"\",\n \"phoneNumber\": \"+234284914921\",\n \"licenseStatus\": \"inactive\",\n \"licenseNumber\": \"\",\n \"licenseStatusName\": \"\",\n \"licenseScope\": \"single-state\"\n },\n \"type\": \"licenseUpdate\",\n \"updateType\": \"lifting_encumbrance\",\n \"removedValues\": [\n \"\",\n \"\"\n ],\n \"licenseType\": \"licensed bachelors social worker\",\n \"updatedValues\": {\n \"homeAddressStreet2\": \"\",\n \"homeAddressPostalCode\": \"\",\n \"givenName\": \"\",\n \"homeAddressStreet1\": \"\",\n \"compactEligibility\": \"eligible\",\n \"jurisdictionUploadedCompactEligibility\": \"eligible\",\n \"dateOfBirth\": \"2616-12-28\",\n \"jurisdictionUploadedLicenseStatus\": \"inactive\",\n \"suffix\": \"\",\n \"dateOfIssuance\": \"1857-08-30\",\n \"emailAddress\": \"\",\n \"dateOfExpiration\": \"1587-10-30\",\n \"phoneNumber\": \"+47428927234081\",\n \"homeAddressState\": \"\",\n \"dateOfRenewal\": \"1624-04-31\",\n \"licenseStatus\": \"inactive\",\n \"familyName\": \"\",\n \"homeAddressCity\": \"\",\n \"licenseNumber\": \"\",\n \"middleName\": \"\",\n \"licenseStatusName\": \"\",\n \"licenseScope\": \"multi-state\"\n }\n }\n ],\n \"homeAddressCity\": \"\",\n \"homeAddressPostalCode\": \"\",\n \"homeAddressState\": \"\",\n \"homeAddressStreet1\": \"\",\n \"jurisdiction\": \"wy\",\n \"jurisdictionUploadedCompactEligibility\": \"ineligible\",\n \"jurisdictionUploadedLicenseStatus\": \"inactive\",\n \"licenseScope\": \"multi-state\",\n \"licenseStatus\": \"active\",\n \"licenseType\": \"licensed master social worker\",\n \"middleName\": \"\",\n \"providerId\": \"a78a7ee2-7337-4e22-8c2d-6120794e798a\",\n \"type\": \"license-home\",\n \"homeAddressStreet2\": \"\",\n \"investigations\": [\n {\n \"compact\": \"socw\",\n \"creationDate\": \"\",\n \"dateOfUpdate\": \"\",\n \"investigationId\": \"\",\n \"jurisdiction\": \"nh\",\n \"licenseScope\": \"multi-state\",\n \"licenseType\": \"\",\n \"providerId\": \"e44eb7ab-67a8-4277-bac1-ccfeaf064bcd\",\n \"submittingUser\": \"\",\n \"type\": \"investigation\"\n },\n {\n \"compact\": \"socw\",\n \"creationDate\": \"\",\n \"dateOfUpdate\": \"\",\n \"investigationId\": \"\",\n \"jurisdiction\": \"ct\",\n \"licenseScope\": \"single-state\",\n \"licenseType\": \"\",\n \"providerId\": \"ecbe623c-c720-4456-acfb-66df63d8c342\",\n \"submittingUser\": \"\",\n \"type\": \"investigation\"\n }\n ],\n \"suffix\": \"\",\n \"emailAddress\": \"\",\n \"licenseNumber\": \"\",\n \"investigationStatus\": \"underInvestigation\",\n \"dateOfBirth\": \"1976-10-31\",\n \"ssnLastFour\": \"7065\",\n \"phoneNumber\": \"+9020793773\",\n \"licenseStatusName\": \"\",\n \"adverseActions\": [\n {\n \"actionAgainst\": \"\",\n \"adverseActionId\": \"\",\n \"compact\": \"socw\",\n \"creationDate\": \"2689-12-05\",\n \"dateOfUpdate\": \"\",\n \"effectiveStartDate\": \"2953-01-01\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"ks\",\n \"licenseScope\": \"multi-state\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"f95a6a85-8fc7-4b08-a7c6-8d7415248372\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"Conflict of Interest\"\n ],\n \"effectiveLiftDate\": \"2089-07-14\",\n \"liftingUser\": \"\"\n },\n {\n \"actionAgainst\": \"\",\n \"adverseActionId\": \"\",\n \"compact\": \"socw\",\n \"creationDate\": \"1428-07-31\",\n \"dateOfUpdate\": \"\",\n \"effectiveStartDate\": \"2128-10-04\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"mt\",\n \"licenseScope\": \"single-state\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"2c0a10d6-1359-445d-9add-a0144d263746\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"Criminal Conviction or Adjudication\"\n ],\n \"effectiveLiftDate\": \"1184-11-30\",\n \"liftingUser\": \"\"\n }\n ]\n }\n ],\n \"privileges\": [\n {\n \"administratorSetStatus\": \"active\",\n \"compact\": \"socw\",\n \"compactTransactionId\": \"\",\n \"dateOfExpiration\": \"1140-09-31\",\n \"history\": [\n {\n \"compact\": \"socw\",\n \"dateOfUpdate\": \"\",\n \"jurisdiction\": \"nd\",\n \"previous\": {\n \"administratorSetStatus\": \"active\",\n \"compactTransactionId\": \"\",\n \"dateOfExpiration\": \"2311-09-08\",\n \"licenseJurisdiction\": \"hi\",\n \"compact\": \"socw\",\n \"providerId\": \"ef986755-7305-48ef-b343-8fd9cf3cbea6\",\n \"jurisdiction\": \"vt\",\n \"type\": \"privilege\",\n \"status\": \"inactive\"\n },\n \"type\": \"privilegeUpdate\",\n \"updateType\": \"other\",\n \"removedValues\": [\n \"\",\n \"\"\n ],\n \"licenseType\": \"licensed clinical social worker\",\n \"updatedValues\": {\n \"administratorSetStatus\": \"active\",\n \"dateOfExpiration\": \"2645-05-18\",\n \"licenseJurisdiction\": \"de\",\n \"compact\": \"socw\",\n \"providerId\": \"d2a90012-8ec5-4b1a-9dee-d50679881235\",\n \"jurisdiction\": \"ct\",\n \"type\": \"privilege\",\n \"compactTransactionId\": \"\",\n \"status\": \"active\"\n }\n },\n {\n \"compact\": \"socw\",\n \"dateOfUpdate\": \"\",\n \"jurisdiction\": \"nc\",\n \"previous\": {\n \"administratorSetStatus\": \"inactive\",\n \"compactTransactionId\": \"\",\n \"dateOfExpiration\": \"2288-03-31\",\n \"licenseJurisdiction\": \"nm\",\n \"compact\": \"socw\",\n \"providerId\": \"b4837c96-e2f1-42ee-8898-c892eb44fcc4\",\n \"jurisdiction\": \"wa\",\n \"type\": \"privilege\",\n \"status\": \"inactive\"\n },\n \"type\": \"privilegeUpdate\",\n \"updateType\": \"renewal\",\n \"removedValues\": [\n \"\",\n \"\"\n ],\n \"licenseType\": \"licensed bachelors social worker\",\n \"updatedValues\": {\n \"administratorSetStatus\": \"inactive\",\n \"dateOfExpiration\": \"2084-12-23\",\n \"licenseJurisdiction\": \"wa\",\n \"compact\": \"socw\",\n \"providerId\": \"a8effaa1-a0d8-46d9-93e6-dd111826f0c9\",\n \"jurisdiction\": \"fl\",\n \"type\": \"privilege\",\n \"compactTransactionId\": \"\",\n \"status\": \"active\"\n }\n }\n ],\n \"jurisdiction\": \"az\",\n \"licenseJurisdiction\": \"ri\",\n \"licenseType\": \"licensed bachelors social worker\",\n \"providerId\": \"ccf5ae1f-1855-4395-85df-add6acf6c624\",\n \"status\": \"inactive\",\n \"type\": \"privilege\",\n \"investigationStatus\": \"underInvestigation\",\n \"investigations\": [\n {\n \"compact\": \"socw\",\n \"creationDate\": \"\",\n \"dateOfUpdate\": \"\",\n \"investigationId\": \"\",\n \"jurisdiction\": \"ky\",\n \"licenseScope\": \"multi-state\",\n \"licenseType\": \"\",\n \"providerId\": \"0d56d43a-6bb0-4243-8369-35e8a3f3e074\",\n \"submittingUser\": \"\",\n \"type\": \"investigation\"\n },\n {\n \"compact\": \"socw\",\n \"creationDate\": \"\",\n \"dateOfUpdate\": \"\",\n \"investigationId\": \"\",\n \"jurisdiction\": \"va\",\n \"licenseScope\": \"single-state\",\n \"licenseType\": \"\",\n \"providerId\": \"19d064b0-f2c8-48cf-bdef-be4f6565015a\",\n \"submittingUser\": \"\",\n \"type\": \"investigation\"\n }\n ],\n \"adverseActions\": [\n {\n \"actionAgainst\": \"\",\n \"adverseActionId\": \"\",\n \"compact\": \"socw\",\n \"creationDate\": \"1897-12-11\",\n \"dateOfUpdate\": \"\",\n \"effectiveStartDate\": \"2140-11-03\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"ct\",\n \"licenseScope\": \"single-state\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"1a55d5d7-a9c3-4c89-b313-a70240d743bc\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"Conflict of Interest\"\n ],\n \"effectiveLiftDate\": \"1451-12-30\",\n \"liftingUser\": \"\"\n },\n {\n \"actionAgainst\": \"\",\n \"adverseActionId\": \"\",\n \"compact\": \"socw\",\n \"creationDate\": \"2543-01-31\",\n \"dateOfUpdate\": \"\",\n \"effectiveStartDate\": \"1819-11-30\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"ok\",\n \"licenseScope\": \"multi-state\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"20815f70-682c-4377-b4b2-b673f954f9c3\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"Improper Prescribing, Dispensing, Administering Medication/Drug Violation\"\n ],\n \"effectiveLiftDate\": \"2440-12-25\",\n \"liftingUser\": \"\"\n }\n ]\n },\n {\n \"administratorSetStatus\": \"active\",\n \"compact\": \"socw\",\n \"compactTransactionId\": \"\",\n \"dateOfExpiration\": \"1825-02-20\",\n \"history\": [\n {\n \"compact\": \"socw\",\n \"dateOfUpdate\": \"\",\n \"jurisdiction\": \"nd\",\n \"previous\": {\n \"administratorSetStatus\": \"inactive\",\n \"compactTransactionId\": \"\",\n \"dateOfExpiration\": \"2356-10-22\",\n \"licenseJurisdiction\": \"in\",\n \"compact\": \"socw\",\n \"providerId\": \"abe0bbaf-d2bc-428e-a948-488aa851fb06\",\n \"jurisdiction\": \"gu\",\n \"type\": \"privilege\",\n \"status\": \"active\"\n },\n \"type\": \"privilegeUpdate\",\n \"updateType\": \"licenseDeactivation\",\n \"removedValues\": [\n \"\",\n \"\"\n ],\n \"licenseType\": \"licensed bachelors social worker\",\n \"updatedValues\": {\n \"administratorSetStatus\": \"inactive\",\n \"dateOfExpiration\": \"1064-12-26\",\n \"licenseJurisdiction\": \"ga\",\n \"compact\": \"socw\",\n \"providerId\": \"8c87db0a-be43-4512-b9de-e277343a240e\",\n \"jurisdiction\": \"de\",\n \"type\": \"privilege\",\n \"compactTransactionId\": \"\",\n \"status\": \"active\"\n }\n },\n {\n \"compact\": \"socw\",\n \"dateOfUpdate\": \"\",\n \"jurisdiction\": \"nh\",\n \"previous\": {\n \"administratorSetStatus\": \"inactive\",\n \"compactTransactionId\": \"\",\n \"dateOfExpiration\": \"2673-08-14\",\n \"licenseJurisdiction\": \"ca\",\n \"compact\": \"socw\",\n \"providerId\": \"744e3aad-6d92-4a08-8f9e-f2708eda78bd\",\n \"jurisdiction\": \"nd\",\n \"type\": \"privilege\",\n \"status\": \"active\"\n },\n \"type\": \"privilegeUpdate\",\n \"updateType\": \"encumbrance\",\n \"removedValues\": [\n \"\",\n \"\"\n ],\n \"licenseType\": \"licensed bachelors social worker\",\n \"updatedValues\": {\n \"administratorSetStatus\": \"active\",\n \"dateOfExpiration\": \"1402-02-06\",\n \"licenseJurisdiction\": \"sd\",\n \"compact\": \"socw\",\n \"providerId\": \"67ef7699-b5ef-41d3-83c6-cbbcd2e20dc5\",\n \"jurisdiction\": \"sc\",\n \"type\": \"privilege\",\n \"compactTransactionId\": \"\",\n \"status\": \"inactive\"\n }\n }\n ],\n \"jurisdiction\": \"nj\",\n \"licenseJurisdiction\": \"la\",\n \"licenseType\": \"licensed bachelors social worker\",\n \"providerId\": \"e612e436-bc6b-448a-b7d8-0afdb1bb08ab\",\n \"status\": \"inactive\",\n \"type\": \"privilege\",\n \"investigationStatus\": \"underInvestigation\",\n \"investigations\": [\n {\n \"compact\": \"socw\",\n \"creationDate\": \"\",\n \"dateOfUpdate\": \"\",\n \"investigationId\": \"\",\n \"jurisdiction\": \"vi\",\n \"licenseScope\": \"multi-state\",\n \"licenseType\": \"\",\n \"providerId\": \"8d992991-c726-4d43-88bc-aab211b0e6e5\",\n \"submittingUser\": \"\",\n \"type\": \"investigation\"\n },\n {\n \"compact\": \"socw\",\n \"creationDate\": \"\",\n \"dateOfUpdate\": \"\",\n \"investigationId\": \"\",\n \"jurisdiction\": \"ut\",\n \"licenseScope\": \"multi-state\",\n \"licenseType\": \"\",\n \"providerId\": \"06bcd8e5-b6e3-4e55-862e-7c6bcfcd122a\",\n \"submittingUser\": \"\",\n \"type\": \"investigation\"\n }\n ],\n \"adverseActions\": [\n {\n \"actionAgainst\": \"\",\n \"adverseActionId\": \"\",\n \"compact\": \"socw\",\n \"creationDate\": \"2943-12-31\",\n \"dateOfUpdate\": \"\",\n \"effectiveStartDate\": \"1105-10-02\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"oh\",\n \"licenseScope\": \"single-state\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"3b28688b-e262-4ee9-b3d0-4c6254528ec2\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"Other\"\n ],\n \"effectiveLiftDate\": \"1894-07-26\",\n \"liftingUser\": \"\"\n },\n {\n \"actionAgainst\": \"\",\n \"adverseActionId\": \"\",\n \"compact\": \"socw\",\n \"creationDate\": \"2368-01-30\",\n \"dateOfUpdate\": \"\",\n \"effectiveStartDate\": \"2461-11-06\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"il\",\n \"licenseScope\": \"multi-state\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"82592313-0bbd-4ac9-b5bd-88e42fd25678\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"Other\"\n ],\n \"effectiveLiftDate\": \"2281-02-29\",\n \"liftingUser\": \"\"\n }\n ]\n }\n ],\n \"providerId\": \"926c97f0-74a8-4eaf-a083-0089b63b4b52\",\n \"type\": \"provider\",\n \"compactEligibility\": \"eligible\",\n \"jurisdictionUploadedCompactEligibility\": \"ineligible\",\n \"dateOfBirth\": \"1723-05-04\",\n \"jurisdictionUploadedLicenseStatus\": \"inactive\",\n \"suffix\": \"\",\n \"publicCompactIdentifier\": \"sWC-2839-8\",\n \"ssnLastFour\": \"7149\",\n \"licenseStatus\": \"active\",\n \"middleName\": \"\"\n}", "code": 200, "cookie": [], "header": [ @@ -1286,7 +1286,7 @@ "value": "application/json" } ], - "id": "64f28134-2908-4e10-b840-22588466005e", + "id": "cb40380c-c40f-4d2f-b505-e767ed7ef6e0", "name": "200 response", "originalRequest": { "body": {}, @@ -1344,7 +1344,7 @@ "item": [ { "event": [], - "id": "9652079a-9107-4da4-ac79-9b5ba8869042", + "id": "b5cf4cb4-16d8-4cfa-ae8a-9b03d3796be7", "name": "/v1/compacts/:compact/providers/:providerId/licenses/jurisdiction/:jurisdiction/licenseType/:licenseType/encumbrance", "protocolProfileBehavior": { "disableBodyPruning": true @@ -1358,7 +1358,7 @@ "language": "json" } }, - "raw": "{\n \"clinicalPrivilegeActionCategories\": [\n \"Substandard Care or Patient Neglect/Abuse\",\n \"Fraud, Deception, or Misrepresentation\"\n ],\n \"encumbranceEffectiveDate\": \"2858-05-18\",\n \"encumbranceType\": \"revocation\",\n \"licenseScope\": \"multi-state\"\n}" + "raw": "{\n \"clinicalPrivilegeActionCategories\": [\n \"Improper Prescribing, Dispensing, Administering Medication/Drug Violation\"\n ],\n \"encumbranceEffectiveDate\": \"1732-05-09\",\n \"encumbranceType\": \"injunctive action\",\n \"licenseScope\": \"multi-state\"\n}" }, "description": {}, "header": [ @@ -1447,7 +1447,7 @@ "value": "application/json" } ], - "id": "3e594aa4-eb26-4784-9642-3e024a4fec47", + "id": "e5708e1b-790c-4ff2-bf73-c3165a5d0e6e", "name": "200 response", "originalRequest": { "body": { @@ -1458,7 +1458,7 @@ "language": "json" } }, - "raw": "{\n \"clinicalPrivilegeActionCategories\": [\n \"Substandard Care or Patient Neglect/Abuse\",\n \"Fraud, Deception, or Misrepresentation\"\n ],\n \"encumbranceEffectiveDate\": \"2858-05-18\",\n \"encumbranceType\": \"revocation\",\n \"licenseScope\": \"multi-state\"\n}" + "raw": "{\n \"clinicalPrivilegeActionCategories\": [\n \"Improper Prescribing, Dispensing, Administering Medication/Drug Violation\"\n ],\n \"encumbranceEffectiveDate\": \"1732-05-09\",\n \"encumbranceType\": \"injunctive action\",\n \"licenseScope\": \"multi-state\"\n}" }, "header": [ { @@ -1509,7 +1509,7 @@ "item": [ { "event": [], - "id": "be29cb7f-f406-4a13-935a-234c6973b77f", + "id": "94a71096-3f9f-47d1-a07f-9ed90f74bc9e", "name": "/v1/compacts/:compact/providers/:providerId/licenses/jurisdiction/:jurisdiction/licenseType/:licenseType/encumbrance/:encumbranceId", "protocolProfileBehavior": { "disableBodyPruning": true @@ -1523,7 +1523,7 @@ "language": "json" } }, - "raw": "{\n \"effectiveLiftDate\": \"2931-12-27\",\n \"licenseScope\": \"multi-state\"\n}" + "raw": "{\n \"effectiveLiftDate\": \"1571-11-29\",\n \"licenseScope\": \"single-state\"\n}" }, "description": {}, "header": [ @@ -1623,7 +1623,7 @@ "value": "application/json" } ], - "id": "bea1d91c-f6bc-4dad-9038-7e6067c68324", + "id": "f31999a6-df5b-42ab-8340-89a7a014a20c", "name": "200 response", "originalRequest": { "body": { @@ -1634,7 +1634,7 @@ "language": "json" } }, - "raw": "{\n \"effectiveLiftDate\": \"2931-12-27\",\n \"licenseScope\": \"multi-state\"\n}" + "raw": "{\n \"effectiveLiftDate\": \"1571-11-29\",\n \"licenseScope\": \"single-state\"\n}" }, "header": [ { @@ -1692,7 +1692,7 @@ "item": [ { "event": [], - "id": "bf2d9a99-2fbf-4af3-8483-3f541db13248", + "id": "da0d252c-bdb3-470b-ac76-73f68494f4bb", "name": "/v1/compacts/:compact/providers/:providerId/licenses/jurisdiction/:jurisdiction/licenseType/:licenseType/investigation", "protocolProfileBehavior": { "disableBodyPruning": true @@ -1795,7 +1795,7 @@ "value": "application/json" } ], - "id": "1bc70528-c823-410b-8cf6-5b1cd3666ac6", + "id": "98a84621-cf65-4e95-8d97-d8e85ef51b2f", "name": "200 response", "originalRequest": { "body": { @@ -1857,7 +1857,7 @@ "item": [ { "event": [], - "id": "c4e52a42-1b19-430f-8fbf-dabb4d177d75", + "id": "ab852721-d6f3-4d70-aec0-a9c516697bfc", "name": "/v1/compacts/:compact/providers/:providerId/licenses/jurisdiction/:jurisdiction/licenseType/:licenseType/investigation/:investigationId", "protocolProfileBehavior": { "disableBodyPruning": true @@ -1871,7 +1871,7 @@ "language": "json" } }, - "raw": "{\n \"action\": \"close\",\n \"licenseScope\": \"multi-state\",\n \"encumbrance\": {\n \"clinicalPrivilegeActionCategories\": [\n \"Substandard Care or Patient Neglect/Abuse\",\n \"Fraud, Deception, or Misrepresentation\"\n ],\n \"encumbranceEffectiveDate\": \"1397-09-25\",\n \"encumbranceType\": \"surrender of license\",\n \"licenseScope\": \"multi-state\"\n }\n}" + "raw": "{\n \"action\": \"close\",\n \"licenseScope\": \"single-state\",\n \"encumbrance\": {\n \"clinicalPrivilegeActionCategories\": [\n \"Improper Prescribing, Dispensing, Administering Medication/Drug Violation\"\n ],\n \"encumbranceEffectiveDate\": \"2869-05-04\",\n \"encumbranceType\": \"reprimand\",\n \"licenseScope\": \"single-state\"\n }\n}" }, "description": {}, "header": [ @@ -1971,7 +1971,7 @@ "value": "application/json" } ], - "id": "45982676-25e9-41af-a43a-43bd0f0d7258", + "id": "835191d9-9656-4a4e-8823-db1c241b349e", "name": "200 response", "originalRequest": { "body": { @@ -1982,7 +1982,7 @@ "language": "json" } }, - "raw": "{\n \"action\": \"close\",\n \"licenseScope\": \"multi-state\",\n \"encumbrance\": {\n \"clinicalPrivilegeActionCategories\": [\n \"Substandard Care or Patient Neglect/Abuse\",\n \"Fraud, Deception, or Misrepresentation\"\n ],\n \"encumbranceEffectiveDate\": \"1397-09-25\",\n \"encumbranceType\": \"surrender of license\",\n \"licenseScope\": \"multi-state\"\n }\n}" + "raw": "{\n \"action\": \"close\",\n \"licenseScope\": \"single-state\",\n \"encumbrance\": {\n \"clinicalPrivilegeActionCategories\": [\n \"Improper Prescribing, Dispensing, Administering Medication/Drug Violation\"\n ],\n \"encumbranceEffectiveDate\": \"2869-05-04\",\n \"encumbranceType\": \"reprimand\",\n \"licenseScope\": \"single-state\"\n }\n}" }, "header": [ { @@ -2070,7 +2070,7 @@ "item": [ { "event": [], - "id": "1032d12b-e102-47a0-bee3-ff48f3f19fa0", + "id": "b6dfd87b-34e4-441b-a5b5-95cd221b8cb1", "name": "/v1/compacts/:compact/providers/:providerId/privileges/jurisdiction/:jurisdiction/licenseType/:licenseType/encumbrance", "protocolProfileBehavior": { "disableBodyPruning": true @@ -2084,7 +2084,7 @@ "language": "json" } }, - "raw": "{\n \"clinicalPrivilegeActionCategories\": [\n \"Fraud, Deception, or Misrepresentation\",\n \"Substandard Care or Patient Neglect/Abuse\"\n ],\n \"encumbranceEffectiveDate\": \"2816-09-30\",\n \"encumbranceType\": \"suspension\"\n}" + "raw": "{\n \"clinicalPrivilegeActionCategories\": [\n \"Non-Compliance With Requirements\"\n ],\n \"encumbranceEffectiveDate\": \"1606-12-08\",\n \"encumbranceType\": \"injunctive action\"\n}" }, "description": {}, "header": [ @@ -2173,7 +2173,7 @@ "value": "application/json" } ], - "id": "3ee87360-cdda-49bd-bb17-a2ce19a08972", + "id": "04b3e7fa-10c9-4b7f-b4bc-ba26b6e366c7", "name": "200 response", "originalRequest": { "body": { @@ -2184,7 +2184,7 @@ "language": "json" } }, - "raw": "{\n \"clinicalPrivilegeActionCategories\": [\n \"Fraud, Deception, or Misrepresentation\",\n \"Substandard Care or Patient Neglect/Abuse\"\n ],\n \"encumbranceEffectiveDate\": \"2816-09-30\",\n \"encumbranceType\": \"suspension\"\n}" + "raw": "{\n \"clinicalPrivilegeActionCategories\": [\n \"Non-Compliance With Requirements\"\n ],\n \"encumbranceEffectiveDate\": \"1606-12-08\",\n \"encumbranceType\": \"injunctive action\"\n}" }, "header": [ { @@ -2235,7 +2235,7 @@ "item": [ { "event": [], - "id": "c80aa8bd-b829-48ca-bb2e-72f70bc60917", + "id": "1b3bdac9-d47a-4b35-92c5-f68b9c666eb9", "name": "/v1/compacts/:compact/providers/:providerId/privileges/jurisdiction/:jurisdiction/licenseType/:licenseType/encumbrance/:encumbranceId", "protocolProfileBehavior": { "disableBodyPruning": true @@ -2249,7 +2249,7 @@ "language": "json" } }, - "raw": "{\n \"effectiveLiftDate\": \"1149-01-04\"\n}" + "raw": "{\n \"effectiveLiftDate\": \"2280-11-24\"\n}" }, "description": {}, "header": [ @@ -2349,7 +2349,7 @@ "value": "application/json" } ], - "id": "92d05358-2ed7-4b85-b10c-a52c04c4319e", + "id": "ecdb6a8e-ecc9-4989-b874-ecd86cd8250e", "name": "200 response", "originalRequest": { "body": { @@ -2360,7 +2360,7 @@ "language": "json" } }, - "raw": "{\n \"effectiveLiftDate\": \"1149-01-04\"\n}" + "raw": "{\n \"effectiveLiftDate\": \"2280-11-24\"\n}" }, "header": [ { @@ -2418,7 +2418,7 @@ "item": [ { "event": [], - "id": "bb7e82c9-35a0-4639-a7cf-6f0920388fa3", + "id": "80e24fea-e9da-47a3-bb15-1eec966ef6b3", "name": "/v1/compacts/:compact/providers/:providerId/privileges/jurisdiction/:jurisdiction/licenseType/:licenseType/investigation", "protocolProfileBehavior": { "disableBodyPruning": true @@ -2521,7 +2521,7 @@ "value": "application/json" } ], - "id": "5d241070-ea35-4a36-b43a-36970e91d212", + "id": "5fc2b665-db44-4114-a4bd-10ef7904aa44", "name": "200 response", "originalRequest": { "body": { @@ -2583,7 +2583,7 @@ "item": [ { "event": [], - "id": "eb3b8c5f-e070-41bb-b825-050c64947145", + "id": "fbb24555-6f68-4050-bdcf-dbf2fa297493", "name": "/v1/compacts/:compact/providers/:providerId/privileges/jurisdiction/:jurisdiction/licenseType/:licenseType/investigation/:investigationId", "protocolProfileBehavior": { "disableBodyPruning": true @@ -2597,7 +2597,7 @@ "language": "json" } }, - "raw": "{\n \"action\": \"close\",\n \"encumbrance\": {\n \"clinicalPrivilegeActionCategories\": [\n \"Fraud, Deception, or Misrepresentation\",\n \"Other\"\n ],\n \"encumbranceEffectiveDate\": \"2234-12-18\",\n \"encumbranceType\": \"surrender of license\"\n }\n}" + "raw": "{\n \"action\": \"close\",\n \"encumbrance\": {\n \"clinicalPrivilegeActionCategories\": [\n \"Confidentiality, Consent or Disclosure Violations\"\n ],\n \"encumbranceEffectiveDate\": \"2966-03-05\",\n \"encumbranceType\": \"injunctive action\"\n }\n}" }, "description": {}, "header": [ @@ -2697,7 +2697,7 @@ "value": "application/json" } ], - "id": "0053a0ea-9ca1-49b4-9bb7-42d69c0d57ef", + "id": "7d8fa435-b63a-4f8f-9cdd-5ab21ed84098", "name": "200 response", "originalRequest": { "body": { @@ -2708,7 +2708,7 @@ "language": "json" } }, - "raw": "{\n \"action\": \"close\",\n \"encumbrance\": {\n \"clinicalPrivilegeActionCategories\": [\n \"Fraud, Deception, or Misrepresentation\",\n \"Other\"\n ],\n \"encumbranceEffectiveDate\": \"2234-12-18\",\n \"encumbranceType\": \"surrender of license\"\n }\n}" + "raw": "{\n \"action\": \"close\",\n \"encumbrance\": {\n \"clinicalPrivilegeActionCategories\": [\n \"Confidentiality, Consent or Disclosure Violations\"\n ],\n \"encumbranceEffectiveDate\": \"2966-03-05\",\n \"encumbranceType\": \"injunctive action\"\n }\n}" }, "header": [ { @@ -2787,7 +2787,7 @@ "item": [ { "event": [], - "id": "37432c9f-9e7c-4950-ab50-c57ea47f76dd", + "id": "0a49429c-badc-44c3-ab84-b6f59dcd16b9", "name": "/v1/compacts/:compact/staff-users", "protocolProfileBehavior": { "disableBodyPruning": true @@ -2831,7 +2831,7 @@ "response": [ { "_postman_previewlanguage": "json", - "body": "{\n \"pagination\": {\n \"prevLastKey\": {},\n \"lastKey\": {},\n \"pageSize\": \"\"\n },\n \"users\": [\n {\n \"attributes\": {\n \"email\": \"\",\n \"familyName\": \"\",\n \"givenName\": \"\"\n },\n \"permissions\": {\n \"ut7\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\"\n },\n \"jurisdictions\": {\n \"qui_ebf\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"non9d9\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"elit4c8\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"Ut0\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n }\n }\n },\n \"sunt6c\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\"\n },\n \"jurisdictions\": {\n \"Ut_95\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n }\n }\n }\n },\n \"status\": \"active\",\n \"userId\": \"\"\n },\n {\n \"attributes\": {\n \"email\": \"\",\n \"familyName\": \"\",\n \"givenName\": \"\"\n },\n \"permissions\": {\n \"quis77\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\"\n },\n \"jurisdictions\": {\n \"mollit2\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n }\n }\n },\n \"aliqua_817\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\"\n },\n \"jurisdictions\": {\n \"sed2\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"consectetur92d\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"ex_a4\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n }\n }\n }\n },\n \"status\": \"inactive\",\n \"userId\": \"\"\n }\n ]\n}", + "body": "{\n \"pagination\": {\n \"prevLastKey\": {},\n \"lastKey\": {},\n \"pageSize\": \"\"\n },\n \"users\": [\n {\n \"attributes\": {\n \"email\": \"\",\n \"familyName\": \"\",\n \"givenName\": \"\"\n },\n \"permissions\": {\n \"eu_9\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\"\n },\n \"jurisdictions\": {\n \"occaecat_d70\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"amet8a2\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"anim6\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n }\n }\n },\n \"elit_4de\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\"\n },\n \"jurisdictions\": {\n \"dolore2c\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"ea_9\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n }\n }\n },\n \"in730\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\"\n },\n \"jurisdictions\": {\n \"nostrud4c\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"essea\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"elit_952\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"velit_05\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"eaf04\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n }\n }\n }\n },\n \"status\": \"inactive\",\n \"userId\": \"\"\n },\n {\n \"attributes\": {\n \"email\": \"\",\n \"familyName\": \"\",\n \"givenName\": \"\"\n },\n \"permissions\": {\n \"laborum_f71\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\"\n },\n \"jurisdictions\": {\n \"adipisicing_4\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"amet_f1\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n }\n }\n }\n },\n \"status\": \"active\",\n \"userId\": \"\"\n }\n ]\n}", "code": 200, "cookie": [], "header": [ @@ -2849,7 +2849,7 @@ "value": "" } ], - "id": "2c64061b-41f9-45cc-8ba7-567b9acf7106", + "id": "ede25810-2d85-4be7-8906-7c268ab9f45a", "name": "200 response", "originalRequest": { "body": {}, @@ -2888,7 +2888,7 @@ }, { "event": [], - "id": "93545f3a-b57a-4924-896a-60a19cb74692", + "id": "232ec384-b54a-4ed5-8275-54da4f7619c8", "name": "/v1/compacts/:compact/staff-users", "protocolProfileBehavior": { "disableBodyPruning": true @@ -2902,7 +2902,7 @@ "language": "json" } }, - "raw": "{\n \"attributes\": {\n \"email\": \"\",\n \"familyName\": \"\",\n \"givenName\": \"\"\n },\n \"permissions\": {\n \"laborum_aa\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\"\n },\n \"jurisdictions\": {\n \"ut_2\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"sint1b\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"etf6\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n }\n }\n },\n \"velit165\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\"\n },\n \"jurisdictions\": {\n \"in_04f\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n }\n }\n },\n \"elita2e\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\"\n },\n \"jurisdictions\": {\n \"elit58_\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n }\n }\n }\n }\n}" + "raw": "{\n \"attributes\": {\n \"email\": \"\",\n \"familyName\": \"\",\n \"givenName\": \"\"\n },\n \"permissions\": {\n \"dolor_9e_\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\"\n },\n \"jurisdictions\": {\n \"do_fa\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"eiusmod068\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n }\n }\n }\n }\n}" }, "description": {}, "header": [ @@ -2945,7 +2945,7 @@ "response": [ { "_postman_previewlanguage": "json", - "body": "{\n \"attributes\": {\n \"email\": \"\",\n \"familyName\": \"\",\n \"givenName\": \"\"\n },\n \"permissions\": {\n \"sit_94\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\"\n },\n \"jurisdictions\": {\n \"sunt0d5\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"nulla_a4\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"ullamco8e\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n }\n }\n },\n \"id384\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\"\n },\n \"jurisdictions\": {\n \"in1a\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"sed_44d\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n }\n }\n }\n },\n \"status\": \"inactive\",\n \"userId\": \"\"\n}", + "body": "{\n \"attributes\": {\n \"email\": \"\",\n \"familyName\": \"\",\n \"givenName\": \"\"\n },\n \"permissions\": {\n \"reprehenderit_1e\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\"\n },\n \"jurisdictions\": {\n \"non86\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"nostrud65\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"aute8b\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"velit90\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n }\n }\n },\n \"enim_9\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\"\n },\n \"jurisdictions\": {\n \"irure_15\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"magna_f\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"Excepteur8\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"amet7\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n }\n }\n },\n \"in__82\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\"\n },\n \"jurisdictions\": {\n \"irure_6\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"officiad72\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"consequat_53\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n }\n }\n }\n },\n \"status\": \"inactive\",\n \"userId\": \"\"\n}", "code": 200, "cookie": [], "header": [ @@ -2963,7 +2963,7 @@ "value": "" } ], - "id": "cd5c5a38-eec8-48ee-8dca-2e935675e509", + "id": "87dd9621-6835-41bf-b782-36b48c451a1d", "name": "200 response", "originalRequest": { "body": { @@ -2974,7 +2974,7 @@ "language": "json" } }, - "raw": "{\n \"attributes\": {\n \"email\": \"\",\n \"familyName\": \"\",\n \"givenName\": \"\"\n },\n \"permissions\": {\n \"laborum_aa\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\"\n },\n \"jurisdictions\": {\n \"ut_2\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"sint1b\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"etf6\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n }\n }\n },\n \"velit165\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\"\n },\n \"jurisdictions\": {\n \"in_04f\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n }\n }\n },\n \"elita2e\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\"\n },\n \"jurisdictions\": {\n \"elit58_\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n }\n }\n }\n }\n}" + "raw": "{\n \"attributes\": {\n \"email\": \"\",\n \"familyName\": \"\",\n \"givenName\": \"\"\n },\n \"permissions\": {\n \"dolor_9e_\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\"\n },\n \"jurisdictions\": {\n \"do_fa\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"eiusmod068\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n }\n }\n }\n }\n}" }, "header": [ { @@ -3018,7 +3018,7 @@ "item": [ { "event": [], - "id": "595cf74d-4899-4455-b8ec-1d28a3e92f7d", + "id": "ee32c636-61a7-499a-be98-cc6611e8e33d", "name": "/v1/compacts/:compact/staff-users/:userId", "protocolProfileBehavior": { "disableBodyPruning": true @@ -3073,7 +3073,7 @@ "response": [ { "_postman_previewlanguage": "json", - "body": "{\n \"attributes\": {\n \"email\": \"\",\n \"familyName\": \"\",\n \"givenName\": \"\"\n },\n \"permissions\": {\n \"sit_94\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\"\n },\n \"jurisdictions\": {\n \"sunt0d5\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"nulla_a4\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"ullamco8e\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n }\n }\n },\n \"id384\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\"\n },\n \"jurisdictions\": {\n \"in1a\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"sed_44d\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n }\n }\n }\n },\n \"status\": \"inactive\",\n \"userId\": \"\"\n}", + "body": "{\n \"attributes\": {\n \"email\": \"\",\n \"familyName\": \"\",\n \"givenName\": \"\"\n },\n \"permissions\": {\n \"reprehenderit_1e\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\"\n },\n \"jurisdictions\": {\n \"non86\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"nostrud65\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"aute8b\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"velit90\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n }\n }\n },\n \"enim_9\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\"\n },\n \"jurisdictions\": {\n \"irure_15\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"magna_f\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"Excepteur8\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"amet7\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n }\n }\n },\n \"in__82\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\"\n },\n \"jurisdictions\": {\n \"irure_6\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"officiad72\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"consequat_53\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n }\n }\n }\n },\n \"status\": \"inactive\",\n \"userId\": \"\"\n}", "code": 200, "cookie": [], "header": [ @@ -3091,7 +3091,7 @@ "value": "" } ], - "id": "bd00fb70-48c5-4d65-9d57-51adf0f8d051", + "id": "0f3f2ae8-c2c0-44dc-a88e-e8c10a5f12cf", "name": "200 response", "originalRequest": { "body": {}, @@ -3138,7 +3138,7 @@ "value": "application/json" } ], - "id": "f0a3bec6-11a7-4464-b63c-b05c7f041195", + "id": "8c932c06-ccaa-4bb9-a7c3-a21d03165676", "name": "404 response", "originalRequest": { "body": {}, @@ -3178,7 +3178,7 @@ }, { "event": [], - "id": "207b7770-3524-4f40-a69a-75a584d14b4f", + "id": "4bb7f3e2-caf4-46f5-bead-68bf7ea79f16", "name": "/v1/compacts/:compact/staff-users/:userId", "protocolProfileBehavior": { "disableBodyPruning": true @@ -3242,7 +3242,7 @@ "value": "application/json" } ], - "id": "8a9ffa4f-c47d-4dd2-9dce-69050721d97d", + "id": "243e8dd8-70b6-4ddb-99ad-a845fa313666", "name": "200 response", "originalRequest": { "body": {}, @@ -3289,7 +3289,7 @@ "value": "application/json" } ], - "id": "39f517fd-836a-4322-bc9a-3d4d6b6615e2", + "id": "771db4b9-52a2-4054-8238-191424efd1b5", "name": "404 response", "originalRequest": { "body": {}, @@ -3329,7 +3329,7 @@ }, { "event": [], - "id": "91c11caf-df34-41b5-bb3c-5d2f2ed90615", + "id": "5bb04943-040c-4761-9bb0-011e3ab42732", "name": "/v1/compacts/:compact/staff-users/:userId", "protocolProfileBehavior": { "disableBodyPruning": true @@ -3343,7 +3343,7 @@ "language": "json" } }, - "raw": "{\n \"permissions\": {\n \"eac3\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\"\n },\n \"jurisdictions\": {\n \"ad_0_6\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"culpa4d3\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n }\n }\n },\n \"aliqua6d\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\"\n },\n \"jurisdictions\": {\n \"eu_4\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n }\n }\n },\n \"mollit_b9f\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\"\n },\n \"jurisdictions\": {\n \"mollit87e\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"consequate\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n }\n }\n }\n }\n}" + "raw": "{\n \"permissions\": {\n \"nulladbf\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\"\n },\n \"jurisdictions\": {\n \"fugiatceb\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"magna_5\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"adipisicing96d\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"velit4_\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"dolor561\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n }\n }\n }\n }\n}" }, "description": {}, "header": [ @@ -3397,7 +3397,7 @@ "response": [ { "_postman_previewlanguage": "json", - "body": "{\n \"attributes\": {\n \"email\": \"\",\n \"familyName\": \"\",\n \"givenName\": \"\"\n },\n \"permissions\": {\n \"sit_94\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\"\n },\n \"jurisdictions\": {\n \"sunt0d5\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"nulla_a4\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"ullamco8e\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n }\n }\n },\n \"id384\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\"\n },\n \"jurisdictions\": {\n \"in1a\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"sed_44d\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n }\n }\n }\n },\n \"status\": \"inactive\",\n \"userId\": \"\"\n}", + "body": "{\n \"attributes\": {\n \"email\": \"\",\n \"familyName\": \"\",\n \"givenName\": \"\"\n },\n \"permissions\": {\n \"reprehenderit_1e\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\"\n },\n \"jurisdictions\": {\n \"non86\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"nostrud65\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"aute8b\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"velit90\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n }\n }\n },\n \"enim_9\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\"\n },\n \"jurisdictions\": {\n \"irure_15\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"magna_f\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"Excepteur8\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"amet7\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n }\n }\n },\n \"in__82\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\"\n },\n \"jurisdictions\": {\n \"irure_6\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"officiad72\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"consequat_53\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n }\n }\n }\n },\n \"status\": \"inactive\",\n \"userId\": \"\"\n}", "code": 200, "cookie": [], "header": [ @@ -3415,7 +3415,7 @@ "value": "" } ], - "id": "19a80dbe-01ed-460e-9599-2ab2ab7de680", + "id": "c8000bd7-ea17-46b2-89b9-e2985dd14e63", "name": "200 response", "originalRequest": { "body": { @@ -3426,7 +3426,7 @@ "language": "json" } }, - "raw": "{\n \"permissions\": {\n \"eac3\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\"\n },\n \"jurisdictions\": {\n \"ad_0_6\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"culpa4d3\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n }\n }\n },\n \"aliqua6d\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\"\n },\n \"jurisdictions\": {\n \"eu_4\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n }\n }\n },\n \"mollit_b9f\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\"\n },\n \"jurisdictions\": {\n \"mollit87e\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"consequate\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n }\n }\n }\n }\n}" + "raw": "{\n \"permissions\": {\n \"nulladbf\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\"\n },\n \"jurisdictions\": {\n \"fugiatceb\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"magna_5\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"adipisicing96d\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"velit4_\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"dolor561\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n }\n }\n }\n }\n}" }, "header": [ { @@ -3475,7 +3475,7 @@ "value": "application/json" } ], - "id": "d58f787b-e45d-47c9-9a30-a288b2054556", + "id": "17e6fa62-aa7d-484a-b47d-042dcf1942df", "name": "404 response", "originalRequest": { "body": { @@ -3486,7 +3486,7 @@ "language": "json" } }, - "raw": "{\n \"permissions\": {\n \"eac3\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\"\n },\n \"jurisdictions\": {\n \"ad_0_6\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"culpa4d3\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n }\n }\n },\n \"aliqua6d\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\"\n },\n \"jurisdictions\": {\n \"eu_4\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n }\n }\n },\n \"mollit_b9f\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\"\n },\n \"jurisdictions\": {\n \"mollit87e\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"consequate\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n }\n }\n }\n }\n}" + "raw": "{\n \"permissions\": {\n \"nulladbf\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\"\n },\n \"jurisdictions\": {\n \"fugiatceb\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"magna_5\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"adipisicing96d\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"velit4_\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"dolor561\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n }\n }\n }\n }\n}" }, "header": [ { @@ -3531,7 +3531,7 @@ "item": [ { "event": [], - "id": "293889c2-bc5b-474b-a3ac-43d0ab1ba647", + "id": "89c49bcd-9bfa-432f-a108-471584c85e2a", "name": "/v1/compacts/:compact/staff-users/:userId/reinvite", "protocolProfileBehavior": { "disableBodyPruning": true @@ -3596,7 +3596,7 @@ "value": "application/json" } ], - "id": "a77b3c89-209f-499e-bf08-a6a6e853b00c", + "id": "fd45d764-94f3-465a-92d8-3d1d98d89364", "name": "200 response", "originalRequest": { "body": {}, @@ -3644,7 +3644,7 @@ "value": "application/json" } ], - "id": "ecad5dc7-a40d-4da5-8e69-0737ce59b494", + "id": "16fa5fb3-1dbb-4c81-b4fe-ec2e800f1ca3", "name": "404 response", "originalRequest": { "body": {}, @@ -3709,7 +3709,7 @@ "item": [ { "event": [], - "id": "d528c13b-cbcb-4c12-8ae9-70c5f4d61d22", + "id": "1f6ba0de-9012-4603-9002-b2b04bb6a35a", "name": "/v1/flags/:flagId/check", "protocolProfileBehavior": { "disableBodyPruning": true @@ -3726,7 +3726,7 @@ "language": "json" } }, - "raw": "{\n \"context\": {\n \"userId\": \"\",\n \"customAttributes\": {\n \"proident_d\": \"\",\n \"ut5_\": \"\"\n }\n }\n}" + "raw": "{\n \"context\": {\n \"userId\": \"\",\n \"customAttributes\": {\n \"aute_8b\": \"\"\n }\n }\n}" }, "description": {}, "header": [ @@ -3778,7 +3778,7 @@ "value": "application/json" } ], - "id": "b7acc588-57be-4789-b88e-aa0551a1cd32", + "id": "13148b27-dd69-4820-920e-887eac55f797", "name": "200 response", "originalRequest": { "body": { @@ -3789,7 +3789,7 @@ "language": "json" } }, - "raw": "{\n \"context\": {\n \"userId\": \"\",\n \"customAttributes\": {\n \"proident_d\": \"\",\n \"ut5_\": \"\"\n }\n }\n}" + "raw": "{\n \"context\": {\n \"userId\": \"\",\n \"customAttributes\": {\n \"aute_8b\": \"\"\n }\n }\n}" }, "header": [ { @@ -3843,7 +3843,7 @@ "item": [ { "event": [], - "id": "4a189649-f610-4239-b2e5-d236fe357768", + "id": "c412ca43-6474-4193-b0f5-d94e25de0bd1", "name": "/v1/public/compacts/:compact/jurisdictions", "protocolProfileBehavior": { "disableBodyPruning": true @@ -3900,7 +3900,7 @@ "value": "application/json" } ], - "id": "3ce51344-7f33-4e56-84d1-43198977af25", + "id": "6944d604-fea4-464c-ad4d-1c3a83f3f16a", "name": "200 response", "originalRequest": { "body": {}, @@ -3941,7 +3941,7 @@ "item": [ { "event": [], - "id": "30ed3699-874f-428a-b4c8-227d3c1034ef", + "id": "145bdc6c-a3ef-46dc-8782-7af43419a16f", "name": "/v1/public/compacts/:compact/providers/query", "protocolProfileBehavior": { "disableBodyPruning": true @@ -3958,7 +3958,7 @@ "language": "json" } }, - "raw": "{\n \"query\": {\n \"providerId\": \"e28ca435-11f7-4f20-acdd-0e7e284b86d5\",\n \"jurisdiction\": \"wa\",\n \"givenName\": \"\",\n \"familyName\": \"\",\n \"licenseNumber\": \"\"\n },\n \"pagination\": {\n \"lastKey\": \"\",\n \"pageSize\": \"\"\n },\n \"sorting\": {\n \"key\": \"familyName\",\n \"direction\": \"ascending\"\n }\n}" + "raw": "{\n \"query\": {\n \"licenseType\": \"licensed bachelors social worker\",\n \"cuid\": \"sWC-1668-5\",\n \"jurisdiction\": \"wi\",\n \"givenName\": \"\",\n \"familyName\": \"\",\n \"licenseNumber\": \"\"\n },\n \"pagination\": {\n \"lastKey\": \"\",\n \"pageSize\": \"\"\n },\n \"sorting\": {\n \"key\": \"familyName\",\n \"direction\": \"descending\"\n }\n}" }, "description": {}, "header": [ @@ -4003,7 +4003,7 @@ "response": [ { "_postman_previewlanguage": "json", - "body": "{\n \"pagination\": {\n \"prevLastKey\": {},\n \"lastKey\": {},\n \"pageSize\": \"\"\n },\n \"providers\": [\n {\n \"compact\": \"socw\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"licenseEligibility\": \"eligible\",\n \"licenseJurisdiction\": \"ky\",\n \"licenseNumber\": \"\",\n \"licenseScope\": \"multi-state\",\n \"licenseType\": \"\",\n \"providerId\": \"14956ac8-1471-4be5-90e5-234da703e39d\"\n },\n {\n \"compact\": \"socw\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"licenseEligibility\": \"eligible\",\n \"licenseJurisdiction\": \"tn\",\n \"licenseNumber\": \"\",\n \"licenseScope\": \"single-state\",\n \"licenseType\": \"\",\n \"providerId\": \"6580c41e-1e45-43de-b582-04e40980027c\"\n }\n ],\n \"query\": {\n \"providerId\": \"3df2e975-b210-4eaa-b823-8c2999f3df40\",\n \"jurisdiction\": \"wa\",\n \"givenName\": \"\",\n \"familyName\": \"\",\n \"licenseNumber\": \"\"\n },\n \"sorting\": {\n \"key\": \"familyName\",\n \"direction\": \"descending\"\n }\n}", + "body": "{\n \"pagination\": {\n \"prevLastKey\": {},\n \"lastKey\": {},\n \"pageSize\": \"\"\n },\n \"providers\": [\n {\n \"compact\": \"socw\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"licenseEligibility\": \"ineligible\",\n \"licenseJurisdiction\": \"nj\",\n \"licenseNumber\": \"\",\n \"licenseScope\": \"single-state\",\n \"licenseType\": \"\",\n \"providerId\": \"7bfc455d-9db2-461d-855d-d1f419731d85\",\n \"publicCompactIdentifier\": \"sWC-7473-93598925\"\n },\n {\n \"compact\": \"socw\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"licenseEligibility\": \"eligible\",\n \"licenseJurisdiction\": \"gu\",\n \"licenseNumber\": \"\",\n \"licenseScope\": \"single-state\",\n \"licenseType\": \"\",\n \"providerId\": \"5d0f144f-ecdd-43b6-ba39-ae077efdef82\",\n \"publicCompactIdentifier\": \"sWC-9100-92\"\n }\n ],\n \"query\": {\n \"licenseType\": \"licensed bachelors social worker\",\n \"cuid\": \"SWC-2630-16\",\n \"providerId\": \"1f947d96-45b1-4223-9fea-aaa2d74f931c\",\n \"jurisdiction\": \"hi\",\n \"givenName\": \"\",\n \"familyName\": \"\",\n \"licenseNumber\": \"\"\n },\n \"sorting\": {\n \"key\": \"familyName\",\n \"direction\": \"descending\"\n }\n}", "code": 200, "cookie": [], "header": [ @@ -4012,7 +4012,7 @@ "value": "application/json" } ], - "id": "55ccb62f-ce81-49cb-b1f8-da6f94c8de8e", + "id": "ab7bcc0c-12c4-4587-88d4-517e90bf82ee", "name": "200 response", "originalRequest": { "body": { @@ -4023,7 +4023,7 @@ "language": "json" } }, - "raw": "{\n \"query\": {\n \"providerId\": \"e28ca435-11f7-4f20-acdd-0e7e284b86d5\",\n \"jurisdiction\": \"wa\",\n \"givenName\": \"\",\n \"familyName\": \"\",\n \"licenseNumber\": \"\"\n },\n \"pagination\": {\n \"lastKey\": \"\",\n \"pageSize\": \"\"\n },\n \"sorting\": {\n \"key\": \"familyName\",\n \"direction\": \"ascending\"\n }\n}" + "raw": "{\n \"query\": {\n \"licenseType\": \"licensed bachelors social worker\",\n \"cuid\": \"sWC-1668-5\",\n \"jurisdiction\": \"wi\",\n \"givenName\": \"\",\n \"familyName\": \"\",\n \"licenseNumber\": \"\"\n },\n \"pagination\": {\n \"lastKey\": \"\",\n \"pageSize\": \"\"\n },\n \"sorting\": {\n \"key\": \"familyName\",\n \"direction\": \"descending\"\n }\n}" }, "header": [ { @@ -4064,7 +4064,7 @@ "item": [ { "event": [], - "id": "125bfcef-27e2-49db-a8a7-70d038a24f4e", + "id": "3f769be1-9ac9-4427-be45-ecbf246997fe", "name": "/v1/public/compacts/:compact/providers/:providerId", "protocolProfileBehavior": { "disableBodyPruning": true @@ -4123,7 +4123,7 @@ "response": [ { "_postman_previewlanguage": "json", - "body": "{\n \"compact\": \"socw\",\n \"dateOfUpdate\": \"\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"licenseJurisdiction\": \"co\",\n \"providerId\": \"63129b26-a34c-4b9c-8d1c-cd6d0abffdf3\",\n \"type\": \"provider\",\n \"privileges\": [\n {\n \"administratorSetStatus\": \"inactive\",\n \"compact\": \"socw\",\n \"dateOfExpiration\": \"2931-10-24\",\n \"jurisdiction\": \"ks\",\n \"licenseJurisdiction\": \"wa\",\n \"licenseType\": \"licensed clinical social worker\",\n \"providerId\": \"86140669-e7d5-493e-9d4d-77def4f5d271\",\n \"status\": \"inactive\",\n \"type\": \"privilege\"\n },\n {\n \"administratorSetStatus\": \"inactive\",\n \"compact\": \"socw\",\n \"dateOfExpiration\": \"2417-11-14\",\n \"jurisdiction\": \"oh\",\n \"licenseJurisdiction\": \"ks\",\n \"licenseType\": \"licensed bachelors social worker\",\n \"providerId\": \"e4486f86-80f8-4f49-bb90-a2a297398c6e\",\n \"status\": \"active\",\n \"type\": \"privilege\"\n }\n ],\n \"licenses\": [\n {\n \"compact\": \"socw\",\n \"compactEligibility\": \"eligible\",\n \"dateOfExpiration\": \"2412-06-31\",\n \"jurisdiction\": \"ky\",\n \"licenseNumber\": \"\",\n \"licenseScope\": \"single-state\",\n \"licenseStatus\": \"inactive\",\n \"licenseType\": \"licensed bachelors social worker\",\n \"type\": \"license\"\n },\n {\n \"compact\": \"socw\",\n \"compactEligibility\": \"ineligible\",\n \"dateOfExpiration\": \"1841-12-03\",\n \"jurisdiction\": \"tn\",\n \"licenseNumber\": \"\",\n \"licenseScope\": \"multi-state\",\n \"licenseStatus\": \"active\",\n \"licenseType\": \"licensed clinical social worker\",\n \"type\": \"license\"\n }\n ],\n \"middleName\": \"\",\n \"suffix\": \"\"\n}", + "body": "{\n \"compact\": \"socw\",\n \"dateOfUpdate\": \"\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"licenseJurisdiction\": \"wa\",\n \"providerId\": \"ca604ef2-466d-4175-ad65-2179c15168c4\",\n \"type\": \"provider\",\n \"privileges\": [\n {\n \"administratorSetStatus\": \"active\",\n \"compact\": \"socw\",\n \"dateOfExpiration\": \"1710-05-15\",\n \"jurisdiction\": \"ne\",\n \"licenseJurisdiction\": \"nj\",\n \"licenseType\": \"licensed bachelors social worker\",\n \"providerId\": \"55c9b8e2-ba11-4e14-8fb6-d04c39e3f590\",\n \"status\": \"active\",\n \"type\": \"privilege\",\n \"adverseActions\": [\n {\n \"actionAgainst\": \"\",\n \"adverseActionId\": \"\",\n \"compact\": \"socw\",\n \"creationDate\": \"2408-11-05\",\n \"dateOfUpdate\": \"\",\n \"effectiveStartDate\": \"2769-08-31\",\n \"jurisdiction\": \"me\",\n \"licenseScope\": \"single-state\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"6f98ffc9-ca64-4f4b-817a-1911b4a59a0e\",\n \"type\": \"adverseAction\",\n \"effectiveLiftDate\": \"1353-10-04\"\n },\n {\n \"actionAgainst\": \"\",\n \"adverseActionId\": \"\",\n \"compact\": \"socw\",\n \"creationDate\": \"1951-12-21\",\n \"dateOfUpdate\": \"\",\n \"effectiveStartDate\": \"1011-11-23\",\n \"jurisdiction\": \"pa\",\n \"licenseScope\": \"single-state\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"e336fc34-f072-4f9d-b743-5d522f3894f8\",\n \"type\": \"adverseAction\",\n \"effectiveLiftDate\": \"1668-06-08\"\n }\n ]\n },\n {\n \"administratorSetStatus\": \"inactive\",\n \"compact\": \"socw\",\n \"dateOfExpiration\": \"2936-11-16\",\n \"jurisdiction\": \"dc\",\n \"licenseJurisdiction\": \"hi\",\n \"licenseType\": \"licensed clinical social worker\",\n \"providerId\": \"f557ced5-01d0-4fa2-99b2-6e29ca3289fc\",\n \"status\": \"active\",\n \"type\": \"privilege\",\n \"adverseActions\": [\n {\n \"actionAgainst\": \"\",\n \"adverseActionId\": \"\",\n \"compact\": \"socw\",\n \"creationDate\": \"1081-11-19\",\n \"dateOfUpdate\": \"\",\n \"effectiveStartDate\": \"2595-01-16\",\n \"jurisdiction\": \"me\",\n \"licenseScope\": \"single-state\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"19750ccd-a29d-446e-b9fa-36c97b4a228e\",\n \"type\": \"adverseAction\",\n \"effectiveLiftDate\": \"1004-05-31\"\n },\n {\n \"actionAgainst\": \"\",\n \"adverseActionId\": \"\",\n \"compact\": \"socw\",\n \"creationDate\": \"1742-11-29\",\n \"dateOfUpdate\": \"\",\n \"effectiveStartDate\": \"2536-04-24\",\n \"jurisdiction\": \"gu\",\n \"licenseScope\": \"single-state\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"54a3dec9-61b4-441d-852d-c540f081b66d\",\n \"type\": \"adverseAction\",\n \"effectiveLiftDate\": \"2926-04-31\"\n }\n ]\n }\n ],\n \"licenses\": [\n {\n \"compact\": \"socw\",\n \"compactEligibility\": \"eligible\",\n \"dateOfExpiration\": \"2908-09-07\",\n \"jurisdiction\": \"ri\",\n \"licenseNumber\": \"\",\n \"licenseScope\": \"multi-state\",\n \"licenseStatus\": \"active\",\n \"licenseType\": \"licensed clinical social worker\",\n \"type\": \"license\",\n \"adverseActions\": [\n {\n \"actionAgainst\": \"\",\n \"adverseActionId\": \"\",\n \"compact\": \"socw\",\n \"creationDate\": \"1926-05-27\",\n \"dateOfUpdate\": \"\",\n \"effectiveStartDate\": \"1429-12-24\",\n \"jurisdiction\": \"ca\",\n \"licenseScope\": \"multi-state\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"20255aaf-3f0e-4165-a2a6-c2e06a35be92\",\n \"type\": \"adverseAction\",\n \"effectiveLiftDate\": \"2155-09-31\"\n },\n {\n \"actionAgainst\": \"\",\n \"adverseActionId\": \"\",\n \"compact\": \"socw\",\n \"creationDate\": \"1459-01-22\",\n \"dateOfUpdate\": \"\",\n \"effectiveStartDate\": \"2089-12-02\",\n \"jurisdiction\": \"tn\",\n \"licenseScope\": \"single-state\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"549e662b-ec69-4501-aee0-bab16629edb0\",\n \"type\": \"adverseAction\",\n \"effectiveLiftDate\": \"2729-11-21\"\n }\n ]\n },\n {\n \"compact\": \"socw\",\n \"compactEligibility\": \"eligible\",\n \"dateOfExpiration\": \"1591-11-09\",\n \"jurisdiction\": \"wa\",\n \"licenseNumber\": \"\",\n \"licenseScope\": \"multi-state\",\n \"licenseStatus\": \"active\",\n \"licenseType\": \"licensed master social worker\",\n \"type\": \"license\",\n \"adverseActions\": [\n {\n \"actionAgainst\": \"\",\n \"adverseActionId\": \"\",\n \"compact\": \"socw\",\n \"creationDate\": \"1427-10-22\",\n \"dateOfUpdate\": \"\",\n \"effectiveStartDate\": \"1522-03-30\",\n \"jurisdiction\": \"fl\",\n \"licenseScope\": \"single-state\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"98b16c87-f6ef-4788-a61a-e9f3d61465c1\",\n \"type\": \"adverseAction\",\n \"effectiveLiftDate\": \"2953-03-18\"\n },\n {\n \"actionAgainst\": \"\",\n \"adverseActionId\": \"\",\n \"compact\": \"socw\",\n \"creationDate\": \"2638-09-24\",\n \"dateOfUpdate\": \"\",\n \"effectiveStartDate\": \"1354-12-03\",\n \"jurisdiction\": \"wa\",\n \"licenseScope\": \"multi-state\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"ec8534c7-793b-4561-a4fe-8b87f6e9425a\",\n \"type\": \"adverseAction\",\n \"effectiveLiftDate\": \"2569-10-08\"\n }\n ]\n }\n ],\n \"middleName\": \"\",\n \"suffix\": \"\",\n \"publicCompactIdentifier\": \"sWC-4604-6517\"\n}", "code": 200, "cookie": [], "header": [ @@ -4132,7 +4132,7 @@ "value": "application/json" } ], - "id": "bb110580-11fc-44ca-926a-deaaa065d229", + "id": "fc3d19fa-c8a5-4b34-96af-ed4a1e53ab28", "name": "200 response", "originalRequest": { "body": {}, @@ -4183,7 +4183,7 @@ "item": [ { "event": [], - "id": "83f0811c-8650-4b15-96c1-9ae6252261fa", + "id": "6ae80299-e01c-4539-a93b-d8a44afaf23c", "name": "/v1/public/jurisdictions/live", "protocolProfileBehavior": { "disableBodyPruning": true @@ -4229,7 +4229,7 @@ "response": [ { "_postman_previewlanguage": "json", - "body": "{\n \"fugiat4\": [\n \"ky\",\n \"oh\"\n ],\n \"estdc\": [\n \"al\",\n \"tn\"\n ],\n \"laborum_7e\": [\n \"co\",\n \"ks\"\n ]\n}", + "body": "{\n \"dolor_1d\": [\n \"ny\",\n \"wi\"\n ],\n \"sunt8\": [\n \"ar\",\n \"nj\"\n ]\n}", "code": 200, "cookie": [], "header": [ @@ -4238,7 +4238,7 @@ "value": "application/json" } ], - "id": "fd9b9f63-db12-40ad-b6b0-a2e8852280c1", + "id": "b3664763-630c-4e11-9f97-447a6787d5e7", "name": "200 response", "originalRequest": { "body": {}, @@ -4294,7 +4294,7 @@ "item": [ { "event": [], - "id": "8fd2e621-a683-4103-97ea-bef94b28fd89", + "id": "e3386fc3-8a9d-48d7-b42a-4353d035c6de", "name": "/v1/staff-users/me", "protocolProfileBehavior": { "disableBodyPruning": true @@ -4326,7 +4326,7 @@ "response": [ { "_postman_previewlanguage": "json", - "body": "{\n \"attributes\": {\n \"email\": \"\",\n \"familyName\": \"\",\n \"givenName\": \"\"\n },\n \"permissions\": {\n \"sit_94\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\"\n },\n \"jurisdictions\": {\n \"sunt0d5\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"nulla_a4\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"ullamco8e\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n }\n }\n },\n \"id384\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\"\n },\n \"jurisdictions\": {\n \"in1a\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"sed_44d\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n }\n }\n }\n },\n \"status\": \"inactive\",\n \"userId\": \"\"\n}", + "body": "{\n \"attributes\": {\n \"email\": \"\",\n \"familyName\": \"\",\n \"givenName\": \"\"\n },\n \"permissions\": {\n \"reprehenderit_1e\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\"\n },\n \"jurisdictions\": {\n \"non86\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"nostrud65\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"aute8b\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"velit90\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n }\n }\n },\n \"enim_9\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\"\n },\n \"jurisdictions\": {\n \"irure_15\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"magna_f\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"Excepteur8\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"amet7\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n }\n }\n },\n \"in__82\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\"\n },\n \"jurisdictions\": {\n \"irure_6\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"officiad72\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"consequat_53\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n }\n }\n }\n },\n \"status\": \"inactive\",\n \"userId\": \"\"\n}", "code": 200, "cookie": [], "header": [ @@ -4344,7 +4344,7 @@ "value": "" } ], - "id": "4a60829b-1b90-433b-9c60-628b1ef9c24e", + "id": "191d61b3-5304-41c2-9b7f-96d383f5f975", "name": "200 response", "originalRequest": { "body": {}, @@ -4389,7 +4389,7 @@ "value": "application/json" } ], - "id": "e7adf07b-e044-4309-bfad-6fa855884b9b", + "id": "bd54ed97-b669-43d0-8839-d2e10e6b68df", "name": "404 response", "originalRequest": { "body": {}, @@ -4427,7 +4427,7 @@ }, { "event": [], - "id": "bb5381a1-363a-455c-aadd-9f84669e3fae", + "id": "f9e6e839-bc10-458e-abb4-fa758aad322e", "name": "/v1/staff-users/me", "protocolProfileBehavior": { "disableBodyPruning": true @@ -4472,7 +4472,7 @@ "response": [ { "_postman_previewlanguage": "json", - "body": "{\n \"attributes\": {\n \"email\": \"\",\n \"familyName\": \"\",\n \"givenName\": \"\"\n },\n \"permissions\": {\n \"sit_94\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\"\n },\n \"jurisdictions\": {\n \"sunt0d5\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"nulla_a4\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"ullamco8e\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n }\n }\n },\n \"id384\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\"\n },\n \"jurisdictions\": {\n \"in1a\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"sed_44d\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n }\n }\n }\n },\n \"status\": \"inactive\",\n \"userId\": \"\"\n}", + "body": "{\n \"attributes\": {\n \"email\": \"\",\n \"familyName\": \"\",\n \"givenName\": \"\"\n },\n \"permissions\": {\n \"reprehenderit_1e\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\"\n },\n \"jurisdictions\": {\n \"non86\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"nostrud65\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"aute8b\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"velit90\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n }\n }\n },\n \"enim_9\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\"\n },\n \"jurisdictions\": {\n \"irure_15\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"magna_f\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"Excepteur8\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"amet7\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n }\n }\n },\n \"in__82\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\"\n },\n \"jurisdictions\": {\n \"irure_6\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"officiad72\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n },\n \"consequat_53\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\"\n }\n }\n }\n }\n },\n \"status\": \"inactive\",\n \"userId\": \"\"\n}", "code": 200, "cookie": [], "header": [ @@ -4490,7 +4490,7 @@ "value": "" } ], - "id": "6bf506b0-f44e-4835-bdfc-462194b188fe", + "id": "9c6948a7-5e86-4724-8e06-9579a7667cca", "name": "200 response", "originalRequest": { "body": { @@ -4548,7 +4548,7 @@ "value": "application/json" } ], - "id": "aee178e5-126b-48e7-95d0-35120703beb7", + "id": "c7bd1769-dc1c-4287-a5e0-8122cd7d0583", "name": "404 response", "originalRequest": { "body": { diff --git a/backend/social-work-app/docs/postman/postman-collection.json b/backend/social-work-app/docs/postman/postman-collection.json index 36992e3035..9822bdc5b2 100644 --- a/backend/social-work-app/docs/postman/postman-collection.json +++ b/backend/social-work-app/docs/postman/postman-collection.json @@ -10,7 +10,7 @@ "type": "bearer" }, "info": { - "_postman_id": "95043349-08ba-496a-915d-39834909a041", + "_postman_id": "6b6dde03-295a-41c5-a21c-aaf0dede8170", "description": { "content": "", "type": "text/plain" @@ -410,7 +410,7 @@ "item": [ { "event": [], - "id": "2873c2cd-04c5-4a1f-a23d-3366e0e189b7", + "id": "8f5473ae-a425-4517-9a49-f4434be24bd1", "name": "/v1/compacts/:compact/jurisdictions/:jurisdiction/licenses", "protocolProfileBehavior": { "disableBodyPruning": true @@ -424,7 +424,7 @@ "language": "json" } }, - "raw": "[\n {\n \"compactEligibility\": \"ineligible\",\n \"dateOfBirth\": \"2453-08-09\",\n \"dateOfExpiration\": \"1805-09-30\",\n \"dateOfIssuance\": \"2227-09-04\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"homeAddressCity\": \"\",\n \"homeAddressPostalCode\": \"\",\n \"homeAddressState\": \"\",\n \"homeAddressStreet1\": \"\",\n \"licenseNumber\": \"\",\n \"licenseScope\": \"multi-state\",\n \"licenseStatus\": \"active\",\n \"licenseType\": \"licensed clinical social worker\",\n \"ssn\": \"191-30-1461\",\n \"homeAddressStreet2\": \"\",\n \"suffix\": \"\",\n \"emailAddress\": \"\",\n \"phoneNumber\": \"+596130827\",\n \"dateOfRenewal\": \"1861-04-30\",\n \"middleName\": \"\",\n \"licenseStatusName\": \"\"\n },\n {\n \"compactEligibility\": \"ineligible\",\n \"dateOfBirth\": \"1214-04-18\",\n \"dateOfExpiration\": \"1769-12-01\",\n \"dateOfIssuance\": \"1125-09-03\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"homeAddressCity\": \"\",\n \"homeAddressPostalCode\": \"\",\n \"homeAddressState\": \"\",\n \"homeAddressStreet1\": \"\",\n \"licenseNumber\": \"\",\n \"licenseScope\": \"single-state\",\n \"licenseStatus\": \"active\",\n \"licenseType\": \"licensed bachelors social worker\",\n \"ssn\": \"793-13-4787\",\n \"homeAddressStreet2\": \"\",\n \"suffix\": \"\",\n \"emailAddress\": \"\",\n \"phoneNumber\": \"+9515811444091\",\n \"dateOfRenewal\": \"1590-10-30\",\n \"middleName\": \"\",\n \"licenseStatusName\": \"\"\n }\n]" + "raw": "[\n {\n \"compactEligibility\": \"eligible\",\n \"dateOfBirth\": \"2924-08-11\",\n \"dateOfExpiration\": \"2854-10-15\",\n \"dateOfIssuance\": \"1889-12-30\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"homeAddressCity\": \"\",\n \"homeAddressPostalCode\": \"\",\n \"homeAddressState\": \"\",\n \"homeAddressStreet1\": \"\",\n \"licenseNumber\": \"\",\n \"licenseScope\": \"single-state\",\n \"licenseStatus\": \"inactive\",\n \"licenseType\": \"licensed master social worker\",\n \"ssn\": \"818-22-9380\",\n \"homeAddressStreet2\": \"\",\n \"suffix\": \"\",\n \"emailAddress\": \"\",\n \"phoneNumber\": \"+31107217303\",\n \"dateOfRenewal\": \"2306-02-08\",\n \"middleName\": \"\",\n \"licenseStatusName\": \"\"\n },\n {\n \"compactEligibility\": \"eligible\",\n \"dateOfBirth\": \"2068-07-04\",\n \"dateOfExpiration\": \"1684-09-11\",\n \"dateOfIssuance\": \"1841-04-30\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"homeAddressCity\": \"\",\n \"homeAddressPostalCode\": \"\",\n \"homeAddressState\": \"\",\n \"homeAddressStreet1\": \"\",\n \"licenseNumber\": \"\",\n \"licenseScope\": \"single-state\",\n \"licenseStatus\": \"active\",\n \"licenseType\": \"licensed master social worker\",\n \"ssn\": \"866-34-7972\",\n \"homeAddressStreet2\": \"\",\n \"suffix\": \"\",\n \"emailAddress\": \"\",\n \"phoneNumber\": \"+2682743428858\",\n \"dateOfRenewal\": \"2737-08-11\",\n \"middleName\": \"\",\n \"licenseStatusName\": \"\"\n }\n]" }, "description": {}, "header": [ @@ -488,7 +488,7 @@ "value": "application/json" } ], - "id": "226c35b0-a976-4237-b9d0-13f0743c94e5", + "id": "61b01e41-6671-4721-a49c-e2de0be2fc7a", "name": "200 response", "originalRequest": { "body": { @@ -499,7 +499,7 @@ "language": "json" } }, - "raw": "[\n {\n \"compactEligibility\": \"ineligible\",\n \"dateOfBirth\": \"2453-08-09\",\n \"dateOfExpiration\": \"1805-09-30\",\n \"dateOfIssuance\": \"2227-09-04\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"homeAddressCity\": \"\",\n \"homeAddressPostalCode\": \"\",\n \"homeAddressState\": \"\",\n \"homeAddressStreet1\": \"\",\n \"licenseNumber\": \"\",\n \"licenseScope\": \"multi-state\",\n \"licenseStatus\": \"active\",\n \"licenseType\": \"licensed clinical social worker\",\n \"ssn\": \"191-30-1461\",\n \"homeAddressStreet2\": \"\",\n \"suffix\": \"\",\n \"emailAddress\": \"\",\n \"phoneNumber\": \"+596130827\",\n \"dateOfRenewal\": \"1861-04-30\",\n \"middleName\": \"\",\n \"licenseStatusName\": \"\"\n },\n {\n \"compactEligibility\": \"ineligible\",\n \"dateOfBirth\": \"1214-04-18\",\n \"dateOfExpiration\": \"1769-12-01\",\n \"dateOfIssuance\": \"1125-09-03\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"homeAddressCity\": \"\",\n \"homeAddressPostalCode\": \"\",\n \"homeAddressState\": \"\",\n \"homeAddressStreet1\": \"\",\n \"licenseNumber\": \"\",\n \"licenseScope\": \"single-state\",\n \"licenseStatus\": \"active\",\n \"licenseType\": \"licensed bachelors social worker\",\n \"ssn\": \"793-13-4787\",\n \"homeAddressStreet2\": \"\",\n \"suffix\": \"\",\n \"emailAddress\": \"\",\n \"phoneNumber\": \"+9515811444091\",\n \"dateOfRenewal\": \"1590-10-30\",\n \"middleName\": \"\",\n \"licenseStatusName\": \"\"\n }\n]" + "raw": "[\n {\n \"compactEligibility\": \"eligible\",\n \"dateOfBirth\": \"2924-08-11\",\n \"dateOfExpiration\": \"2854-10-15\",\n \"dateOfIssuance\": \"1889-12-30\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"homeAddressCity\": \"\",\n \"homeAddressPostalCode\": \"\",\n \"homeAddressState\": \"\",\n \"homeAddressStreet1\": \"\",\n \"licenseNumber\": \"\",\n \"licenseScope\": \"single-state\",\n \"licenseStatus\": \"inactive\",\n \"licenseType\": \"licensed master social worker\",\n \"ssn\": \"818-22-9380\",\n \"homeAddressStreet2\": \"\",\n \"suffix\": \"\",\n \"emailAddress\": \"\",\n \"phoneNumber\": \"+31107217303\",\n \"dateOfRenewal\": \"2306-02-08\",\n \"middleName\": \"\",\n \"licenseStatusName\": \"\"\n },\n {\n \"compactEligibility\": \"eligible\",\n \"dateOfBirth\": \"2068-07-04\",\n \"dateOfExpiration\": \"1684-09-11\",\n \"dateOfIssuance\": \"1841-04-30\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"homeAddressCity\": \"\",\n \"homeAddressPostalCode\": \"\",\n \"homeAddressState\": \"\",\n \"homeAddressStreet1\": \"\",\n \"licenseNumber\": \"\",\n \"licenseScope\": \"single-state\",\n \"licenseStatus\": \"active\",\n \"licenseType\": \"licensed master social worker\",\n \"ssn\": \"866-34-7972\",\n \"homeAddressStreet2\": \"\",\n \"suffix\": \"\",\n \"emailAddress\": \"\",\n \"phoneNumber\": \"+2682743428858\",\n \"dateOfRenewal\": \"2737-08-11\",\n \"middleName\": \"\",\n \"licenseStatusName\": \"\"\n }\n]" }, "header": [ { @@ -540,7 +540,7 @@ }, { "_postman_previewlanguage": "json", - "body": "{\n \"message\": \"\",\n \"errors\": {\n \"dolor6ad\": {\n \"tempor_da_\": [\n \"\",\n \"\"\n ],\n \"nostrud2c9\": [\n \"\",\n \"\"\n ]\n }\n }\n}", + "body": "{\n \"message\": \"\",\n \"errors\": {\n \"Lorem_76\": {\n \"sit_d\": [\n \"\",\n \"\"\n ],\n \"occaecat_5_3\": [\n \"\",\n \"\"\n ]\n },\n \"Ut_\": {\n \"elitf\": [\n \"\",\n \"\"\n ],\n \"ut_f\": [\n \"\",\n \"\"\n ],\n \"suntab\": [\n \"\",\n \"\"\n ],\n \"consectetur_2\": [\n \"\",\n \"\"\n ]\n }\n }\n}", "code": 400, "cookie": [], "header": [ @@ -549,7 +549,7 @@ "value": "application/json" } ], - "id": "77fec55e-be4b-4f37-ae66-a8a46cb9df1b", + "id": "c2d545c6-9fcc-4fc5-9774-5f053f84d04d", "name": "400 response", "originalRequest": { "body": { @@ -560,7 +560,7 @@ "language": "json" } }, - "raw": "[\n {\n \"compactEligibility\": \"ineligible\",\n \"dateOfBirth\": \"2453-08-09\",\n \"dateOfExpiration\": \"1805-09-30\",\n \"dateOfIssuance\": \"2227-09-04\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"homeAddressCity\": \"\",\n \"homeAddressPostalCode\": \"\",\n \"homeAddressState\": \"\",\n \"homeAddressStreet1\": \"\",\n \"licenseNumber\": \"\",\n \"licenseScope\": \"multi-state\",\n \"licenseStatus\": \"active\",\n \"licenseType\": \"licensed clinical social worker\",\n \"ssn\": \"191-30-1461\",\n \"homeAddressStreet2\": \"\",\n \"suffix\": \"\",\n \"emailAddress\": \"\",\n \"phoneNumber\": \"+596130827\",\n \"dateOfRenewal\": \"1861-04-30\",\n \"middleName\": \"\",\n \"licenseStatusName\": \"\"\n },\n {\n \"compactEligibility\": \"ineligible\",\n \"dateOfBirth\": \"1214-04-18\",\n \"dateOfExpiration\": \"1769-12-01\",\n \"dateOfIssuance\": \"1125-09-03\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"homeAddressCity\": \"\",\n \"homeAddressPostalCode\": \"\",\n \"homeAddressState\": \"\",\n \"homeAddressStreet1\": \"\",\n \"licenseNumber\": \"\",\n \"licenseScope\": \"single-state\",\n \"licenseStatus\": \"active\",\n \"licenseType\": \"licensed bachelors social worker\",\n \"ssn\": \"793-13-4787\",\n \"homeAddressStreet2\": \"\",\n \"suffix\": \"\",\n \"emailAddress\": \"\",\n \"phoneNumber\": \"+9515811444091\",\n \"dateOfRenewal\": \"1590-10-30\",\n \"middleName\": \"\",\n \"licenseStatusName\": \"\"\n }\n]" + "raw": "[\n {\n \"compactEligibility\": \"eligible\",\n \"dateOfBirth\": \"2924-08-11\",\n \"dateOfExpiration\": \"2854-10-15\",\n \"dateOfIssuance\": \"1889-12-30\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"homeAddressCity\": \"\",\n \"homeAddressPostalCode\": \"\",\n \"homeAddressState\": \"\",\n \"homeAddressStreet1\": \"\",\n \"licenseNumber\": \"\",\n \"licenseScope\": \"single-state\",\n \"licenseStatus\": \"inactive\",\n \"licenseType\": \"licensed master social worker\",\n \"ssn\": \"818-22-9380\",\n \"homeAddressStreet2\": \"\",\n \"suffix\": \"\",\n \"emailAddress\": \"\",\n \"phoneNumber\": \"+31107217303\",\n \"dateOfRenewal\": \"2306-02-08\",\n \"middleName\": \"\",\n \"licenseStatusName\": \"\"\n },\n {\n \"compactEligibility\": \"eligible\",\n \"dateOfBirth\": \"2068-07-04\",\n \"dateOfExpiration\": \"1684-09-11\",\n \"dateOfIssuance\": \"1841-04-30\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"homeAddressCity\": \"\",\n \"homeAddressPostalCode\": \"\",\n \"homeAddressState\": \"\",\n \"homeAddressStreet1\": \"\",\n \"licenseNumber\": \"\",\n \"licenseScope\": \"single-state\",\n \"licenseStatus\": \"active\",\n \"licenseType\": \"licensed master social worker\",\n \"ssn\": \"866-34-7972\",\n \"homeAddressStreet2\": \"\",\n \"suffix\": \"\",\n \"emailAddress\": \"\",\n \"phoneNumber\": \"+2682743428858\",\n \"dateOfRenewal\": \"2737-08-11\",\n \"middleName\": \"\",\n \"licenseStatusName\": \"\"\n }\n]" }, "header": [ { @@ -631,7 +631,7 @@ } } ], - "id": "43c0a599-d731-434f-b4f6-d89f4b3e2b75", + "id": "cf391ba3-9952-4803-b7bf-d5e81c33deaf", "name": "/v1/compacts/:compact/jurisdictions/:jurisdiction/licenses/bulk-upload", "protocolProfileBehavior": { "disableBodyPruning": true @@ -688,7 +688,7 @@ "response": [ { "_postman_previewlanguage": "json", - "body": "{\n \"upload\": {\n \"fields\": {\n \"aute_04\": \"\",\n \"dolord1\": \"\"\n },\n \"url\": \"\"\n }\n}", + "body": "{\n \"upload\": {\n \"fields\": {\n \"laborum8be\": \"\",\n \"veniam_b6a\": \"\",\n \"non_5\": \"\"\n },\n \"url\": \"\"\n }\n}", "code": 200, "cookie": [], "header": [ @@ -697,7 +697,7 @@ "value": "application/json" } ], - "id": "9e8bc365-d4d9-48d7-a626-35ccecdd8539", + "id": "4daba72b-4015-4d46-83dd-6466b4d0a566", "name": "200 response", "originalRequest": { "body": {}, diff --git a/backend/social-work-app/docs/search-internal/api-specification/latest-oas30.json b/backend/social-work-app/docs/search-internal/api-specification/latest-oas30.json index 52aeb0f8e2..ab21b364bc 100644 --- a/backend/social-work-app/docs/search-internal/api-specification/latest-oas30.json +++ b/backend/social-work-app/docs/search-internal/api-specification/latest-oas30.json @@ -2,7 +2,7 @@ "openapi": "3.0.1", "info": { "title": "SearchApi", - "version": "2026-06-01T19:39:25Z" + "version": "2026-07-28T19:48:11Z" }, "servers": [ { @@ -37,7 +37,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TestSSearcJnrX6LLHKFCY" + "$ref": "#/components/schemas/TestSSearcR7dqzbyiybEo" } } }, @@ -49,7 +49,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/TestSSearcK2HOYHlEWsa8" + "$ref": "#/components/schemas/TestSSearc0R2Gjp9HdCCP" } } } @@ -67,42 +67,7 @@ }, "components": { "schemas": { - "TestSSearcJnrX6LLHKFCY": { - "required": [ - "query" - ], - "type": "object", - "properties": { - "search_after": { - "type": "array", - "description": "Sort values from the last hit of the previous page for cursor-based pagination" - }, - "size": { - "maximum": 100, - "minimum": 1, - "type": "integer", - "description": "Number of results to return" - }, - "query": { - "type": "object", - "description": "The OpenSearch query body" - }, - "from": { - "minimum": 0, - "type": "integer", - "description": "Starting document offset for pagination" - }, - "sort": { - "type": "array", - "description": "Sort order for results (required for search_after pagination)", - "items": { - "type": "object" - } - } - }, - "additionalProperties": false - }, - "TestSSearcK2HOYHlEWsa8": { + "TestSSearc0R2Gjp9HdCCP": { "required": [ "providers", "total" @@ -175,15 +140,59 @@ "type": "string", "enum": [ "al", + "ak", "az", + "ar", + "ca", "co", + "ct", + "de", + "dc", + "fl", + "ga", + "gu", + "hi", + "id", + "il", + "in", + "ia", "ks", "ky", + "la", + "me", "md", + "ma", + "mi", + "mn", + "ms", + "mo", + "mt", + "mp", + "ne", + "nv", + "nh", + "nj", + "nm", + "ny", + "nc", + "nd", "oh", + "ok", + "or", + "pa", + "ri", + "sc", + "sd", "tn", + "tx", + "ut", + "vt", "va", - "wa" + "vi", + "wa", + "wv", + "wi", + "wy" ] }, "compact": { @@ -196,15 +205,59 @@ "type": "string", "enum": [ "al", + "ak", "az", + "ar", + "ca", "co", + "ct", + "de", + "dc", + "fl", + "ga", + "gu", + "hi", + "id", + "il", + "in", + "ia", "ks", "ky", + "la", + "me", "md", + "ma", + "mi", + "mn", + "ms", + "mo", + "mt", + "mp", + "ne", + "nv", + "nh", + "nj", + "nm", + "ny", + "nc", + "nd", "oh", + "ok", + "or", + "pa", + "ri", + "sc", + "sd", "tn", + "tx", + "ut", + "vt", "va", - "wa" + "vi", + "wa", + "wv", + "wi", + "wy" ] }, "investigations": { @@ -243,15 +296,59 @@ "type": "string", "enum": [ "al", + "ak", "az", + "ar", + "ca", "co", + "ct", + "de", + "dc", + "fl", + "ga", + "gu", + "hi", + "id", + "il", + "in", + "ia", "ks", "ky", + "la", + "me", "md", + "ma", + "mi", + "mn", + "ms", + "mo", + "mt", + "mp", + "ne", + "nv", + "nh", + "nj", + "nm", + "ny", + "nc", + "nd", "oh", + "ok", + "or", + "pa", + "ri", + "sc", + "sd", "tn", + "tx", + "ut", + "vt", "va", - "wa" + "vi", + "wa", + "wv", + "wi", + "wy" ] }, "submittingUser": { @@ -349,15 +446,59 @@ "type": "string", "enum": [ "al", + "ak", "az", + "ar", + "ca", "co", + "ct", + "de", + "dc", + "fl", + "ga", + "gu", + "hi", + "id", + "il", + "in", + "ia", "ks", "ky", + "la", + "me", "md", + "ma", + "mi", + "mn", + "ms", + "mo", + "mt", + "mp", + "ne", + "nv", + "nh", + "nj", + "nm", + "ny", + "nc", + "nd", "oh", + "ok", + "or", + "pa", + "ri", + "sc", + "sd", "tn", + "tx", + "ut", + "vt", "va", - "wa" + "vi", + "wa", + "wv", + "wi", + "wy" ] }, "licenseTypeAbbreviation": { @@ -431,15 +572,59 @@ "type": "string", "enum": [ "al", + "ak", "az", + "ar", + "ca", "co", + "ct", + "de", + "dc", + "fl", + "ga", + "gu", + "hi", + "id", + "il", + "in", + "ia", "ks", "ky", + "la", + "me", "md", + "ma", + "mi", + "mn", + "ms", + "mo", + "mt", + "mp", + "ne", + "nv", + "nh", + "nj", + "nm", + "ny", + "nc", + "nd", "oh", + "ok", + "or", + "pa", + "ri", + "sc", + "sd", "tn", + "tx", + "ut", + "vt", "va", - "wa" + "vi", + "wa", + "wv", + "wi", + "wy" ] }, "compact": { @@ -487,15 +672,59 @@ "type": "string", "enum": [ "al", + "ak", "az", + "ar", + "ca", "co", + "ct", + "de", + "dc", + "fl", + "ga", + "gu", + "hi", + "id", + "il", + "in", + "ia", "ks", "ky", + "la", + "me", "md", + "ma", + "mi", + "mn", + "ms", + "mo", + "mt", + "mp", + "ne", + "nv", + "nh", + "nj", + "nm", + "ny", + "nc", + "nd", "oh", + "ok", + "or", + "pa", + "ri", + "sc", + "sd", "tn", + "tx", + "ut", + "vt", "va", - "wa" + "vi", + "wa", + "wv", + "wi", + "wy" ] }, "licenses": { @@ -540,15 +769,59 @@ "type": "string", "enum": [ "al", + "ak", "az", + "ar", + "ca", "co", + "ct", + "de", + "dc", + "fl", + "ga", + "gu", + "hi", + "id", + "il", + "in", + "ia", "ks", "ky", + "la", + "me", "md", + "ma", + "mi", + "mn", + "ms", + "mo", + "mt", + "mp", + "ne", + "nv", + "nh", + "nj", + "nm", + "ny", + "nc", + "nd", "oh", + "ok", + "or", + "pa", + "ri", + "sc", + "sd", "tn", + "tx", + "ut", + "vt", "va", - "wa" + "vi", + "wa", + "wv", + "wi", + "wy" ] }, "homeAddressStreet1": { @@ -592,15 +865,59 @@ "type": "string", "enum": [ "al", + "ak", "az", + "ar", + "ca", "co", + "ct", + "de", + "dc", + "fl", + "ga", + "gu", + "hi", + "id", + "il", + "in", + "ia", "ks", "ky", + "la", + "me", "md", + "ma", + "mi", + "mn", + "ms", + "mo", + "mt", + "mp", + "ne", + "nv", + "nh", + "nj", + "nm", + "ny", + "nc", + "nd", "oh", + "ok", + "or", + "pa", + "ri", + "sc", + "sd", "tn", + "tx", + "ut", + "vt", "va", - "wa" + "vi", + "wa", + "wv", + "wi", + "wy" ] }, "submittingUser": { @@ -780,15 +1097,59 @@ "type": "string", "enum": [ "al", + "ak", "az", + "ar", + "ca", "co", + "ct", + "de", + "dc", + "fl", + "ga", + "gu", + "hi", + "id", + "il", + "in", + "ia", "ks", "ky", + "la", + "me", "md", + "ma", + "mi", + "mn", + "ms", + "mo", + "mt", + "mp", + "ne", + "nv", + "nh", + "nj", + "nm", + "ny", + "nc", + "nd", "oh", + "ok", + "or", + "pa", + "ri", + "sc", + "sd", "tn", + "tx", + "ut", + "vt", "va", - "wa" + "vi", + "wa", + "wv", + "wi", + "wy" ] }, "licenseTypeAbbreviation": { @@ -901,6 +1262,41 @@ } } } + }, + "TestSSearcR7dqzbyiybEo": { + "required": [ + "query" + ], + "type": "object", + "properties": { + "search_after": { + "type": "array", + "description": "Sort values from the last hit of the previous page for cursor-based pagination" + }, + "size": { + "maximum": 100, + "minimum": 1, + "type": "integer", + "description": "Number of results to return" + }, + "query": { + "type": "object", + "description": "The OpenSearch query body" + }, + "from": { + "minimum": 0, + "type": "integer", + "description": "Starting document offset for pagination" + }, + "sort": { + "type": "array", + "description": "Sort order for results (required for search_after pagination)", + "items": { + "type": "object" + } + } + }, + "additionalProperties": false } }, "securitySchemes": { diff --git a/backend/social-work-app/docs/search-internal/postman/postman-collection.json b/backend/social-work-app/docs/search-internal/postman/postman-collection.json index ea33ad6a1e..0023215d23 100644 --- a/backend/social-work-app/docs/search-internal/postman/postman-collection.json +++ b/backend/social-work-app/docs/search-internal/postman/postman-collection.json @@ -10,7 +10,7 @@ "type": "bearer" }, "info": { - "_postman_id": "0c30f4f8-6263-4d4e-8168-d5dc7f716398", + "_postman_id": "db8b4811-3c60-4ad3-a92f-e81cdade8338", "description": { "content": "", "type": "text/plain" @@ -407,7 +407,7 @@ "item": [ { "event": [], - "id": "62fe42b0-1443-40aa-809d-a511d9a7cbcd", + "id": "5d695ef1-a96d-4868-a8a5-968e210cf8a7", "name": "/v1/compacts/:compact/providers/search", "protocolProfileBehavior": { "disableBodyPruning": true @@ -465,7 +465,7 @@ "response": [ { "_postman_previewlanguage": "json", - "body": "{\n \"providers\": [\n {\n \"birthMonthDay\": \"02-38\",\n \"compact\": \"socw\",\n \"compactEligibility\": \"ineligible\",\n \"dateOfExpiration\": \"\",\n \"dateOfUpdate\": \"\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"jurisdictionUploadedCompactEligibility\": \"ineligible\",\n \"jurisdictionUploadedLicenseStatus\": \"active\",\n \"licenseJurisdiction\": \"va\",\n \"licenseStatus\": \"inactive\",\n \"providerId\": \"74f5dae3-18fa-4a77-97a3-4b964921cdee\",\n \"type\": \"provider\",\n \"privileges\": [\n {\n \"administratorSetStatus\": \"active\",\n \"compact\": \"socw\",\n \"dateOfExpiration\": \"2263-06-30\",\n \"jurisdiction\": \"co\",\n \"licenseJurisdiction\": \"va\",\n \"licenseType\": \"\",\n \"providerId\": \"5f72bd71-b6d7-4d92-a07d-254810385b33\",\n \"status\": \"active\",\n \"type\": \"privilege\",\n \"investigationStatus\": \"underInvestigation\",\n \"investigations\": [\n {\n \"compact\": \"socw\",\n \"creationDate\": \"\",\n \"dateOfUpdate\": \"\",\n \"investigationId\": \"\",\n \"jurisdiction\": \"md\",\n \"licenseType\": \"\",\n \"providerId\": \"8855ac21-c48d-4155-985c-8ebb7f063c21\",\n \"submittingUser\": \"\",\n \"type\": \"investigation\"\n },\n {\n \"compact\": \"socw\",\n \"creationDate\": \"\",\n \"dateOfUpdate\": \"\",\n \"investigationId\": \"\",\n \"jurisdiction\": \"tn\",\n \"licenseType\": \"\",\n \"providerId\": \"bd793ab2-cec0-4878-aa55-593a7acb15a6\",\n \"submittingUser\": \"\",\n \"type\": \"investigation\"\n }\n ],\n \"compactTransactionId\": \"\",\n \"adverseActions\": [\n {\n \"actionAgainst\": \"license\",\n \"adverseActionId\": \"\",\n \"compact\": \"socw\",\n \"creationDate\": \"2846-02-09\",\n \"dateOfUpdate\": \"\",\n \"effectiveStartDate\": \"1621-12-30\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"oh\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"16afef54-434b-45ca-8f5c-4b1cfd813d81\",\n \"submittingUser\": \"\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"Other\",\n \"Fraud, Deception, or Misrepresentation\"\n ],\n \"effectiveLiftDate\": \"1155-05-30\",\n \"liftingUser\": \"\"\n },\n {\n \"actionAgainst\": \"privilege\",\n \"adverseActionId\": \"\",\n \"compact\": \"socw\",\n \"creationDate\": \"2168-11-31\",\n \"dateOfUpdate\": \"\",\n \"effectiveStartDate\": \"2400-10-15\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"tn\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"ac01e6a8-4a49-4bb5-94de-354e61119576\",\n \"submittingUser\": \"\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"Other\",\n \"Substandard Care or Patient Neglect/Abuse\"\n ],\n \"effectiveLiftDate\": \"1492-08-30\",\n \"liftingUser\": \"\"\n }\n ]\n },\n {\n \"administratorSetStatus\": \"active\",\n \"compact\": \"socw\",\n \"dateOfExpiration\": \"1818-02-07\",\n \"jurisdiction\": \"wa\",\n \"licenseJurisdiction\": \"al\",\n \"licenseType\": \"\",\n \"providerId\": \"aeb5173a-78c6-44aa-810f-0d3c6950aa02\",\n \"status\": \"active\",\n \"type\": \"privilege\",\n \"investigationStatus\": \"underInvestigation\",\n \"investigations\": [\n {\n \"compact\": \"socw\",\n \"creationDate\": \"\",\n \"dateOfUpdate\": \"\",\n \"investigationId\": \"\",\n \"jurisdiction\": \"ks\",\n \"licenseType\": \"\",\n \"providerId\": \"081adac7-8778-41f0-9f7f-dd323cabcda8\",\n \"submittingUser\": \"\",\n \"type\": \"investigation\"\n },\n {\n \"compact\": \"socw\",\n \"creationDate\": \"\",\n \"dateOfUpdate\": \"\",\n \"investigationId\": \"\",\n \"jurisdiction\": \"va\",\n \"licenseType\": \"\",\n \"providerId\": \"7e1f8f70-feb6-4b9c-8386-be635eef91d6\",\n \"submittingUser\": \"\",\n \"type\": \"investigation\"\n }\n ],\n \"compactTransactionId\": \"\",\n \"adverseActions\": [\n {\n \"actionAgainst\": \"license\",\n \"adverseActionId\": \"\",\n \"compact\": \"socw\",\n \"creationDate\": \"2313-05-12\",\n \"dateOfUpdate\": \"\",\n \"effectiveStartDate\": \"1113-03-30\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"ks\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"ae8e065a-2d50-4b0a-b01c-316445fc9640\",\n \"submittingUser\": \"\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"Fraud, Deception, or Misrepresentation\",\n \"Other\"\n ],\n \"effectiveLiftDate\": \"2940-08-10\",\n \"liftingUser\": \"\"\n },\n {\n \"actionAgainst\": \"license\",\n \"adverseActionId\": \"\",\n \"compact\": \"socw\",\n \"creationDate\": \"1727-10-30\",\n \"dateOfUpdate\": \"\",\n \"effectiveStartDate\": \"2088-06-30\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"md\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"3bd743ea-8677-48d2-a353-5c0c25fb3e41\",\n \"submittingUser\": \"\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"Other\",\n \"Substandard Care or Patient Neglect/Abuse\"\n ],\n \"effectiveLiftDate\": \"2500-11-31\",\n \"liftingUser\": \"\"\n }\n ]\n }\n ],\n \"suffix\": \"\",\n \"currentHomeJurisdiction\": \"va\",\n \"licenses\": [\n {\n \"compact\": \"socw\",\n \"compactEligibility\": \"ineligible\",\n \"dateOfExpiration\": \"1895-10-21\",\n \"dateOfIssuance\": \"2600-12-30\",\n \"dateOfUpdate\": \"\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"homeAddressCity\": \"\",\n \"homeAddressPostalCode\": \"\",\n \"homeAddressState\": \"\",\n \"homeAddressStreet1\": \"\",\n \"jurisdiction\": \"md\",\n \"jurisdictionUploadedCompactEligibility\": \"ineligible\",\n \"jurisdictionUploadedLicenseStatus\": \"active\",\n \"licenseNumber\": \"\",\n \"licenseScope\": \"multi-state\",\n \"licenseStatus\": \"inactive\",\n \"licenseType\": \"\",\n \"providerId\": \"d07ede9f-f6da-4f87-b03a-fd5e228b177a\",\n \"type\": \"license-home\",\n \"homeAddressStreet2\": \"\",\n \"investigations\": [\n {\n \"compact\": \"socw\",\n \"creationDate\": \"\",\n \"dateOfUpdate\": \"\",\n \"investigationId\": \"\",\n \"jurisdiction\": \"ky\",\n \"licenseType\": \"\",\n \"providerId\": \"ee7fd305-d245-4b80-abbc-4e22aac3af17\",\n \"submittingUser\": \"\",\n \"type\": \"investigation\"\n },\n {\n \"compact\": \"socw\",\n \"creationDate\": \"\",\n \"dateOfUpdate\": \"\",\n \"investigationId\": \"\",\n \"jurisdiction\": \"tn\",\n \"licenseType\": \"\",\n \"providerId\": \"18b0e870-4c7c-4c82-8dd2-67ae396dd9af\",\n \"submittingUser\": \"\",\n \"type\": \"investigation\"\n }\n ],\n \"suffix\": \"\",\n \"emailAddress\": \"\",\n \"dateOfRenewal\": \"1486-10-30\",\n \"investigationStatus\": \"underInvestigation\",\n \"phoneNumber\": \"+10037723998\",\n \"licenseStatusName\": \"\",\n \"middleName\": \"\",\n \"adverseActions\": [\n {\n \"actionAgainst\": \"license\",\n \"adverseActionId\": \"\",\n \"compact\": \"socw\",\n \"creationDate\": \"2285-07-16\",\n \"dateOfUpdate\": \"\",\n \"effectiveStartDate\": \"1815-02-31\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"oh\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"8aac86d2-017f-4a1d-b3b8-2743ee3ecda5\",\n \"submittingUser\": \"\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"Substandard Care or Patient Neglect/Abuse\",\n \"Other\"\n ],\n \"effectiveLiftDate\": \"2987-12-29\",\n \"liftingUser\": \"\"\n },\n {\n \"actionAgainst\": \"privilege\",\n \"adverseActionId\": \"\",\n \"compact\": \"socw\",\n \"creationDate\": \"1146-09-18\",\n \"dateOfUpdate\": \"\",\n \"effectiveStartDate\": \"2087-12-23\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"al\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"cc107b7d-340d-431e-84af-fd73023914c0\",\n \"submittingUser\": \"\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"Fraud, Deception, or Misrepresentation\",\n \"Substandard Care or Patient Neglect/Abuse\"\n ],\n \"effectiveLiftDate\": \"2904-12-14\",\n \"liftingUser\": \"\"\n }\n ]\n },\n {\n \"compact\": \"socw\",\n \"compactEligibility\": \"eligible\",\n \"dateOfExpiration\": \"1872-11-13\",\n \"dateOfIssuance\": \"2817-01-14\",\n \"dateOfUpdate\": \"\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"homeAddressCity\": \"\",\n \"homeAddressPostalCode\": \"\",\n \"homeAddressState\": \"\",\n \"homeAddressStreet1\": \"\",\n \"jurisdiction\": \"va\",\n \"jurisdictionUploadedCompactEligibility\": \"ineligible\",\n \"jurisdictionUploadedLicenseStatus\": \"inactive\",\n \"licenseNumber\": \"\",\n \"licenseScope\": \"multi-state\",\n \"licenseStatus\": \"active\",\n \"licenseType\": \"\",\n \"providerId\": \"f5f8104a-ad41-49f9-91b8-2386212782ed\",\n \"type\": \"license-home\",\n \"homeAddressStreet2\": \"\",\n \"investigations\": [\n {\n \"compact\": \"socw\",\n \"creationDate\": \"\",\n \"dateOfUpdate\": \"\",\n \"investigationId\": \"\",\n \"jurisdiction\": \"md\",\n \"licenseType\": \"\",\n \"providerId\": \"c86f7639-53ed-437d-b3dc-75c0b6514931\",\n \"submittingUser\": \"\",\n \"type\": \"investigation\"\n },\n {\n \"compact\": \"socw\",\n \"creationDate\": \"\",\n \"dateOfUpdate\": \"\",\n \"investigationId\": \"\",\n \"jurisdiction\": \"tn\",\n \"licenseType\": \"\",\n \"providerId\": \"291000d5-d280-4115-bfad-28b277f00f6a\",\n \"submittingUser\": \"\",\n \"type\": \"investigation\"\n }\n ],\n \"suffix\": \"\",\n \"emailAddress\": \"\",\n \"dateOfRenewal\": \"1536-08-05\",\n \"investigationStatus\": \"underInvestigation\",\n \"phoneNumber\": \"+30022521297176\",\n \"licenseStatusName\": \"\",\n \"middleName\": \"\",\n \"adverseActions\": [\n {\n \"actionAgainst\": \"privilege\",\n \"adverseActionId\": \"\",\n \"compact\": \"socw\",\n \"creationDate\": \"2277-10-03\",\n \"dateOfUpdate\": \"\",\n \"effectiveStartDate\": \"2509-12-08\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"co\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"bed70d59-e1c6-424b-b562-77299b5ab067\",\n \"submittingUser\": \"\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"Other\",\n \"Fraud, Deception, or Misrepresentation\"\n ],\n \"effectiveLiftDate\": \"1874-08-06\",\n \"liftingUser\": \"\"\n },\n {\n \"actionAgainst\": \"privilege\",\n \"adverseActionId\": \"\",\n \"compact\": \"socw\",\n \"creationDate\": \"2601-10-08\",\n \"dateOfUpdate\": \"\",\n \"effectiveStartDate\": \"2912-04-02\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"ky\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"1ffe8f05-3c2f-489d-b388-b87538edb32b\",\n \"submittingUser\": \"\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"Fraud, Deception, or Misrepresentation\",\n \"Other\"\n ],\n \"effectiveLiftDate\": \"2363-10-31\",\n \"liftingUser\": \"\"\n }\n ]\n }\n ],\n \"middleName\": \"\",\n \"compactConnectRegisteredEmailAddress\": \"\"\n },\n {\n \"birthMonthDay\": \"12-13\",\n \"compact\": \"socw\",\n \"compactEligibility\": \"ineligible\",\n \"dateOfExpiration\": \"\",\n \"dateOfUpdate\": \"\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"jurisdictionUploadedCompactEligibility\": \"ineligible\",\n \"jurisdictionUploadedLicenseStatus\": \"active\",\n \"licenseJurisdiction\": \"co\",\n \"licenseStatus\": \"inactive\",\n \"providerId\": \"9f4f898d-5c9a-4357-b2c8-d9f2318eb47e\",\n \"type\": \"provider\",\n \"privileges\": [\n {\n \"administratorSetStatus\": \"active\",\n \"compact\": \"socw\",\n \"dateOfExpiration\": \"1542-05-15\",\n \"jurisdiction\": \"az\",\n \"licenseJurisdiction\": \"wa\",\n \"licenseType\": \"\",\n \"providerId\": \"ffad1705-e9af-4790-879f-c713e0b33c9c\",\n \"status\": \"active\",\n \"type\": \"privilege\",\n \"investigationStatus\": \"underInvestigation\",\n \"investigations\": [\n {\n \"compact\": \"socw\",\n \"creationDate\": \"\",\n \"dateOfUpdate\": \"\",\n \"investigationId\": \"\",\n \"jurisdiction\": \"al\",\n \"licenseType\": \"\",\n \"providerId\": \"fa4e32c6-a68e-4eba-b461-e1bfab80db86\",\n \"submittingUser\": \"\",\n \"type\": \"investigation\"\n },\n {\n \"compact\": \"socw\",\n \"creationDate\": \"\",\n \"dateOfUpdate\": \"\",\n \"investigationId\": \"\",\n \"jurisdiction\": \"wa\",\n \"licenseType\": \"\",\n \"providerId\": \"97ba6371-9868-40c8-a1c8-33bea508d9f5\",\n \"submittingUser\": \"\",\n \"type\": \"investigation\"\n }\n ],\n \"compactTransactionId\": \"\",\n \"adverseActions\": [\n {\n \"actionAgainst\": \"license\",\n \"adverseActionId\": \"\",\n \"compact\": \"socw\",\n \"creationDate\": \"1216-03-30\",\n \"dateOfUpdate\": \"\",\n \"effectiveStartDate\": \"2562-10-03\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"az\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"8c0b5003-fbca-4328-9e9b-a09b84b64feb\",\n \"submittingUser\": \"\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"Fraud, Deception, or Misrepresentation\",\n \"Fraud, Deception, or Misrepresentation\"\n ],\n \"effectiveLiftDate\": \"2482-07-08\",\n \"liftingUser\": \"\"\n },\n {\n \"actionAgainst\": \"license\",\n \"adverseActionId\": \"\",\n \"compact\": \"socw\",\n \"creationDate\": \"1798-11-02\",\n \"dateOfUpdate\": \"\",\n \"effectiveStartDate\": \"2177-02-31\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"va\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"fa9b8be3-329f-42cb-a829-9b4645e53ff9\",\n \"submittingUser\": \"\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"Other\",\n \"Other\"\n ],\n \"effectiveLiftDate\": \"2178-12-05\",\n \"liftingUser\": \"\"\n }\n ]\n },\n {\n \"administratorSetStatus\": \"active\",\n \"compact\": \"socw\",\n \"dateOfExpiration\": \"2916-01-27\",\n \"jurisdiction\": \"az\",\n \"licenseJurisdiction\": \"tn\",\n \"licenseType\": \"\",\n \"providerId\": \"81d10e2f-4b60-4095-8b37-0965ede8e91e\",\n \"status\": \"active\",\n \"type\": \"privilege\",\n \"investigationStatus\": \"underInvestigation\",\n \"investigations\": [\n {\n \"compact\": \"socw\",\n \"creationDate\": \"\",\n \"dateOfUpdate\": \"\",\n \"investigationId\": \"\",\n \"jurisdiction\": \"ks\",\n \"licenseType\": \"\",\n \"providerId\": \"034036df-6682-458d-b33d-1c3a33528a24\",\n \"submittingUser\": \"\",\n \"type\": \"investigation\"\n },\n {\n \"compact\": \"socw\",\n \"creationDate\": \"\",\n \"dateOfUpdate\": \"\",\n \"investigationId\": \"\",\n \"jurisdiction\": \"al\",\n \"licenseType\": \"\",\n \"providerId\": \"d6560741-e20a-4a5a-a68d-f7ee04c4b36c\",\n \"submittingUser\": \"\",\n \"type\": \"investigation\"\n }\n ],\n \"compactTransactionId\": \"\",\n \"adverseActions\": [\n {\n \"actionAgainst\": \"privilege\",\n \"adverseActionId\": \"\",\n \"compact\": \"socw\",\n \"creationDate\": \"2880-11-05\",\n \"dateOfUpdate\": \"\",\n \"effectiveStartDate\": \"2947-12-30\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"az\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"6115af67-4609-4a0b-a671-da74f8e5a896\",\n \"submittingUser\": \"\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"Fraud, Deception, or Misrepresentation\",\n \"Fraud, Deception, or Misrepresentation\"\n ],\n \"effectiveLiftDate\": \"2906-11-30\",\n \"liftingUser\": \"\"\n },\n {\n \"actionAgainst\": \"privilege\",\n \"adverseActionId\": \"\",\n \"compact\": \"socw\",\n \"creationDate\": \"1237-11-30\",\n \"dateOfUpdate\": \"\",\n \"effectiveStartDate\": \"1431-07-03\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"md\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"b547e42b-0f70-40e3-b35b-0d262fcc88ee\",\n \"submittingUser\": \"\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"Substandard Care or Patient Neglect/Abuse\",\n \"Other\"\n ],\n \"effectiveLiftDate\": \"2819-04-03\",\n \"liftingUser\": \"\"\n }\n ]\n }\n ],\n \"suffix\": \"\",\n \"currentHomeJurisdiction\": \"co\",\n \"licenses\": [\n {\n \"compact\": \"socw\",\n \"compactEligibility\": \"ineligible\",\n \"dateOfExpiration\": \"2926-04-06\",\n \"dateOfIssuance\": \"1804-05-09\",\n \"dateOfUpdate\": \"\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"homeAddressCity\": \"\",\n \"homeAddressPostalCode\": \"\",\n \"homeAddressState\": \"\",\n \"homeAddressStreet1\": \"\",\n \"jurisdiction\": \"al\",\n \"jurisdictionUploadedCompactEligibility\": \"ineligible\",\n \"jurisdictionUploadedLicenseStatus\": \"active\",\n \"licenseNumber\": \"\",\n \"licenseScope\": \"single-state\",\n \"licenseStatus\": \"active\",\n \"licenseType\": \"\",\n \"providerId\": \"1d00d771-1736-44e5-8698-021e77ef9d7d\",\n \"type\": \"license-home\",\n \"homeAddressStreet2\": \"\",\n \"investigations\": [\n {\n \"compact\": \"socw\",\n \"creationDate\": \"\",\n \"dateOfUpdate\": \"\",\n \"investigationId\": \"\",\n \"jurisdiction\": \"wa\",\n \"licenseType\": \"\",\n \"providerId\": \"e2961462-b167-4d97-8e67-d9c3683a0587\",\n \"submittingUser\": \"\",\n \"type\": \"investigation\"\n },\n {\n \"compact\": \"socw\",\n \"creationDate\": \"\",\n \"dateOfUpdate\": \"\",\n \"investigationId\": \"\",\n \"jurisdiction\": \"co\",\n \"licenseType\": \"\",\n \"providerId\": \"70e95156-e697-470a-95b5-7ed0d8e5569f\",\n \"submittingUser\": \"\",\n \"type\": \"investigation\"\n }\n ],\n \"suffix\": \"\",\n \"emailAddress\": \"\",\n \"dateOfRenewal\": \"1001-12-06\",\n \"investigationStatus\": \"underInvestigation\",\n \"phoneNumber\": \"+91177413084660\",\n \"licenseStatusName\": \"\",\n \"middleName\": \"\",\n \"adverseActions\": [\n {\n \"actionAgainst\": \"privilege\",\n \"adverseActionId\": \"\",\n \"compact\": \"socw\",\n \"creationDate\": \"2009-11-31\",\n \"dateOfUpdate\": \"\",\n \"effectiveStartDate\": \"2910-01-11\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"az\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"a7c92c4f-fcef-440a-b173-f2baf701b6af\",\n \"submittingUser\": \"\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"Other\",\n \"Other\"\n ],\n \"effectiveLiftDate\": \"1892-10-05\",\n \"liftingUser\": \"\"\n },\n {\n \"actionAgainst\": \"license\",\n \"adverseActionId\": \"\",\n \"compact\": \"socw\",\n \"creationDate\": \"1584-12-01\",\n \"dateOfUpdate\": \"\",\n \"effectiveStartDate\": \"1692-09-11\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"wa\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"9b0e2f4c-7c3e-44f7-b69c-da794983beca\",\n \"submittingUser\": \"\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"Substandard Care or Patient Neglect/Abuse\",\n \"Fraud, Deception, or Misrepresentation\"\n ],\n \"effectiveLiftDate\": \"1779-12-21\",\n \"liftingUser\": \"\"\n }\n ]\n },\n {\n \"compact\": \"socw\",\n \"compactEligibility\": \"ineligible\",\n \"dateOfExpiration\": \"2614-02-18\",\n \"dateOfIssuance\": \"1176-09-31\",\n \"dateOfUpdate\": \"\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"homeAddressCity\": \"\",\n \"homeAddressPostalCode\": \"\",\n \"homeAddressState\": \"\",\n \"homeAddressStreet1\": \"\",\n \"jurisdiction\": \"co\",\n \"jurisdictionUploadedCompactEligibility\": \"ineligible\",\n \"jurisdictionUploadedLicenseStatus\": \"inactive\",\n \"licenseNumber\": \"\",\n \"licenseScope\": \"single-state\",\n \"licenseStatus\": \"inactive\",\n \"licenseType\": \"\",\n \"providerId\": \"43da7025-d2f0-4a33-a3ac-63d9822cefcd\",\n \"type\": \"license-home\",\n \"homeAddressStreet2\": \"\",\n \"investigations\": [\n {\n \"compact\": \"socw\",\n \"creationDate\": \"\",\n \"dateOfUpdate\": \"\",\n \"investigationId\": \"\",\n \"jurisdiction\": \"wa\",\n \"licenseType\": \"\",\n \"providerId\": \"056df11e-e52c-4d75-b3cc-cad9052b2344\",\n \"submittingUser\": \"\",\n \"type\": \"investigation\"\n },\n {\n \"compact\": \"socw\",\n \"creationDate\": \"\",\n \"dateOfUpdate\": \"\",\n \"investigationId\": \"\",\n \"jurisdiction\": \"wa\",\n \"licenseType\": \"\",\n \"providerId\": \"09dc62b4-9d60-4f31-8fee-5f4ce2c6a0be\",\n \"submittingUser\": \"\",\n \"type\": \"investigation\"\n }\n ],\n \"suffix\": \"\",\n \"emailAddress\": \"\",\n \"dateOfRenewal\": \"2472-05-30\",\n \"investigationStatus\": \"underInvestigation\",\n \"phoneNumber\": \"+72464522486\",\n \"licenseStatusName\": \"\",\n \"middleName\": \"\",\n \"adverseActions\": [\n {\n \"actionAgainst\": \"privilege\",\n \"adverseActionId\": \"\",\n \"compact\": \"socw\",\n \"creationDate\": \"2887-01-07\",\n \"dateOfUpdate\": \"\",\n \"effectiveStartDate\": \"1605-05-04\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"va\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"cea13954-176f-4b73-8a3b-ad475eeb57df\",\n \"submittingUser\": \"\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"Substandard Care or Patient Neglect/Abuse\",\n \"Fraud, Deception, or Misrepresentation\"\n ],\n \"effectiveLiftDate\": \"1827-05-22\",\n \"liftingUser\": \"\"\n },\n {\n \"actionAgainst\": \"privilege\",\n \"adverseActionId\": \"\",\n \"compact\": \"socw\",\n \"creationDate\": \"1532-04-31\",\n \"dateOfUpdate\": \"\",\n \"effectiveStartDate\": \"1550-08-26\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"wa\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"117f1560-f5ab-42fb-914e-8f4573febac8\",\n \"submittingUser\": \"\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"Substandard Care or Patient Neglect/Abuse\",\n \"Other\"\n ],\n \"effectiveLiftDate\": \"1642-10-27\",\n \"liftingUser\": \"\"\n }\n ]\n }\n ],\n \"middleName\": \"\",\n \"compactConnectRegisteredEmailAddress\": \"\"\n }\n ],\n \"total\": {\n \"value\": \"\",\n \"relation\": \"gte\"\n },\n \"lastSort\": \"\"\n}", + "body": "{\n \"providers\": [\n {\n \"birthMonthDay\": \"11-15\",\n \"compact\": \"socw\",\n \"compactEligibility\": \"eligible\",\n \"dateOfExpiration\": \"\",\n \"dateOfUpdate\": \"\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"jurisdictionUploadedCompactEligibility\": \"ineligible\",\n \"jurisdictionUploadedLicenseStatus\": \"active\",\n \"licenseJurisdiction\": \"sd\",\n \"licenseStatus\": \"active\",\n \"providerId\": \"009e1e32-6efc-4f14-81a4-be3d128ceb33\",\n \"type\": \"provider\",\n \"privileges\": [\n {\n \"administratorSetStatus\": \"active\",\n \"compact\": \"socw\",\n \"dateOfExpiration\": \"2531-03-01\",\n \"jurisdiction\": \"vt\",\n \"licenseJurisdiction\": \"pa\",\n \"licenseType\": \"\",\n \"providerId\": \"6b3cb8d1-78f2-47ec-8caf-71584713cfc3\",\n \"status\": \"active\",\n \"type\": \"privilege\",\n \"investigationStatus\": \"underInvestigation\",\n \"investigations\": [\n {\n \"compact\": \"socw\",\n \"creationDate\": \"\",\n \"dateOfUpdate\": \"\",\n \"investigationId\": \"\",\n \"jurisdiction\": \"ct\",\n \"licenseType\": \"\",\n \"providerId\": \"83543e82-ab32-4ad5-98ea-a43b895e654e\",\n \"submittingUser\": \"\",\n \"type\": \"investigation\"\n },\n {\n \"compact\": \"socw\",\n \"creationDate\": \"\",\n \"dateOfUpdate\": \"\",\n \"investigationId\": \"\",\n \"jurisdiction\": \"mo\",\n \"licenseType\": \"\",\n \"providerId\": \"5f370741-2aac-4fd8-b77e-b55f1ededea8\",\n \"submittingUser\": \"\",\n \"type\": \"investigation\"\n }\n ],\n \"compactTransactionId\": \"\",\n \"adverseActions\": [\n {\n \"actionAgainst\": \"privilege\",\n \"adverseActionId\": \"\",\n \"compact\": \"socw\",\n \"creationDate\": \"2531-08-31\",\n \"dateOfUpdate\": \"\",\n \"effectiveStartDate\": \"1848-11-05\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"ok\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"78efc930-6ba3-4557-bd70-34fcb6cb149c\",\n \"submittingUser\": \"\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"Improper Supervision or Allowing Unlicensed Practice\",\n \"Criminal Conviction or Adjudication\"\n ],\n \"effectiveLiftDate\": \"2631-12-30\",\n \"liftingUser\": \"\"\n },\n {\n \"actionAgainst\": \"privilege\",\n \"adverseActionId\": \"\",\n \"compact\": \"socw\",\n \"creationDate\": \"2522-10-11\",\n \"dateOfUpdate\": \"\",\n \"effectiveStartDate\": \"1141-10-29\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"nv\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"0d3ab4e0-9b64-448e-8b87-1096f1ddf2ee\",\n \"submittingUser\": \"\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"Confidentiality, Consent or Disclosure Violations\",\n \"Non-Compliance With Requirements\"\n ],\n \"effectiveLiftDate\": \"1535-02-07\",\n \"liftingUser\": \"\"\n }\n ]\n },\n {\n \"administratorSetStatus\": \"inactive\",\n \"compact\": \"socw\",\n \"dateOfExpiration\": \"1542-10-19\",\n \"jurisdiction\": \"nj\",\n \"licenseJurisdiction\": \"al\",\n \"licenseType\": \"\",\n \"providerId\": \"725263e5-d83b-4906-9927-67f4c0f49cc8\",\n \"status\": \"active\",\n \"type\": \"privilege\",\n \"investigationStatus\": \"underInvestigation\",\n \"investigations\": [\n {\n \"compact\": \"socw\",\n \"creationDate\": \"\",\n \"dateOfUpdate\": \"\",\n \"investigationId\": \"\",\n \"jurisdiction\": \"oh\",\n \"licenseType\": \"\",\n \"providerId\": \"1d74989c-7508-449e-af7d-ce054ea0c564\",\n \"submittingUser\": \"\",\n \"type\": \"investigation\"\n },\n {\n \"compact\": \"socw\",\n \"creationDate\": \"\",\n \"dateOfUpdate\": \"\",\n \"investigationId\": \"\",\n \"jurisdiction\": \"mn\",\n \"licenseType\": \"\",\n \"providerId\": \"cbb6c451-266e-40a2-9aa1-847739d8b8a7\",\n \"submittingUser\": \"\",\n \"type\": \"investigation\"\n }\n ],\n \"compactTransactionId\": \"\",\n \"adverseActions\": [\n {\n \"actionAgainst\": \"license\",\n \"adverseActionId\": \"\",\n \"compact\": \"socw\",\n \"creationDate\": \"1672-07-30\",\n \"dateOfUpdate\": \"\",\n \"effectiveStartDate\": \"2149-03-30\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"mn\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"7a018120-50c8-4394-b101-75940a15c258\",\n \"submittingUser\": \"\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"Criminal Conviction or Adjudication\",\n \"Improper Supervision or Allowing Unlicensed Practice\"\n ],\n \"effectiveLiftDate\": \"1436-10-04\",\n \"liftingUser\": \"\"\n },\n {\n \"actionAgainst\": \"license\",\n \"adverseActionId\": \"\",\n \"compact\": \"socw\",\n \"creationDate\": \"1358-09-30\",\n \"dateOfUpdate\": \"\",\n \"effectiveStartDate\": \"1128-09-03\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"vi\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"93295942-465f-4fae-a893-6a072ff48975\",\n \"submittingUser\": \"\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"Fraud, Deception, or Misrepresentation\",\n \"Conflict of Interest\"\n ],\n \"effectiveLiftDate\": \"2587-11-01\",\n \"liftingUser\": \"\"\n }\n ]\n }\n ],\n \"suffix\": \"\",\n \"currentHomeJurisdiction\": \"fl\",\n \"licenses\": [\n {\n \"compact\": \"socw\",\n \"compactEligibility\": \"ineligible\",\n \"dateOfExpiration\": \"2719-06-13\",\n \"dateOfIssuance\": \"2917-11-03\",\n \"dateOfUpdate\": \"\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"homeAddressCity\": \"\",\n \"homeAddressPostalCode\": \"\",\n \"homeAddressState\": \"\",\n \"homeAddressStreet1\": \"\",\n \"jurisdiction\": \"ct\",\n \"jurisdictionUploadedCompactEligibility\": \"eligible\",\n \"jurisdictionUploadedLicenseStatus\": \"inactive\",\n \"licenseNumber\": \"\",\n \"licenseScope\": \"single-state\",\n \"licenseStatus\": \"inactive\",\n \"licenseType\": \"\",\n \"providerId\": \"3406a2cc-3575-4d18-b2fa-4e951b29872a\",\n \"type\": \"license-home\",\n \"homeAddressStreet2\": \"\",\n \"investigations\": [\n {\n \"compact\": \"socw\",\n \"creationDate\": \"\",\n \"dateOfUpdate\": \"\",\n \"investigationId\": \"\",\n \"jurisdiction\": \"sd\",\n \"licenseType\": \"\",\n \"providerId\": \"bf29e273-ebd5-4f91-99bf-07a52c4d4c2c\",\n \"submittingUser\": \"\",\n \"type\": \"investigation\"\n },\n {\n \"compact\": \"socw\",\n \"creationDate\": \"\",\n \"dateOfUpdate\": \"\",\n \"investigationId\": \"\",\n \"jurisdiction\": \"mp\",\n \"licenseType\": \"\",\n \"providerId\": \"c60a3c93-ae6a-4f6f-9e0d-1c51bc4d58bc\",\n \"submittingUser\": \"\",\n \"type\": \"investigation\"\n }\n ],\n \"suffix\": \"\",\n \"emailAddress\": \"\",\n \"dateOfRenewal\": \"2747-02-21\",\n \"investigationStatus\": \"underInvestigation\",\n \"phoneNumber\": \"+656150136828\",\n \"licenseStatusName\": \"\",\n \"middleName\": \"\",\n \"adverseActions\": [\n {\n \"actionAgainst\": \"privilege\",\n \"adverseActionId\": \"\",\n \"compact\": \"socw\",\n \"creationDate\": \"2203-12-31\",\n \"dateOfUpdate\": \"\",\n \"effectiveStartDate\": \"2517-12-05\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"mp\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"b8fc7b9c-e18c-42b2-95fb-7f67c2b4acbb\",\n \"submittingUser\": \"\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"Fraud, Deception, or Misrepresentation\",\n \"Improper Prescribing, Dispensing, Administering Medication/Drug Violation\"\n ],\n \"effectiveLiftDate\": \"1754-10-30\",\n \"liftingUser\": \"\"\n },\n {\n \"actionAgainst\": \"privilege\",\n \"adverseActionId\": \"\",\n \"compact\": \"socw\",\n \"creationDate\": \"2716-10-31\",\n \"dateOfUpdate\": \"\",\n \"effectiveStartDate\": \"1051-04-31\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"ct\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"700c646b-dfde-4ef8-9ec5-462c68e94899\",\n \"submittingUser\": \"\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"Other\",\n \"Conflict of Interest\"\n ],\n \"effectiveLiftDate\": \"1416-04-12\",\n \"liftingUser\": \"\"\n }\n ]\n },\n {\n \"compact\": \"socw\",\n \"compactEligibility\": \"eligible\",\n \"dateOfExpiration\": \"2392-11-18\",\n \"dateOfIssuance\": \"1596-02-08\",\n \"dateOfUpdate\": \"\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"homeAddressCity\": \"\",\n \"homeAddressPostalCode\": \"\",\n \"homeAddressState\": \"\",\n \"homeAddressStreet1\": \"\",\n \"jurisdiction\": \"ut\",\n \"jurisdictionUploadedCompactEligibility\": \"ineligible\",\n \"jurisdictionUploadedLicenseStatus\": \"inactive\",\n \"licenseNumber\": \"\",\n \"licenseScope\": \"single-state\",\n \"licenseStatus\": \"inactive\",\n \"licenseType\": \"\",\n \"providerId\": \"16eb77d3-79f8-464f-a05d-c148911a67d6\",\n \"type\": \"license-home\",\n \"homeAddressStreet2\": \"\",\n \"investigations\": [\n {\n \"compact\": \"socw\",\n \"creationDate\": \"\",\n \"dateOfUpdate\": \"\",\n \"investigationId\": \"\",\n \"jurisdiction\": \"or\",\n \"licenseType\": \"\",\n \"providerId\": \"834e991d-dbd3-4bd4-b4d8-152006b56bfc\",\n \"submittingUser\": \"\",\n \"type\": \"investigation\"\n },\n {\n \"compact\": \"socw\",\n \"creationDate\": \"\",\n \"dateOfUpdate\": \"\",\n \"investigationId\": \"\",\n \"jurisdiction\": \"al\",\n \"licenseType\": \"\",\n \"providerId\": \"702e39cc-3b4f-42b0-8bbb-4a985cf5c434\",\n \"submittingUser\": \"\",\n \"type\": \"investigation\"\n }\n ],\n \"suffix\": \"\",\n \"emailAddress\": \"\",\n \"dateOfRenewal\": \"1954-01-31\",\n \"investigationStatus\": \"underInvestigation\",\n \"phoneNumber\": \"+554302269537\",\n \"licenseStatusName\": \"\",\n \"middleName\": \"\",\n \"adverseActions\": [\n {\n \"actionAgainst\": \"license\",\n \"adverseActionId\": \"\",\n \"compact\": \"socw\",\n \"creationDate\": \"1181-04-08\",\n \"dateOfUpdate\": \"\",\n \"effectiveStartDate\": \"1562-11-11\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"sd\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"735d8816-eac0-4198-8d0c-250d8cd7a2a2\",\n \"submittingUser\": \"\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"Confidentiality, Consent or Disclosure Violations\",\n \"Improper Supervision or Allowing Unlicensed Practice\"\n ],\n \"effectiveLiftDate\": \"2802-08-30\",\n \"liftingUser\": \"\"\n },\n {\n \"actionAgainst\": \"privilege\",\n \"adverseActionId\": \"\",\n \"compact\": \"socw\",\n \"creationDate\": \"1940-03-31\",\n \"dateOfUpdate\": \"\",\n \"effectiveStartDate\": \"1009-12-22\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"il\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"a022c820-3a27-4ca0-b990-86b8734dee6d\",\n \"submittingUser\": \"\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"Conflict of Interest\",\n \"Conflict of Interest\"\n ],\n \"effectiveLiftDate\": \"2458-10-31\",\n \"liftingUser\": \"\"\n }\n ]\n }\n ],\n \"middleName\": \"\",\n \"compactConnectRegisteredEmailAddress\": \"\"\n },\n {\n \"birthMonthDay\": \"07-11\",\n \"compact\": \"socw\",\n \"compactEligibility\": \"eligible\",\n \"dateOfExpiration\": \"\",\n \"dateOfUpdate\": \"\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"jurisdictionUploadedCompactEligibility\": \"eligible\",\n \"jurisdictionUploadedLicenseStatus\": \"inactive\",\n \"licenseJurisdiction\": \"gu\",\n \"licenseStatus\": \"active\",\n \"providerId\": \"654c64f5-8b20-4c9b-9e12-5a013e0e5993\",\n \"type\": \"provider\",\n \"privileges\": [\n {\n \"administratorSetStatus\": \"inactive\",\n \"compact\": \"socw\",\n \"dateOfExpiration\": \"2194-11-31\",\n \"jurisdiction\": \"al\",\n \"licenseJurisdiction\": \"ky\",\n \"licenseType\": \"\",\n \"providerId\": \"82d59d6d-f790-4572-9ab7-b1f79129c978\",\n \"status\": \"active\",\n \"type\": \"privilege\",\n \"investigationStatus\": \"underInvestigation\",\n \"investigations\": [\n {\n \"compact\": \"socw\",\n \"creationDate\": \"\",\n \"dateOfUpdate\": \"\",\n \"investigationId\": \"\",\n \"jurisdiction\": \"mn\",\n \"licenseType\": \"\",\n \"providerId\": \"3e576200-bf92-4748-8332-fd7e0de924cb\",\n \"submittingUser\": \"\",\n \"type\": \"investigation\"\n },\n {\n \"compact\": \"socw\",\n \"creationDate\": \"\",\n \"dateOfUpdate\": \"\",\n \"investigationId\": \"\",\n \"jurisdiction\": \"hi\",\n \"licenseType\": \"\",\n \"providerId\": \"03c976db-51c7-43fc-9d30-9013a2d9a76e\",\n \"submittingUser\": \"\",\n \"type\": \"investigation\"\n }\n ],\n \"compactTransactionId\": \"\",\n \"adverseActions\": [\n {\n \"actionAgainst\": \"license\",\n \"adverseActionId\": \"\",\n \"compact\": \"socw\",\n \"creationDate\": \"2793-10-05\",\n \"dateOfUpdate\": \"\",\n \"effectiveStartDate\": \"1015-10-31\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"nh\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"22a3c23e-27a7-4d78-885c-92b9e8aca102\",\n \"submittingUser\": \"\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"Improper Supervision or Allowing Unlicensed Practice\",\n \"Substandard Care or Patient Neglect/Abuse\"\n ],\n \"effectiveLiftDate\": \"1698-10-30\",\n \"liftingUser\": \"\"\n },\n {\n \"actionAgainst\": \"license\",\n \"adverseActionId\": \"\",\n \"compact\": \"socw\",\n \"creationDate\": \"1406-11-30\",\n \"dateOfUpdate\": \"\",\n \"effectiveStartDate\": \"2762-10-09\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"la\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"0a023cae-47e7-494f-bc1b-1e37c7861cab\",\n \"submittingUser\": \"\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"Improper Supervision or Allowing Unlicensed Practice\",\n \"Improper Supervision or Allowing Unlicensed Practice\"\n ],\n \"effectiveLiftDate\": \"2254-10-30\",\n \"liftingUser\": \"\"\n }\n ]\n },\n {\n \"administratorSetStatus\": \"active\",\n \"compact\": \"socw\",\n \"dateOfExpiration\": \"2507-11-27\",\n \"jurisdiction\": \"al\",\n \"licenseJurisdiction\": \"mi\",\n \"licenseType\": \"\",\n \"providerId\": \"7f3de217-36c2-4382-aa9e-5f4e7b3d5ab3\",\n \"status\": \"inactive\",\n \"type\": \"privilege\",\n \"investigationStatus\": \"underInvestigation\",\n \"investigations\": [\n {\n \"compact\": \"socw\",\n \"creationDate\": \"\",\n \"dateOfUpdate\": \"\",\n \"investigationId\": \"\",\n \"jurisdiction\": \"mt\",\n \"licenseType\": \"\",\n \"providerId\": \"4f869331-7d6c-47e0-9328-72af5b40fa9c\",\n \"submittingUser\": \"\",\n \"type\": \"investigation\"\n },\n {\n \"compact\": \"socw\",\n \"creationDate\": \"\",\n \"dateOfUpdate\": \"\",\n \"investigationId\": \"\",\n \"jurisdiction\": \"ky\",\n \"licenseType\": \"\",\n \"providerId\": \"09ff7934-dd72-4bbd-9c93-d85c56735f88\",\n \"submittingUser\": \"\",\n \"type\": \"investigation\"\n }\n ],\n \"compactTransactionId\": \"\",\n \"adverseActions\": [\n {\n \"actionAgainst\": \"license\",\n \"adverseActionId\": \"\",\n \"compact\": \"socw\",\n \"creationDate\": \"1555-09-24\",\n \"dateOfUpdate\": \"\",\n \"effectiveStartDate\": \"2190-12-03\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"ms\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"f6389f81-6866-4256-9fb0-eed708ce0761\",\n \"submittingUser\": \"\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"Conflict of Interest\",\n \"Fraud, Deception, or Misrepresentation\"\n ],\n \"effectiveLiftDate\": \"1025-01-16\",\n \"liftingUser\": \"\"\n },\n {\n \"actionAgainst\": \"license\",\n \"adverseActionId\": \"\",\n \"compact\": \"socw\",\n \"creationDate\": \"1986-11-31\",\n \"dateOfUpdate\": \"\",\n \"effectiveStartDate\": \"2887-06-31\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"ms\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"f184cfde-f616-4772-b453-bceb759166a3\",\n \"submittingUser\": \"\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"Improper Supervision or Allowing Unlicensed Practice\",\n \"Criminal Conviction or Adjudication\"\n ],\n \"effectiveLiftDate\": \"2776-03-08\",\n \"liftingUser\": \"\"\n }\n ]\n }\n ],\n \"suffix\": \"\",\n \"currentHomeJurisdiction\": \"wv\",\n \"licenses\": [\n {\n \"compact\": \"socw\",\n \"compactEligibility\": \"ineligible\",\n \"dateOfExpiration\": \"1225-11-31\",\n \"dateOfIssuance\": \"1784-11-29\",\n \"dateOfUpdate\": \"\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"homeAddressCity\": \"\",\n \"homeAddressPostalCode\": \"\",\n \"homeAddressState\": \"\",\n \"homeAddressStreet1\": \"\",\n \"jurisdiction\": \"wi\",\n \"jurisdictionUploadedCompactEligibility\": \"ineligible\",\n \"jurisdictionUploadedLicenseStatus\": \"inactive\",\n \"licenseNumber\": \"\",\n \"licenseScope\": \"single-state\",\n \"licenseStatus\": \"inactive\",\n \"licenseType\": \"\",\n \"providerId\": \"f5cb86d4-459f-45a0-8231-b1675a2f0092\",\n \"type\": \"license-home\",\n \"homeAddressStreet2\": \"\",\n \"investigations\": [\n {\n \"compact\": \"socw\",\n \"creationDate\": \"\",\n \"dateOfUpdate\": \"\",\n \"investigationId\": \"\",\n \"jurisdiction\": \"ma\",\n \"licenseType\": \"\",\n \"providerId\": \"8ba14bcb-8b5e-47a0-a91e-61a94bf23f6c\",\n \"submittingUser\": \"\",\n \"type\": \"investigation\"\n },\n {\n \"compact\": \"socw\",\n \"creationDate\": \"\",\n \"dateOfUpdate\": \"\",\n \"investigationId\": \"\",\n \"jurisdiction\": \"mp\",\n \"licenseType\": \"\",\n \"providerId\": \"06d08341-6091-4851-97f7-5ad2913df2b9\",\n \"submittingUser\": \"\",\n \"type\": \"investigation\"\n }\n ],\n \"suffix\": \"\",\n \"emailAddress\": \"\",\n \"dateOfRenewal\": \"1430-11-30\",\n \"investigationStatus\": \"underInvestigation\",\n \"phoneNumber\": \"+747044226\",\n \"licenseStatusName\": \"\",\n \"middleName\": \"\",\n \"adverseActions\": [\n {\n \"actionAgainst\": \"privilege\",\n \"adverseActionId\": \"\",\n \"compact\": \"socw\",\n \"creationDate\": \"2740-04-07\",\n \"dateOfUpdate\": \"\",\n \"effectiveStartDate\": \"2132-02-13\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"nm\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"a3373a49-70c3-4880-8b3c-07df385689a1\",\n \"submittingUser\": \"\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"Improper Prescribing, Dispensing, Administering Medication/Drug Violation\",\n \"Improper Prescribing, Dispensing, Administering Medication/Drug Violation\"\n ],\n \"effectiveLiftDate\": \"2605-12-17\",\n \"liftingUser\": \"\"\n },\n {\n \"actionAgainst\": \"privilege\",\n \"adverseActionId\": \"\",\n \"compact\": \"socw\",\n \"creationDate\": \"1701-02-14\",\n \"dateOfUpdate\": \"\",\n \"effectiveStartDate\": \"1957-05-30\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"mt\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"d0f65595-250f-4ec3-b2c1-7b4f6bbed521\",\n \"submittingUser\": \"\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"Conflict of Interest\",\n \"Other\"\n ],\n \"effectiveLiftDate\": \"1390-02-05\",\n \"liftingUser\": \"\"\n }\n ]\n },\n {\n \"compact\": \"socw\",\n \"compactEligibility\": \"eligible\",\n \"dateOfExpiration\": \"2020-07-30\",\n \"dateOfIssuance\": \"1235-05-28\",\n \"dateOfUpdate\": \"\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"homeAddressCity\": \"\",\n \"homeAddressPostalCode\": \"\",\n \"homeAddressState\": \"\",\n \"homeAddressStreet1\": \"\",\n \"jurisdiction\": \"nc\",\n \"jurisdictionUploadedCompactEligibility\": \"eligible\",\n \"jurisdictionUploadedLicenseStatus\": \"inactive\",\n \"licenseNumber\": \"\",\n \"licenseScope\": \"multi-state\",\n \"licenseStatus\": \"inactive\",\n \"licenseType\": \"\",\n \"providerId\": \"c7a43631-4ba7-4c46-941e-ca74ad50bdf5\",\n \"type\": \"license-home\",\n \"homeAddressStreet2\": \"\",\n \"investigations\": [\n {\n \"compact\": \"socw\",\n \"creationDate\": \"\",\n \"dateOfUpdate\": \"\",\n \"investigationId\": \"\",\n \"jurisdiction\": \"de\",\n \"licenseType\": \"\",\n \"providerId\": \"9a83042e-0697-4b60-9543-e06a158f54d6\",\n \"submittingUser\": \"\",\n \"type\": \"investigation\"\n },\n {\n \"compact\": \"socw\",\n \"creationDate\": \"\",\n \"dateOfUpdate\": \"\",\n \"investigationId\": \"\",\n \"jurisdiction\": \"in\",\n \"licenseType\": \"\",\n \"providerId\": \"3245aa1b-9740-495a-ac29-7211d041318f\",\n \"submittingUser\": \"\",\n \"type\": \"investigation\"\n }\n ],\n \"suffix\": \"\",\n \"emailAddress\": \"\",\n \"dateOfRenewal\": \"1323-04-11\",\n \"investigationStatus\": \"underInvestigation\",\n \"phoneNumber\": \"+37186868032\",\n \"licenseStatusName\": \"\",\n \"middleName\": \"\",\n \"adverseActions\": [\n {\n \"actionAgainst\": \"privilege\",\n \"adverseActionId\": \"\",\n \"compact\": \"socw\",\n \"creationDate\": \"2672-08-31\",\n \"dateOfUpdate\": \"\",\n \"effectiveStartDate\": \"1401-12-30\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"tx\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"ebc40f6b-7596-4281-a612-cf6c1490847f\",\n \"submittingUser\": \"\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"Confidentiality, Consent or Disclosure Violations\",\n \"Confidentiality, Consent or Disclosure Violations\"\n ],\n \"effectiveLiftDate\": \"2004-10-30\",\n \"liftingUser\": \"\"\n },\n {\n \"actionAgainst\": \"privilege\",\n \"adverseActionId\": \"\",\n \"compact\": \"socw\",\n \"creationDate\": \"1485-04-31\",\n \"dateOfUpdate\": \"\",\n \"effectiveStartDate\": \"1258-12-22\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"nd\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"f524f154-b676-4ff6-b845-f7f12792d6e0\",\n \"submittingUser\": \"\",\n \"type\": \"adverseAction\",\n \"clinicalPrivilegeActionCategories\": [\n \"Other\",\n \"Improper Prescribing, Dispensing, Administering Medication/Drug Violation\"\n ],\n \"effectiveLiftDate\": \"1258-11-10\",\n \"liftingUser\": \"\"\n }\n ]\n }\n ],\n \"middleName\": \"\",\n \"compactConnectRegisteredEmailAddress\": \"\"\n }\n ],\n \"total\": {\n \"value\": \"\",\n \"relation\": \"gte\"\n },\n \"lastSort\": \"\"\n}", "code": 200, "cookie": [], "header": [ @@ -474,7 +474,7 @@ "value": "application/json" } ], - "id": "4254c514-75ba-411a-9338-384221027c68", + "id": "b57a6d5a-7b4e-4d89-83e5-996a1a6d013a", "name": "200 response", "originalRequest": { "body": { diff --git a/backend/social-work-app/lambdas/nodejs/package.json b/backend/social-work-app/lambdas/nodejs/package.json index 7d007fc78b..763bd27246 100644 --- a/backend/social-work-app/lambdas/nodejs/package.json +++ b/backend/social-work-app/lambdas/nodejs/package.json @@ -5,7 +5,8 @@ "description": "NodeJS lambdas for CompactConnect", "resolutions": { "fast-xml-parser": "5.7.3", - "postcss": "8.5.24" + "postcss": "8.5.24", + "sanitize-html": "2.17.5" }, "scripts": { "build": "tsc", diff --git a/backend/social-work-app/lambdas/nodejs/yarn.lock b/backend/social-work-app/lambdas/nodejs/yarn.lock index e6daa62f01..3c986c512f 100644 --- a/backend/social-work-app/lambdas/nodejs/yarn.lock +++ b/backend/social-work-app/lambdas/nodejs/yarn.lock @@ -3311,6 +3311,11 @@ csstype@^3.2.2: resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.2.3.tgz#ec48c0f3e993e50648c86da559e2610995cf989a" integrity sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ== +dayjs@^1.11.7: + version "1.11.21" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.21.tgz#57f87562e62de76f3c704bd2b8d522fc33068eb2" + integrity sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA== + debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.5, debug@^4.4.3: version "4.4.3" resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" @@ -3386,7 +3391,7 @@ domhandler@^5.0.2, domhandler@^5.0.3: dependencies: domelementtype "^2.3.0" -domutils@^3.0.1: +domutils@^3.2.2: version "3.2.2" resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.2.2.tgz#edbfe2b668b0c1d97c24baf0f1062b132221bc78" integrity sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw== @@ -3430,11 +3435,16 @@ enabled@2.0.x: resolved "https://registry.yarnpkg.com/enabled/-/enabled-2.0.0.tgz#f9dd92ec2d6f4bbc0d5d1e64e21d61cd4665e7c2" integrity sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ== -entities@^4.2.0, entities@^4.4.0: +entities@^4.2.0: version "4.5.0" resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== +entities@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/entities/-/entities-7.0.1.tgz#26e8a88889db63417dcb9a1e79a3f1bc92b5976b" + integrity sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA== + error-ex@^1.3.1: version "1.3.4" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.4.tgz#b3a8d8bb6f92eecc1629e3e27d3c8607a8a32414" @@ -3856,15 +3866,15 @@ html-escaper@^2.0.0: resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== -htmlparser2@^8.0.0: - version "8.0.2" - resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-8.0.2.tgz#f002151705b383e62433b5cf466f5b716edaec21" - integrity sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA== +htmlparser2@^10.1.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-10.1.0.tgz#fe3f2e12c73b6e462d4e10395db9c1119e4d6ae4" + integrity sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ== dependencies: domelementtype "^2.3.0" domhandler "^5.0.3" - domutils "^3.0.1" - entities "^4.4.0" + domutils "^3.2.2" + entities "^7.0.1" human-signals@^2.1.0: version "2.1.0" @@ -4447,6 +4457,13 @@ lambda-local@^2.2.0: dotenv "^16.3.1" winston "^3.10.0" +launder@^1.7.1: + version "1.7.1" + resolved "https://registry.yarnpkg.com/launder/-/launder-1.7.1.tgz#ef7155ab0c3ddec2323089c961d2e9a249aa5b0d" + integrity sha512-mU6WRz5EusL9ZZuiZ5SO4Y6C0P9PAUR9iwdb6bzj4KDihm28DiHFw+/yk9DBH4f+Pv1wuzQ4e2jV3oQ7mkIqvw== + dependencies: + dayjs "^1.11.7" + leven@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" @@ -4982,15 +4999,16 @@ safe-stable-stringify@^2.3.1: resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz#4ca2f8e385f2831c432a719b108a3bf7af42a1dd" integrity sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA== -sanitize-html@^2.17.0: - version "2.17.0" - resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.17.0.tgz#a8f66420a6be981d8fe412e3397cc753782598e4" - integrity sha512-dLAADUSS8rBwhaevT12yCezvioCA+bmUTPH/u57xKPT8d++voeYE6HeluA/bPbQ15TwDBG2ii+QZIEmYx8VdxA== +sanitize-html@2.17.5, sanitize-html@^2.17.0: + version "2.17.5" + resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.17.5.tgz#bef1d103773760e1c52a01b1f58178bee9c10556" + integrity sha512-ZmU1joGRrvoyctKIiuwUxqR6moLoU2Wk+2bMccN6f7UwhAmwYDvWziqPxRDDN2Qip62NqnIrVrT9akbL6Wretg== dependencies: deepmerge "^4.2.2" escape-string-regexp "^4.0.0" - htmlparser2 "^8.0.0" + htmlparser2 "^10.1.0" is-plain-object "^5.0.0" + launder "^1.7.1" parse-srcset "^1.0.2" postcss "^8.3.11" diff --git a/backend/social-work-app/lambdas/python/common/cc_common/data_model/data_client.py b/backend/social-work-app/lambdas/python/common/cc_common/data_model/data_client.py index 960557990e..8c43d7c993 100644 --- a/backend/social-work-app/lambdas/python/common/cc_common/data_model/data_client.py +++ b/backend/social-work-app/lambdas/python/common/cc_common/data_model/data_client.py @@ -72,6 +72,34 @@ def get_or_create_provider_id(self, *, compact: str, ssn: str) -> str: raise return provider_id + @logger_inject_kwargs(logger, 'compact') + def claim_cuid_number(self, compact: str) -> int: + """ + Claim a unique Compact Unique Identifier (CUID) counter value for a compact by atomically incrementing + the CUID counter. If the counter doesn't exist yet, it will be created with an initial value of 1. + + This is a single ADD-based UpdateItem, which is atomic under concurrency: numbers may be skipped + (if a caller claims one but never uses it), but are never reused. + """ + logger.info('Claiming CUID number') + resp = self.config.provider_table.update_item( + Key={ + 'pk': f'{compact}#CUID_COUNT', + 'sk': f'{compact}#CUID_COUNT', + }, + UpdateExpression='ADD #count :increment', + ExpressionAttributeNames={ + '#count': 'cuidCount', + }, + ExpressionAttributeValues={ + ':increment': 1, + }, + ReturnValues='UPDATED_NEW', + ) + cuid_count = resp['Attributes']['cuidCount'] + logger.info('Claimed CUID number', cuid_count=cuid_count) + return cuid_count + @logger_inject_kwargs(logger, 'compact', 'provider_id') def get_ssn_by_provider_id(self, *, compact: str, provider_id: str) -> str: logger.info('Getting ssn by provider id', compact=compact, provider_id=provider_id) diff --git a/backend/social-work-app/lambdas/python/common/cc_common/data_model/provider_record_util.py b/backend/social-work-app/lambdas/python/common/cc_common/data_model/provider_record_util.py index 6d74af92ba..28c123fc51 100644 --- a/backend/social-work-app/lambdas/python/common/cc_common/data_model/provider_record_util.py +++ b/backend/social-work-app/lambdas/python/common/cc_common/data_model/provider_record_util.py @@ -113,14 +113,63 @@ def find_most_recently_issued_or_renewed_license( return latest_licenses[0] @staticmethod - def populate_provider_record(current_provider_record: ProviderData | None, license_record: dict) -> ProviderData: + def has_paired_single_and_multi_state_license(license_records: Iterable[dict]) -> bool: + """ + Determine whether the given license records contain a matching pair of a single-state and a + multi-state license: same jurisdiction and same licenseType, one of each scope. + + Eligibility and active status are deliberately not considered here, matching the pairing semantics + already used by ``find_most_recently_issued_or_renewed_license``. + + :param license_records: An iterable of license records (dicts) + :return: True if a matching single-state/multi-state pair exists, otherwise False + """ + single_state_keys = { + (record['jurisdiction'], record['licenseType']) + for record in license_records + if record['licenseScope'] == LicenseScopeEnum.SINGLE_STATE.value + } + multi_state_keys = { + (record['jurisdiction'], record['licenseType']) + for record in license_records + if record['licenseScope'] == LicenseScopeEnum.MULTI_STATE.value + } + return not single_state_keys.isdisjoint(multi_state_keys) + + @staticmethod + def populate_provider_record( + current_provider_record: ProviderData | None, + license_record: dict, + *, + public_compact_identifier: str | None = None, + ) -> ProviderData: """ Create a provider record from a license record. :param current_provider_record: The current provider record to update if it currently exists. :param license_record: The license record to use as a basis for the provider record + :param public_compact_identifier: A newly-minted CUID to set on the record, if one was just assigned. + Note: an existing publicCompactIdentifier on current_provider_record already survives via + **current_provider_record.to_dict() below; this parameter must never be used to overwrite an + existing value, since the CUID is write-once. :return: A provider record ready to be persisted - """ + :raises CCInternalException: If a new CUID is supplied for a provider that already has one + """ + if ( + current_provider_record is not None + and current_provider_record.publicCompactIdentifier + and public_compact_identifier is not None + ): + # The CUID is a permanent, public-facing identifier. Reaching this point means a caller + # decided to mint one for a practitioner who already had one, which would silently + # replace an identifier the public may already be using to look them up. Fail loudly + # rather than let the overwrite through. + raise CCInternalException( + 'publicCompactIdentifier cannot be overwritten once set. Attempted to assign ' + f'{public_compact_identifier} to a provider that already has ' + f'{current_provider_record.publicCompactIdentifier}.' + ) + if current_provider_record is None: return ProviderData.create_new( { @@ -128,6 +177,7 @@ def populate_provider_record(current_provider_record: ProviderData | None, licen 'compact': license_record['compact'], 'licenseJurisdiction': license_record['jurisdiction'], **license_record, + **({'publicCompactIdentifier': public_compact_identifier} if public_compact_identifier else {}), } ) # else populate the current fields of the provider record first before updating with @@ -140,6 +190,7 @@ def populate_provider_record(current_provider_record: ProviderData | None, licen 'licenseJurisdiction': license_record['jurisdiction'], # now override the key values on the current provider record with the new license record **license_record, + **({'publicCompactIdentifier': public_compact_identifier} if public_compact_identifier else {}), } ) diff --git a/backend/social-work-app/lambdas/python/common/cc_common/data_model/schema/common.py b/backend/social-work-app/lambdas/python/common/cc_common/data_model/schema/common.py index d83edc4645..0fc343cc15 100644 --- a/backend/social-work-app/lambdas/python/common/cc_common/data_model/schema/common.py +++ b/backend/social-work-app/lambdas/python/common/cc_common/data_model/schema/common.py @@ -403,6 +403,11 @@ class LicenseScopeEnum(CCEnum): MULTI_STATE = 'multi-state' +CUID_PREFIX = 'SWC' +# Compact Unique Identifier: SWC-<4-digit random>-, e.g. SWC-4548-1 +CUID_PATTERN = r'^SWC-[0-9]{4}-[1-9][0-9]*$' + + def provider_pk(compact: str, provider_id) -> str: """Return the partition key shared by provider-scoped records in the provider data table.""" return f'{compact}#PROVIDER#{provider_id}' diff --git a/backend/social-work-app/lambdas/python/common/cc_common/data_model/schema/fields.py b/backend/social-work-app/lambdas/python/common/cc_common/data_model/schema/fields.py index 634092f98a..47123064dd 100644 --- a/backend/social-work-app/lambdas/python/common/cc_common/data_model/schema/fields.py +++ b/backend/social-work-app/lambdas/python/common/cc_common/data_model/schema/fields.py @@ -3,6 +3,7 @@ from cc_common.config import config from cc_common.data_model.schema.common import ( + CUID_PATTERN, ActiveInactiveStatus, ClinicalPrivilegeActionCategory, CompactEligibilityStatus, @@ -107,6 +108,13 @@ def __init__(self, *args, **kwargs): super().__init__(*args, validate=OneOf([entry.value for entry in LicenseScopeEnum]), **kwargs) +class PublicCompactIdentifierField(String): + """Validates values against the Compact Unique Identifier (CUID) format: SWC-<4-digit random>-.""" + + def __init__(self, *args, **kwargs): + super().__init__(*args, validate=Regexp(CUID_PATTERN), **kwargs) + + class ITUTE164PhoneNumber(String): """Phone number format consistent with ITU-T E.164: https://www.itu.int/rec/T-REC-E.164-201011-I/en diff --git a/backend/social-work-app/lambdas/python/common/cc_common/data_model/schema/provider/__init__.py b/backend/social-work-app/lambdas/python/common/cc_common/data_model/schema/provider/__init__.py index 8f4cdf9d3d..9ee8930448 100644 --- a/backend/social-work-app/lambdas/python/common/cc_common/data_model/schema/provider/__init__.py +++ b/backend/social-work-app/lambdas/python/common/cc_common/data_model/schema/provider/__init__.py @@ -85,6 +85,10 @@ def compactEligibility(self) -> str: def licenseStatus(self) -> str | None: return self._data.get('licenseStatus') + @property + def publicCompactIdentifier(self) -> str | None: + return self._data.get('publicCompactIdentifier') + class ProviderUpdateData(CCDataClass): """ diff --git a/backend/social-work-app/lambdas/python/common/cc_common/data_model/schema/provider/api.py b/backend/social-work-app/lambdas/python/common/cc_common/data_model/schema/provider/api.py index d450ca742a..d88a7b035b 100644 --- a/backend/social-work-app/lambdas/python/common/cc_common/data_model/schema/provider/api.py +++ b/backend/social-work-app/lambdas/python/common/cc_common/data_model/schema/provider/api.py @@ -1,17 +1,20 @@ # ruff: noqa: N801, N815, ARG002 invalid-name unused-argument -from marshmallow import ValidationError, validates_schema -from marshmallow.fields import Integer, List, Nested, Raw, String -from marshmallow.validate import Length, Range, Regexp +import re + +from marshmallow import ValidationError, pre_load +from marshmallow.fields import Dict, Integer, List, Nested, Raw, String +from marshmallow.validate import Length, OneOf, Range, Regexp from cc_common.data_model.schema.adverse_action.api import AdverseActionGeneralResponseSchema -from cc_common.data_model.schema.base_record import ForgivingSchema -from cc_common.data_model.schema.common import CCRequestSchema +from cc_common.data_model.schema.base_record import ForgivingSchema, StrictSchema +from cc_common.data_model.schema.common import CUID_PATTERN, CCRequestSchema from cc_common.data_model.schema.fields import ( ActiveInactive, Compact, CompactEligibility, Jurisdiction, LicenseScopeField, + PublicCompactIdentifierField, ) from cc_common.data_model.schema.license.api import ( LicenseGeneralResponseSchema, @@ -57,6 +60,193 @@ def _validate_no_cross_index_keys(obj, path: str = 'query') -> None: # Scalar values (str, int, bool, None) are safe - we only check keys +# Maximum nesting depth accepted in a caller-supplied query. The deepest query the frontend can +# build is 13 levels (the encumbrance-date condition: bool > should > nested > query > nested > +# query > range > field > bound), so this leaves room for the UI to grow while staying well clear +# of the point where recursing through the schema would exhaust the Python stack and surface as an +# unhandled 500 instead of a 400. +_MAX_QUERY_DEPTH = 20 + + +def _validate_query_depth(query) -> None: + """ + Validate that a caller-supplied query is not nested beyond _MAX_QUERY_DEPTH. + + Walked iteratively rather than recursively: this guard has to be able to measure a query that + is deeper than the interpreter's own recursion limit, which is the very thing it exists to + reject. + + :param query: The caller-supplied query body + :raises ValidationError: If the query nests deeper than _MAX_QUERY_DEPTH + """ + stack = [(query, 1)] + while stack: + node, depth = stack.pop() + if depth > _MAX_QUERY_DEPTH: + raise ValidationError(f'Query is too deeply nested (maximum depth: {_MAX_QUERY_DEPTH}).') + if isinstance(node, dict): + stack.extend((value, depth + 1) for value in node.values()) + elif isinstance(node, list): + stack.extend((item, depth + 1) for item in node) + + +def _validate_query_input(query) -> None: + """ + Run the checks that must happen before a query is deserialized through the schema. + + Ordering matters. The depth guard runs first because ``_validate_no_cross_index_keys`` is itself + recursive and would blow the stack on a pathologically deep query. Both run ahead of field + deserialization (from a ``pre_load`` hook) so that a cross-index attempt is always reported as + such, rather than being pre-empted by, or skipped because of, an unrelated field error. + + :param query: The caller-supplied query body + :raises ValidationError: If the query is too deeply nested or attempts a cross-index lookup + """ + _validate_query_depth(query) + _validate_no_cross_index_keys(query) + + +# Keys permitted inside a nested clause's `inner_hits`. Deliberately excludes `_source`, so a caller +# cannot use inner_hits to select which nested fields come back. +_ALLOWED_INNER_HITS_KEYS = ('from', 'name', 'size') + + +class BoolQuerySchema(StrictSchema): + """ + A `bool` query clause. + + Each occupant holds further query clauses, so they recurse back into QueryClauseSchema (via a + lambda, since that schema is defined below). Inheriting StrictSchema is what rejects any bool + key the frontend does not send. + + Serialization direction: + API -> load() -> Python + """ + + must = List(Nested(lambda: QueryClauseSchema()), required=False, allow_none=False) + must_not = List(Nested(lambda: QueryClauseSchema()), required=False, allow_none=False) + should = List(Nested(lambda: QueryClauseSchema()), required=False, allow_none=False) + filter = List(Nested(lambda: QueryClauseSchema()), required=False, allow_none=False) + # Left untyped, matching the previous hand-rolled validation, which allowlisted the key without + # constraining its value. + minimum_should_match = Raw(required=False, allow_none=False) + + +class NestedQuerySchema(StrictSchema): + """ + A `nested` query clause. + + `query` recurses back into QueryClauseSchema, which is what keeps a free-form clause from being + smuggled in below a nested path. + + Serialization direction: + API -> load() -> Python + """ + + path = String(required=True, allow_none=False) + query = Nested(lambda: QueryClauseSchema(), required=True, allow_none=False) + inner_hits = Dict( + keys=String( + validate=OneOf( + _ALLOWED_INNER_HITS_KEYS, + error="'{input}' is not allowed in inner_hits. Allowed: {choices}.", + ) + ), + values=Raw(), + required=False, + allow_none=False, + ) + # Left untyped, matching the previous hand-rolled validation, which allowlisted these keys + # without constraining their values. + score_mode = Raw(required=False, allow_none=False) + ignore_unmapped = Raw(required=False, allow_none=False) + + +class QueryClauseSchema(StrictSchema): + """ + A single OpenSearch DSL query clause, restricted to the clause types the CompactConnect frontend + actually builds (see prepRequestSearchParams in webroot/src/network/searchApi/data.api.ts). + + Because a request is *loaded* through this schema rather than merely inspected, only structure + described here can reach OpenSearch at all. + + The field-keyed clauses take arbitrary field names, so their contents stay Raw: restricting + which fields may be *queried* is the job of the permission checks in the search handler, not of + this schema. Note that `terms` in particular stays permissive so that a terms lookup carrying an + `index` key is still caught by _validate_no_cross_index_keys with a cross-index error. + + Serialization direction: + API -> load() -> Python + """ + + match_all = Dict(required=False, allow_none=False) + term = Dict(keys=String(), values=Raw(), required=False, allow_none=False) + terms = Dict( + keys=String(), + values=List( + Raw(), + error_messages={'invalid': 'A terms query must be a plain list of values.'}, + ), + required=False, + allow_none=False, + ) + match = Dict(keys=String(), values=Raw(), required=False, allow_none=False) + match_phrase_prefix = Dict(keys=String(), values=Raw(), required=False, allow_none=False) + range = Dict(keys=String(), values=Raw(), required=False, allow_none=False) + bool = Nested(BoolQuerySchema, required=False, allow_none=False) + nested = Nested(NestedQuerySchema, required=False, allow_none=False) + + +# Sort shape the search API accepts, derived from what the CompactConnect frontend actually emits +# (see prepRequestSearchParams in webroot/src/network/searchApi/data.api.ts), which is exactly: +# +# sort: [{'.keyword': {'order': 'asc' | 'desc'}}] +# +# This is modelled declaratively rather than walked, so anything not described here is rejected by +# marshmallow itself. +_ALLOWED_SORT_ORDERS = ('asc', 'desc') + +# Fields a caller is permitted to sort by. The frontend only ever sorts by family name, but +# providerId and the date fields are kept available so `search_after` cursor pagination has a unique +# tiebreaker to sort on. +# +# This list is deliberately restricted to non-sensitive fields. Sort values are echoed back to the +# caller as `lastSort` (see the search handler), which is outside the response schema that strips +# restricted fields -- so whatever is sortable is also readable. Sensitive fields (dateOfBirth, +# emailAddress, phoneNumber, home address, ...) are therefore not sortable by anyone, regardless of +# scopes. Note this is independent of *querying*: querying by dateOfBirth is still supported for +# callers holding readPrivate. +# +# Text fields are only listed via their `.keyword` subfield, since OpenSearch rejects sorting on an +# analyzed text field anyway. +_ALLOWED_SORT_FIELDS = ( + 'dateOfExpiration', + 'dateOfUpdate', + 'familyName.keyword', + 'givenName.keyword', + 'providerId', +) + + +class SortOptionsSchema(StrictSchema): + """ + Sort options for a single sorted field, e.g. {'order': 'asc'}. + + Inheriting StrictSchema (unknown = RAISE) is what rejects every sort option the frontend does + not send -- including `nested`, whose `filter` would otherwise be an unvalidated query-DSL + position. + + Serialization direction: + API -> load() -> Python + """ + + order = String( + required=True, + allow_none=False, + validate=OneOf(_ALLOWED_SORT_ORDERS, error="Invalid sort order '{input}'. Allowed values: {choices}."), + ) + + class ProviderReadPrivateResponseSchema(ForgivingSchema): """ Provider object fields that are sanitized for users with the 'readPrivate' permission. @@ -95,6 +285,9 @@ class ProviderReadPrivateResponseSchema(ForgivingSchema): providerDateOfUpdate = Raw(required=False, allow_none=False) birthMonthDay = String(required=True, allow_none=False, validate=Regexp('^[0-1]{1}[0-9]{1}-[0-3]{1}[0-9]{1}')) + # Compact Unique Identifier (CUID); only present once a paired single-state/multi-state license exists. + publicCompactIdentifier = PublicCompactIdentifierField(required=False, allow_none=False) + # these records are present when getting provider information from the GET endpoint # so we check for them here and sanitize them if they are present licenses = List(Nested(LicenseReadPrivateResponseSchema(), required=False, allow_none=False)) @@ -144,6 +337,9 @@ class ProviderGeneralResponseSchema(ForgivingSchema): providerDateOfUpdate = Raw(required=False, allow_none=False) birthMonthDay = String(required=True, allow_none=False, validate=Regexp('^[0-1]{1}[0-9]{1}-[0-3]{1}[0-9]{1}')) + # Compact Unique Identifier (CUID); only present once a paired single-state/multi-state license exists. + publicCompactIdentifier = PublicCompactIdentifierField(required=False, allow_none=False) + # these records are present when getting provider information from the GET endpoint # so we check for them here and sanitize them if they are present licenses = List(Nested(LicenseGeneralResponseSchema(), required=False, allow_none=False)) @@ -194,6 +390,9 @@ class ProviderPublicResponseSchema(ForgivingSchema): familyName = String(required=True, allow_none=False, validate=Length(1, 100)) suffix = String(required=False, allow_none=False, validate=Length(1, 100)) + # Compact Unique Identifier (CUID); only present once a paired single-state/multi-state license exists. + publicCompactIdentifier = PublicCompactIdentifierField(required=False, allow_none=False) + # Unlike the JCC public provider search, which only returns privilege data for a provider, Social Work returns # both licenses and privileges. Adverse actions are also returned (nested under licenses/privileges), but with # NPDB category and other staff-only fields stripped via AdverseActionPublicResponseSchema. @@ -215,6 +414,8 @@ class PublicLicenseSearchResponseSchema(ForgivingSchema): licenseScope = LicenseScopeField(required=True, allow_none=False) licenseNumber = String(required=True, allow_none=False, validate=Length(1, 100)) licenseEligibility = CompactEligibility(required=True, allow_none=False) + # Compact Unique Identifier (CUID); only present once a paired single-state/multi-state license exists. + publicCompactIdentifier = PublicCompactIdentifierField(required=False, allow_none=False) class QueryProvidersRequestSchema(CCRequestSchema): @@ -276,6 +477,18 @@ class PublicQuerySchema(CCRequestSchema): givenName = String(required=False, allow_none=False, validate=Length(min=1, max=100)) familyName = String(required=False, allow_none=False, validate=Length(min=1, max=100)) licenseNumber = String(required=False, allow_none=False, validate=Length(min=1, max=100)) + # Anchored, case-insensitive CUID match. This is the primary input guard: it rejects wildcards, + # partial values, and counter-only lookups before anything is forwarded to OpenSearch. The length cap + # bounds the cost of regex matching against a maliciously long input; a real CUID is never anywhere + # close to this long, since the counter would have to reach an implausible number of digits. + cuid = String( + required=False, + allow_none=False, + validate=[Length(max=64), Regexp(CUID_PATTERN, flags=re.IGNORECASE)], + ) + # Validated for shape here; validated against config.license_types_for_compact(compact) in the handler, + # since the compact is a path parameter and not available to this schema. + licenseType = String(required=False, allow_none=False, validate=Length(min=1, max=100)) query = Nested(PublicQuerySchema, required=True, allow_none=False) pagination = Nested(QueryProvidersRequestSchema.PaginationSchema, required=False, allow_none=False) @@ -287,7 +500,9 @@ class SearchProvidersRequestSchema(CCRequestSchema): Schema for advanced search providers requests. This schema is used to validate incoming requests to the advanced search providers API endpoint. - It accepts an OpenSearch DSL query body for flexible querying of the provider index. + It accepts an OpenSearch DSL query body, restricted to the clause types QueryClauseSchema + declares -- the ones the frontend actually builds. Free-form and scripted clauses are not + accepted; see QueryClauseSchema for why. The request body closely mirrors OpenSearch DSL for pagination using `search_after`. See: https://docs.opensearch.org/latest/search-plugins/searching-data/paginate/#the-search_after-parameter @@ -296,8 +511,8 @@ class SearchProvidersRequestSchema(CCRequestSchema): API -> load() -> Python """ - # The OpenSearch query body - we use Raw to allow the full flexibility of OpenSearch queries - query = Raw(required=True, allow_none=False) + # The OpenSearch query body, restricted to the clause types QueryClauseSchema declares + query = Nested(QueryClauseSchema, required=True, allow_none=False) # Pagination parameters following OpenSearch DSL # 'from' is a reserved word in Python, so we use 'from_' with data_key='from' @@ -306,25 +521,48 @@ class SearchProvidersRequestSchema(CCRequestSchema): # Sort order - required when using search_after pagination # Example: [{"providerId": "asc"}, {"dateOfUpdate": "desc"}] - sort = Raw(required=False, allow_none=False) + sort = List( + Dict( + keys=String( + validate=OneOf( + _ALLOWED_SORT_FIELDS, + error="Sorting by '{input}' is not allowed. Sortable fields: {choices}.", + ) + ), + values=Nested(SortOptionsSchema), + ), + required=False, + allow_none=False, + ) # The search_after parameter for cursor-based pagination # This should be the 'sort' values from the last hit of the previous page # Example: ["provider-uuid-123", "2024-01-15T10:30:00Z"] search_after = Raw(required=False, allow_none=False) - @validates_schema - def validate_no_cross_index_queries(self, data, **kwargs): + @pre_load + def validate_search_request_dsl(self, data, **kwargs): """ - Validate that the query does not contain cross-index lookup attempts. - - This is a defense-in-depth security measure to prevent queries that attempt to access - data from other compact indices. The primary protection is the OpenSearch domain setting - `rest.action.multi.allow_explicit_index: false`, but this validation provides an - additional application-layer check. - - Dangerous patterns blocked: - - Terms lookup with external index: {"terms": {"field": {"index": "other_index", ...}}} - - More like this with external docs: {"more_like_this": {"like": [{"_index": "other_index"}]}} + Validate the caller-supplied OpenSearch DSL in the search request body. + + Three application-layer checks are applied: + + 1. Query clause allowlist: only the structured query types the frontend builds are accepted. + Free-form and scripted clauses are rejected, since they can carry a field reference + inside a string value and thereby slip past field-level permission checks. + 2. Cross-index lookups: queries that attempt to reach another compact's index are blocked. + Note that this application-layer check is currently the only protection against + cross-index lookups. The OpenSearch domain setting + `rest.action.multi.allow_explicit_index: false` would provide a second layer, but it is + not configured on the domain due to issues it causes with blue/green deployments + (see stacks/search_persistent_stack/provider_search_domain.py). + + Dangerous patterns blocked: + - Terms lookup with external index: {"terms": {"field": {"index": "other_index", ...}}} + - More like this with external docs: {"more_like_this": {"like": [{"_index": "other"}]}} """ - _validate_no_cross_index_keys(data.get('query', {})) + # Runs ahead of field deserialization, so `data` is still exactly what the caller sent and + # is not guaranteed to be an object. Anything else is left for the field layer to reject. + if isinstance(data, dict): + _validate_query_input(data.get('query', {})) + return data diff --git a/backend/social-work-app/lambdas/python/common/cc_common/data_model/schema/provider/record.py b/backend/social-work-app/lambdas/python/common/cc_common/data_model/schema/provider/record.py index b3de574de5..9033ee5fc1 100644 --- a/backend/social-work-app/lambdas/python/common/cc_common/data_model/schema/provider/record.py +++ b/backend/social-work-app/lambdas/python/common/cc_common/data_model/schema/provider/record.py @@ -21,6 +21,7 @@ CompactEligibility, Jurisdiction, LicenseEncumberedStatusField, + PublicCompactIdentifierField, UpdateType, ) from cc_common.data_model.update_tier_enum import UpdateTierEnum @@ -53,6 +54,10 @@ class ProviderRecordSchema(BaseRecordSchema): # optional field for setting encumbrance status encumberedStatus = LicenseEncumberedStatusField(required=False, allow_none=False) + # Compact Unique Identifier (CUID); assigned once a paired single-state/multi-state license exists. + # This is write-once: once assigned, it must never be overwritten or removed. + publicCompactIdentifier = PublicCompactIdentifierField(required=False, allow_none=False) + ssnLastFour = String(required=True, allow_none=False) givenName = String(required=True, allow_none=False, validate=Length(1, 100)) middleName = String(required=False, allow_none=False, validate=Length(1, 100)) @@ -174,6 +179,9 @@ class ProviderUpdatePreviousRecordSchema(ForgivingSchema): dateOfExpiration = Date(required=True, allow_none=False) dateOfBirth = Date(required=True, allow_none=False) dateOfUpdate = AwareDateTime(required=True, allow_none=False) + # Optional, since a provider has no CUID until they hold a paired single-state and multi-state + # license. Tracked here so the upload that assigns one leaves an audit trail. + publicCompactIdentifier = PublicCompactIdentifierField(required=False, allow_none=False) @BaseRecordSchema.register_schema('providerUpdate') diff --git a/backend/social-work-app/lambdas/python/common/cc_common/license_util.py b/backend/social-work-app/lambdas/python/common/cc_common/license_util.py index 423840af8a..c31719e195 100644 --- a/backend/social-work-app/lambdas/python/common/cc_common/license_util.py +++ b/backend/social-work-app/lambdas/python/common/cc_common/license_util.py @@ -35,6 +35,33 @@ def get_license_type_by_abbreviation(compact: str, abbreviation: str) -> License logger.error('Invalid license type abbreviation provided.', exc_info=e) raise CCInvalidRequestException(f'Invalid license type abbreviation: {abbreviation}') from e + @staticmethod + def get_license_type_by_name(compact: str, license_type_name: str) -> LicenseType: + """ + Get a license type by its full name within a compact, matched case-insensitively. + + This is the shared source of truth for validating a license type *name* (as opposed to an + abbreviation, see get_license_type_by_abbreviation) against a compact's configured license types. + Returning the canonically-cased LicenseType (rather than just validating) lets callers normalize + user input to the exact casing configured for the compact, instead of assuming a particular casing + convention (e.g. lower-casing) happens to match what was persisted. + + :param compact: The compact code + :param license_type_name: The license type name to look up (case-insensitive) + + :return: LicenseType object + :raises CCInvalidRequestException: If the license type name is not configured for the compact + """ + try: + abbreviations = config.license_type_abbreviations_for_compact(compact) + for name, abbr in abbreviations.items(): + if name.lower() == license_type_name.lower(): + return LicenseType(name=name, abbreviation=abbr) + raise CCInvalidRequestException(f'Invalid license type: {license_type_name}') + except KeyError as e: + logger.error('Invalid license type provided.', exc_info=e) + raise CCInvalidRequestException(f'Invalid license type: {license_type_name}') from e + @staticmethod def get_valid_license_type_abbreviations(compact: str) -> set[str]: """ diff --git a/backend/social-work-app/lambdas/python/common/tests/function/test_data_client.py b/backend/social-work-app/lambdas/python/common/tests/function/test_data_client.py index 4679891c42..f23e5a413c 100644 --- a/backend/social-work-app/lambdas/python/common/tests/function/test_data_client.py +++ b/backend/social-work-app/lambdas/python/common/tests/function/test_data_client.py @@ -877,6 +877,38 @@ def test_close_privilege_investigation_with_encumbrance(self): self.assertEqual(expected_investigation_close, investigation_record) + def test_claim_cuid_number_returns_sequential_values(self): + """claim_cuid_number atomically increments a dedicated counter, returning 1 then 2.""" + from cc_common.data_model.data_client import DataClient + + client = DataClient(self.config) + + self.assertEqual(1, client.claim_cuid_number('socw')) + self.assertEqual(2, client.claim_cuid_number('socw')) + + def test_claim_cuid_number_writes_counter_item_at_expected_key(self): + """The CUID counter is stored at pk/sk = {compact}#CUID_COUNT with attribute cuidCount.""" + from cc_common.data_model.data_client import DataClient + + client = DataClient(self.config) + + client.claim_cuid_number('socw') + + counter_item = self.config.provider_table.get_item(Key={'pk': 'socw#CUID_COUNT', 'sk': 'socw#CUID_COUNT'})[ + 'Item' + ] + self.assertEqual(1, counter_item['cuidCount']) + + def test_claim_cuid_number_is_independent_per_compact(self): + """The CUID counter is scoped per-compact, independent of other compacts' counters.""" + from cc_common.data_model.data_client import DataClient + + client = DataClient(self.config) + + self.assertEqual(1, client.claim_cuid_number('socw')) + self.assertEqual(1, client.claim_cuid_number('some-other-compact')) + self.assertEqual(2, client.claim_cuid_number('socw')) + def test_close_license_investigation_with_encumbrance(self): """Test closing license investigation with encumbrance creation""" from cc_common.data_model.data_client import DataClient diff --git a/backend/social-work-app/lambdas/python/common/tests/resources/api/provider-detail-response.json b/backend/social-work-app/lambdas/python/common/tests/resources/api/provider-detail-response.json index 5850a7c2dc..d755b220ce 100644 --- a/backend/social-work-app/lambdas/python/common/tests/resources/api/provider-detail-response.json +++ b/backend/social-work-app/lambdas/python/common/tests/resources/api/provider-detail-response.json @@ -1,6 +1,7 @@ { "type": "provider", "providerId": "89a6377e-c3a5-40e5-bca5-317ec854c570", + "publicCompactIdentifier": "SWC-1234-1", "ssnLastFour": "1234", "givenName": "Björk", "middleName": "Gunnar", diff --git a/backend/social-work-app/lambdas/python/common/tests/resources/api/provider-response.json b/backend/social-work-app/lambdas/python/common/tests/resources/api/provider-response.json index 66d67cac50..1209140acb 100644 --- a/backend/social-work-app/lambdas/python/common/tests/resources/api/provider-response.json +++ b/backend/social-work-app/lambdas/python/common/tests/resources/api/provider-response.json @@ -6,6 +6,7 @@ "familyName": "Gu\u00f0mundsd\u00f3ttir", "licenseStatus": "active", "compactEligibility": "eligible", + "publicCompactIdentifier": "SWC-1234-1", "jurisdictionUploadedLicenseStatus": "active", "jurisdictionUploadedCompactEligibility": "eligible", "compact": "socw", diff --git a/backend/social-work-app/lambdas/python/common/tests/resources/dynamo/provider.json b/backend/social-work-app/lambdas/python/common/tests/resources/dynamo/provider.json index ce6627cf51..41b84f2040 100644 --- a/backend/social-work-app/lambdas/python/common/tests/resources/dynamo/provider.json +++ b/backend/social-work-app/lambdas/python/common/tests/resources/dynamo/provider.json @@ -12,6 +12,7 @@ "familyName": "Gu\u00f0mundsd\u00f3ttir", "licenseJurisdiction": "oh", "jurisdictionUploadedLicenseStatus": "active", + "publicCompactIdentifier": "SWC-1234-1", "jurisdictionUploadedCompactEligibility": "eligible", "dateOfExpiration": "2025-04-04", "dateOfBirth": "1985-06-06", diff --git a/backend/social-work-app/lambdas/python/common/tests/unit/test_data_model/test_schema/test_provider.py b/backend/social-work-app/lambdas/python/common/tests/unit/test_data_model/test_schema/test_provider.py index b5401d4f9b..68ee97654e 100644 --- a/backend/social-work-app/lambdas/python/common/tests/unit/test_data_model/test_schema/test_provider.py +++ b/backend/social-work-app/lambdas/python/common/tests/unit/test_data_model/test_schema/test_provider.py @@ -108,6 +108,26 @@ def test_general_response_schema_does_not_include_date_of_birth_in_licenses(self self.assertNotIn('dateOfBirth', result['licenses'][0]) + def test_general_response_schema_retains_public_compact_identifier(self): + """ProviderGeneralResponseSchema (a ForgivingSchema) must declare publicCompactIdentifier so it survives + sanitization instead of being silently dropped as an unknown field.""" + from cc_common.data_model.schema.provider.api import ProviderGeneralResponseSchema + + data = self._make_provider_data_with_license() + data['publicCompactIdentifier'] = 'SWC-4548-1' + result = ProviderGeneralResponseSchema().load(data) + + self.assertEqual('SWC-4548-1', result['publicCompactIdentifier']) + + def test_opensearch_document_schema_retains_public_compact_identifier(self): + from cc_common.data_model.schema.provider.api import ProviderOpenSearchDocumentSchema + + data = self._make_provider_data_with_license() + data['publicCompactIdentifier'] = 'SWC-4548-1' + result = ProviderOpenSearchDocumentSchema().load(data) + + self.assertEqual('SWC-4548-1', result['publicCompactIdentifier']) + class TestQueryProvidersRequestSchema(TstLambdas): """QueryProvidersRequestSchema.QuerySchema licenseNumber length matches API Gateway model (max 100).""" @@ -192,6 +212,46 @@ def test_provider_record_schema_sets_status_to_inactive_if_license_status_inacti self.assertEqual('inactive', provider_data['licenseStatus']) self.assertEqual('ineligible', provider_data['compactEligibility']) + def test_serde_round_trips_public_compact_identifier(self): + """ProviderRecordSchema round-trips a valid publicCompactIdentifier.""" + from cc_common.data_model.schema import ProviderRecordSchema + + with open('tests/resources/dynamo/provider.json') as f: + provider_record = json.load(f) + provider_record['publicCompactIdentifier'] = 'SWC-4548-1' + + schema = ProviderRecordSchema() + loaded_record = schema.load(provider_record.copy()) + self.assertEqual('SWC-4548-1', loaded_record['publicCompactIdentifier']) + + dumped_record = schema.dump(loaded_record) + self.assertEqual('SWC-4548-1', dumped_record['publicCompactIdentifier']) + + def test_serde_omitting_public_compact_identifier_stays_valid(self): + """publicCompactIdentifier is optional; omitting it must not raise a ValidationError.""" + from cc_common.data_model.schema import ProviderRecordSchema + + with open('tests/resources/dynamo/provider.json') as f: + provider_record = json.load(f) + del provider_record['publicCompactIdentifier'] + + schema = ProviderRecordSchema() + loaded_record = schema.load(provider_record.copy()) + self.assertNotIn('publicCompactIdentifier', loaded_record) + + def test_malformed_public_compact_identifier_raises_validation_error(self): + from cc_common.data_model.schema import ProviderRecordSchema + + with open('tests/resources/dynamo/provider.json') as f: + provider_record = json.load(f) + + schema = ProviderRecordSchema() + for malformed_value in ('SWC-1', 'swc-0001-1', 'SWC-0001-1*', 'SWC-0001-0', 'SWC-00001-1'): + with self.subTest(malformed_value=malformed_value): + bad_record = {**provider_record, 'publicCompactIdentifier': malformed_value} + with self.assertRaises(ValidationError): + schema.load(bad_record) + def test_prov_date_of_update_matches_new_date_of_update(self): """ When a provider record is serialized date of update fields should be processed like: diff --git a/backend/social-work-app/lambdas/python/common/tests/unit/test_provider_record_util.py b/backend/social-work-app/lambdas/python/common/tests/unit/test_provider_record_util.py index f741c9f11f..db4d7aa704 100644 --- a/backend/social-work-app/lambdas/python/common/tests/unit/test_provider_record_util.py +++ b/backend/social-work-app/lambdas/python/common/tests/unit/test_provider_record_util.py @@ -1026,6 +1026,161 @@ def test_find_best_license_complex_scenario(self): self.assertEqual(best_license['dateOfIssuance'], '2024-03-01') self.assertEqual(best_license['compactEligibility'], CompactEligibilityStatus.INELIGIBLE) + def test_has_paired_single_and_multi_state_license_true_when_matching_pair_exists(self): + from cc_common.data_model.provider_record_util import ProviderRecordUtility + + licenses = [ + {**self.base_license, 'licenseScope': 'single-state'}, + {**self.base_license, 'licenseScope': 'multi-state', 'licenseNumber': '67890'}, + ] + self.assertTrue(ProviderRecordUtility.has_paired_single_and_multi_state_license(licenses)) + + def test_has_paired_single_and_multi_state_license_false_for_multi_state_only(self): + from cc_common.data_model.provider_record_util import ProviderRecordUtility + + licenses = [{**self.base_license, 'licenseScope': 'multi-state'}] + self.assertFalse(ProviderRecordUtility.has_paired_single_and_multi_state_license(licenses)) + + def test_has_paired_single_and_multi_state_license_false_for_single_state_only(self): + from cc_common.data_model.provider_record_util import ProviderRecordUtility + + licenses = [{**self.base_license, 'licenseScope': 'single-state'}] + self.assertFalse(ProviderRecordUtility.has_paired_single_and_multi_state_license(licenses)) + + def test_has_paired_single_and_multi_state_license_false_for_different_jurisdictions(self): + from cc_common.data_model.provider_record_util import ProviderRecordUtility + + licenses = [ + {**self.base_license, 'licenseScope': 'single-state', 'jurisdiction': 'oh'}, + {**self.base_license, 'licenseScope': 'multi-state', 'jurisdiction': 'ky', 'licenseNumber': '67890'}, + ] + self.assertFalse(ProviderRecordUtility.has_paired_single_and_multi_state_license(licenses)) + + def test_has_paired_single_and_multi_state_license_false_for_different_license_types(self): + from cc_common.data_model.provider_record_util import ProviderRecordUtility + + licenses = [ + {**self.base_license, 'licenseScope': 'single-state', 'licenseType': 'physician'}, + { + **self.base_license, + 'licenseScope': 'multi-state', + 'licenseType': 'psychologist', + 'licenseNumber': '67890', + }, + ] + self.assertFalse(ProviderRecordUtility.has_paired_single_and_multi_state_license(licenses)) + + def test_has_paired_single_and_multi_state_license_ignores_eligibility_and_status(self): + """Eligibility and active status are not considered when detecting a pairing.""" + from cc_common.data_model.provider_record_util import ProviderRecordUtility + from cc_common.data_model.schema.common import ActiveInactiveStatus, CompactEligibilityStatus + + licenses = [ + { + **self.base_license, + 'licenseScope': 'single-state', + 'licenseStatus': ActiveInactiveStatus.INACTIVE, + 'compactEligibility': CompactEligibilityStatus.INELIGIBLE, + }, + { + **self.base_license, + 'licenseScope': 'multi-state', + 'licenseNumber': '67890', + 'licenseStatus': ActiveInactiveStatus.INACTIVE, + 'compactEligibility': CompactEligibilityStatus.INELIGIBLE, + }, + ] + self.assertTrue(ProviderRecordUtility.has_paired_single_and_multi_state_license(licenses)) + + def test_has_paired_single_and_multi_state_license_false_for_empty_list(self): + from cc_common.data_model.provider_record_util import ProviderRecordUtility + + self.assertFalse(ProviderRecordUtility.has_paired_single_and_multi_state_license([])) + + def test_populate_provider_record_preserves_existing_cuid_across_home_jurisdiction_change(self): + """An existing publicCompactIdentifier must survive a home-jurisdiction-changing update.""" + from cc_common.data_model.provider_record_util import ProviderRecordUtility + from common_test.test_data_generator import TestDataGenerator + + current_provider = TestDataGenerator.generate_default_provider( + {'publicCompactIdentifier': 'SWC-4548-1', 'licenseJurisdiction': 'oh'} + ) + new_license = TestDataGenerator.generate_default_license({'jurisdiction': 'ky'}).to_dict() + + updated_provider = ProviderRecordUtility.populate_provider_record(current_provider, new_license) + + self.assertEqual('SWC-4548-1', updated_provider.publicCompactIdentifier) + self.assertEqual('ky', updated_provider.licenseJurisdiction) + + def test_populate_provider_record_sets_new_cuid_when_provided(self): + from cc_common.data_model.provider_record_util import ProviderRecordUtility + from common_test.test_data_generator import TestDataGenerator + + new_license = TestDataGenerator.generate_default_license().to_dict() + + updated_provider = ProviderRecordUtility.populate_provider_record( + None, new_license, public_compact_identifier='SWC-4548-1' + ) + + self.assertEqual('SWC-4548-1', updated_provider.publicCompactIdentifier) + + def test_populate_provider_record_does_not_set_cuid_when_not_provided(self): + from cc_common.data_model.provider_record_util import ProviderRecordUtility + from common_test.test_data_generator import TestDataGenerator + + new_license = TestDataGenerator.generate_default_license().to_dict() + + updated_provider = ProviderRecordUtility.populate_provider_record(None, new_license) + + self.assertIsNone(updated_provider.publicCompactIdentifier) + + def test_populate_provider_record_raises_when_overwriting_existing_cuid(self): + """ + A CUID is write-once, so assigning a new one over an existing one must fail loudly. + + Reaching this state means the caller decided to mint a CUID for a practitioner who already + had one -- a bug that would otherwise silently replace a published, public-facing identifier. + """ + from cc_common.data_model.provider_record_util import ProviderRecordUtility + from cc_common.exceptions import CCInternalException + from common_test.test_data_generator import TestDataGenerator + + current_provider = TestDataGenerator.generate_default_provider({'publicCompactIdentifier': 'SWC-4548-1'}) + new_license = TestDataGenerator.generate_default_license().to_dict() + + with self.assertRaises(CCInternalException) as context: + ProviderRecordUtility.populate_provider_record( + current_provider, new_license, public_compact_identifier='SWC-9999-2' + ) + + self.assertIn('cannot be overwritten', str(context.exception)) + + def test_populate_provider_record_allows_existing_cuid_when_no_new_cuid_provided(self): + """The guard must only fire on an actual overwrite attempt, not on ordinary updates.""" + from cc_common.data_model.provider_record_util import ProviderRecordUtility + from common_test.test_data_generator import TestDataGenerator + + current_provider = TestDataGenerator.generate_default_provider({'publicCompactIdentifier': 'SWC-4548-1'}) + new_license = TestDataGenerator.generate_default_license().to_dict() + + updated_provider = ProviderRecordUtility.populate_provider_record(current_provider, new_license) + + self.assertEqual('SWC-4548-1', updated_provider.publicCompactIdentifier) + + def test_populate_provider_record_allows_new_cuid_when_none_exists(self): + """Assigning a CUID to a practitioner who has none is the normal path and must still work.""" + from cc_common.data_model.provider_record_util import ProviderRecordUtility + from common_test.test_data_generator import TestDataGenerator + + current_provider = TestDataGenerator.generate_default_provider() + new_license = TestDataGenerator.generate_default_license().to_dict() + + updated_provider = ProviderRecordUtility.populate_provider_record( + current_provider, new_license, public_compact_identifier='SWC-4548-1' + ) + + self.assertEqual('SWC-4548-1', updated_provider.publicCompactIdentifier) + @patch('cc_common.config._Config.expiration_resolution_date', date(2025, 6, 1)) class TestProviderUserRecordsBestLicense(TstLambdas): @@ -2078,3 +2233,20 @@ def test_opensearch_most_recent_license_for_type_true_for_each_license_type(self } self.assertTrue(multi_state_flags[(lcsw, 'oh')]) self.assertTrue(multi_state_flags[(lmsw, 'al')]) + + def test_opensearch_documents_include_public_compact_identifier_on_every_document(self): + """publicCompactIdentifier must be present on every per-license OpenSearch document for the provider.""" + from cc_common.data_model.provider_record_util import ProviderUserRecords + + license_type = 'licensed clinical social worker' + records = self._make_provider_records( + provider_overrides={'publicCompactIdentifier': 'SWC-4548-1'}, + license_overrides_list=_license_pair_overrides('oh', license_type), + ) + with self._patch_config_for_privilege_generation(): + provider_user_records = ProviderUserRecords(records) + docs = provider_user_records.generate_opensearch_documents() + + self.assertEqual(2, len(docs)) + for doc in docs: + self.assertEqual('SWC-4548-1', doc['publicCompactIdentifier']) diff --git a/backend/social-work-app/lambdas/python/provider-data-v1/handlers/ingest.py b/backend/social-work-app/lambdas/python/provider-data-v1/handlers/ingest.py index 915d1caf42..fd0072822c 100644 --- a/backend/social-work-app/lambdas/python/provider-data-v1/handlers/ingest.py +++ b/backend/social-work-app/lambdas/python/provider-data-v1/handlers/ingest.py @@ -1,4 +1,5 @@ import json +import secrets from copy import deepcopy from boto3.dynamodb.types import TypeSerializer @@ -6,10 +7,12 @@ from cc_common.data_model.provider_record_util import ProviderRecordType, ProviderRecordUtility, ProviderUserRecords from cc_common.data_model.schema import LicenseRecordSchema from cc_common.data_model.schema.common import ( + CUID_PREFIX, ActiveInactiveStatus, CompactEligibilityStatus, LicenseScopeEnum, UpdateCategory, + provider_pk, ) from cc_common.data_model.schema.license import LicenseData from cc_common.data_model.schema.license.ingest import LicenseIngestSchema @@ -39,6 +42,7 @@ 'suffix', 'dateOfExpiration', 'dateOfBirth', + 'publicCompactIdentifier', } MULTI_STATE_SINGLE_STATE_ELIGIBILITY_MISMATCH_MESSAGE = ( @@ -218,6 +222,16 @@ def _matches_posted_license(license_record: LicenseData) -> bool: ] known_licenses.append(posted_license_record) + # Determine whether this upload newly qualifies the provider for a Compact Unique Identifier (CUID). + # This must be resolved independently of the provider-record Put decision below: the license that + # completes a single-state/multi-state pairing frequently loses that decision (multi-state is always + # preferred), which would otherwise silently skip CUID assignment. + new_public_compact_identifier = _resolve_public_compact_identifier( + compact=compact, + current_provider_record=current_provider_record, + known_licenses=known_licenses, + ) + # Determine if this upload triggers a home jurisdiction change. new_home_license = _get_license_triggering_home_jurisdiction_change( current_provider_record=current_provider_record, @@ -263,16 +277,24 @@ def _matches_posted_license(license_record: LicenseData) -> bool: provider_record = ProviderRecordUtility.populate_provider_record( current_provider_record=current_provider_record, license_record=license_record_for_provider_update, + public_compact_identifier=new_public_compact_identifier, ) - dynamo_transactions.append( - { - 'Put': { - 'TableName': config.provider_table_name, - 'Item': TypeSerializer().serialize(provider_record.serialize_to_database_record())['M'], - } - } - ) + provider_put = { + 'TableName': config.provider_table_name, + 'Item': TypeSerializer().serialize(provider_record.serialize_to_database_record())['M'], + } + if new_public_compact_identifier is not None: + # Guard the CUID assignment against a competing transaction -- either another + # provider Put, or the conditional Update path below -- assigning one between our + # consistent read above and this write. DynamoDB cancels the losing transaction; + # SQS redelivers it, and the retry's consistent read finds the existing CUID, so + # _resolve_public_compact_identifier returns None and the retry writes without this + # condition. The condition is only applied when we are actually assigning, so a + # practitioner who already has a CUID is never blocked from ordinary updates. + provider_put['ConditionExpression'] = 'attribute_not_exists(publicCompactIdentifier)' + + dynamo_transactions.append({'Put': provider_put}) # If this is an update to an existing provider record (not a first-upload create), capture the # delta as a providerUpdate history record so an upload-driven change (e.g. home jurisdiction) @@ -283,6 +305,30 @@ def _matches_posted_license(license_record: LicenseData) -> bool: new_provider=provider_record.to_dict(), dynamo_transactions=dynamo_transactions, ) + elif new_public_compact_identifier is not None: + # No provider Put is part of this transaction, + # but the CUID must still be assigned. DynamoDB transactions reject two + # operations on the same item, so this Update is mutually exclusive with the Put case above. + logger.info('Assigning CUID via conditional update; no provider Put in this transaction') + dynamo_transactions.append( + _generate_cuid_assignment_update_item( + compact=compact, + provider_id=provider_id, + public_compact_identifier=new_public_compact_identifier, + ) + ) + + # The Update still changes the provider record, so it needs the same providerUpdate + # history record the Put path writes. current_provider_record is never None here: a + # provider with no existing record always takes the Put branch above. + _process_provider_update( + existing_provider=current_provider_record.to_dict(), + new_provider={ + **current_provider_record.to_dict(), + 'publicCompactIdentifier': new_public_compact_identifier, + }, + dynamo_transactions=dynamo_transactions, + ) # Write the records together as a transaction that succeeds or fails as one, to ensure consistency config.dynamodb_client.transact_write_items(TransactItems=dynamo_transactions) @@ -341,6 +387,74 @@ def _check_for_multi_state_single_state_eligibility_validation_error( ) +def _generate_cuid(compact: str) -> str: + """ + Generate a new Compact Unique Identifier (CUID) for a provider. + + The monotonic counter segment is claimed atomically here rather than accepted as an argument, so a + counter value can never be reused or passed in by a caller. The four-digit random segment is not a + uniqueness guarantee; it only makes a mis-keyed CUID far less likely to resolve to a real, unrelated + practitioner. Uniqueness comes solely from the claimed counter. This is the only place in the codebase + that constructs a CUID. + """ + counter = config.data_client.claim_cuid_number(compact) + return f'{CUID_PREFIX}-{secrets.randbelow(10000):04d}-{counter}' + + +def _resolve_public_compact_identifier( + *, + compact: str, + current_provider_record: ProviderData | None, + known_licenses: list[dict], +) -> str | None: + """ + Return a newly generated CUID if the provider now qualifies and does not already have one, else None. + + Kept separate from ``_generate_cuid`` so the "should we assign one?" decision is testable independently + of minting, and so a counter is only ever claimed once both preconditions hold. + """ + if current_provider_record is not None and current_provider_record.publicCompactIdentifier: + logger.info('Provider already has a CUID; skipping CUID assignment') + return None + if not ProviderRecordUtility.has_paired_single_and_multi_state_license(known_licenses): + logger.info('Provider does not have a paired single-state/multi-state license; skipping CUID assignment') + return None + logger.info('Provider qualifies for CUID; generating new CUID') + return _generate_cuid(compact) + + +def _generate_cuid_assignment_update_item(*, compact: str, provider_id: str, public_compact_identifier: str) -> dict: + """ + Build a conditional Update transaction item that assigns a newly-minted CUID to the provider's top-level + record, for use when no provider Put is already part of this transaction. + + Bumping dateOfUpdate/providerDateOfUpdate here matters: it keeps the providerDateOfUpdate GSI coherent and + guarantees the DynamoDB stream fires so the OpenSearch documents get reindexed with the new CUID. + + The attribute_not_exists condition guard makes concurrent ingest of the two paired licenses safe: the + loser's transaction fails the condition check, SQS retries the message, and the retry observes the + already-assigned CUID and does nothing (``_resolve_public_compact_identifier`` short-circuits to None). + """ + now = config.current_standard_datetime.isoformat() + return { + 'Update': { + 'TableName': config.provider_table_name, + 'Key': { + 'pk': {'S': provider_pk(compact, provider_id)}, + 'sk': {'S': f'{compact}#PROVIDER'}, + }, + 'UpdateExpression': ( + 'SET publicCompactIdentifier = :cuid, dateOfUpdate = :now, providerDateOfUpdate = :now' + ), + 'ConditionExpression': 'attribute_not_exists(publicCompactIdentifier)', + 'ExpressionAttributeValues': { + ':cuid': {'S': public_compact_identifier}, + ':now': {'S': now}, + }, + } + } + + def _get_license_triggering_home_jurisdiction_change( *, current_provider_record: ProviderData | None, diff --git a/backend/social-work-app/lambdas/python/provider-data-v1/tests/function/test_data_model/test_provider_transformations.py b/backend/social-work-app/lambdas/python/provider-data-v1/tests/function/test_data_model/test_provider_transformations.py index 8a2cf175ab..2adf2cc88c 100644 --- a/backend/social-work-app/lambdas/python/provider-data-v1/tests/function/test_data_model/test_provider_transformations.py +++ b/backend/social-work-app/lambdas/python/provider-data-v1/tests/function/test_data_model/test_provider_transformations.py @@ -1,4 +1,5 @@ import json +import re from datetime import datetime from unittest.mock import patch @@ -8,6 +9,7 @@ from .. import TstFunction MOCK_CURRENT_DATETIME_STRING = '2024-11-08T23:59:59+00:00' +CUID_PATTERN = re.compile(r'^SWC-[0-9]{4}-[1-9][0-9]*$') @mock_aws @@ -95,7 +97,8 @@ def test_transformations(self, mock_license_preprocessing_queue): compact='socw', provider_id=provider_id, include_update_tier=UpdateTierEnum.TIER_THREE ) - # One record for each of: provider and license (no privileges in Social Work model) + # One record for each of: provider, single-state license, multi-state license + # (no privileges in Social Work model) license_records = [r for r in provider_user_records.provider_records if r['type'] == 'license'] self.assertEqual(2, len(license_records)) records = {item['type']: item for item in provider_user_records.provider_records if item['type'] != 'license'} @@ -124,6 +127,15 @@ def test_transformations(self, mock_license_preprocessing_queue): del expected_provider['providerDateOfUpdate'] del records['provider']['providerDateOfUpdate'] + # These two license posts pair a single-state and multi-state license in the same jurisdiction and + # licenseType, so a CUID is assigned as part of this ingest chain. Assert its format/counter separately. + provider_cuid = records['provider'].get('publicCompactIdentifier') + self.assertRegex(provider_cuid, CUID_PATTERN) + self.assertEqual('1', provider_cuid.split('-')[-1]) + # the CUID includes a randomly generated number, so we set the expected_provider's + # value to match + expected_provider['publicCompactIdentifier'] = provider_cuid + # Make sure each is represented the way we expect, in the db self.assertEqual(expected_provider, records['provider']) self.assertEqual(expected_license, records['license']) @@ -161,6 +173,12 @@ def test_transformations(self, mock_license_preprocessing_queue): for license_data in expected_provider['licenses']: del license_data['dateOfUpdate'] + # The CUID assigned earlier in this test is echoed back through the API. Assert it separately + self.assertEqual(provider_cuid, provider_data.get('publicCompactIdentifier')) + # The CUID includes a randomly generated number, so we set the expected_provider's + # value to match + expected_provider['publicCompactIdentifier'] = provider_cuid + # Phew! We've loaded the data all the way in via the ingest chain and back out via the API! self.maxDiff = None self.assertEqual(expected_provider, provider_data) diff --git a/backend/social-work-app/lambdas/python/provider-data-v1/tests/function/test_handlers/test_ingest.py b/backend/social-work-app/lambdas/python/provider-data-v1/tests/function/test_handlers/test_ingest.py index cfb9ca7f8f..523a100c4f 100644 --- a/backend/social-work-app/lambdas/python/provider-data-v1/tests/function/test_handlers/test_ingest.py +++ b/backend/social-work-app/lambdas/python/provider-data-v1/tests/function/test_handlers/test_ingest.py @@ -1,4 +1,6 @@ import json +import re +from collections.abc import Callable from datetime import date, datetime from unittest.mock import MagicMock, patch @@ -7,6 +9,8 @@ from .. import TstFunction +CUID_PATTERN = re.compile(r'^SWC-[0-9]{4}-[1-9][0-9]*$') + @mock_aws @patch('cc_common.config._Config.current_standard_datetime', datetime.fromisoformat('2024-11-08T23:59:59+00:00')) @@ -21,6 +25,9 @@ def _set_provider_data_to_empty_values(expected_provider: dict) -> dict: for license_record in expected_provider['licenses'] if license_record.get('licenseScope') == 'single-state' ] + # These tests only ever ingest a single single-state license, so no paired single-state/multi-state + # license exists and a CUID is never assigned. + del expected_provider['publicCompactIdentifier'] return expected_provider @@ -192,6 +199,11 @@ def test_old_inactive_license(self): del expected_provider['licenses'] licenses = provider_data.pop('licenses') + # The loaded fixture already has a paired OH single-state/multi-state license, so the provider already + # has a CUID assigned before this ingest occurs. The inactive KY license upload does not win the + # provider-record Put decision, so the CUID is left unchanged. + self.assertEqual(expected_provider['publicCompactIdentifier'], provider_data['publicCompactIdentifier']) + # The original provider data is preferred over the posted license data in our test case self.assertEqual(expected_provider, provider_data) @@ -1782,3 +1794,510 @@ def test_eligible_multi_state_without_single_state_does_not_emit_validation_erro license_records = [record for record in provider_records if record['type'] == 'license'] self.assertEqual(1, len(license_records)) self.assertEqual('multi-state', license_records[0]['licenseScope']) + + +@mock_aws +@patch('cc_common.config._Config.current_standard_datetime', datetime.fromisoformat('2024-11-08T23:59:59+00:00')) +class TestCuidGeneration(TstFunction): + """ + Tests for Compact Unique Identifier (CUID) assignment during license ingest. + + A CUID is assigned the first time a provider has a paired single-state and multi-state license (same + jurisdiction and licenseType). Assignment must work regardless of whether that pairing upload results in a + provider-record Put (the license becomes the new best license for the jurisdiction) or not (the paired + license loses to the existing best license, so the provider record would otherwise go untouched). + """ + + # A second, distinct provider id for tests that need two providers. DEFAULT_PROVIDER_ID (from + # common_test.test_constants) is used for single-provider tests, since it already matches the providerId + # baked into the event-bridge-message.json fixture. + _OTHER_PROVIDER_ID = '11111111-1111-4111-8111-111111111111' + + def _ingest(self, detail_overrides: dict, message_id: str = '1'): + """ + Ingest a license message. ingest_license_message never reads the SSN table (only the separate + preprocess_license_ingest handler does), so these tests only need providerId/ssnLastFour present on + the message body, not an actual SSN record. + """ + from handlers.ingest import ingest_license_message + + with open('../common/tests/resources/ingest/event-bridge-message.json') as f: + message = json.load(f) + message['detail'].update(detail_overrides) + event = {'Records': [{'messageId': message_id, 'body': json.dumps(message)}]} + resp = ingest_license_message(event, self.mock_context) + self.assertEqual({'batchItemFailures': []}, resp) + + def _get_provider_record(self, provider_id: str) -> dict: + provider_records = self._provider_table.query( + Select='ALL_ATTRIBUTES', + KeyConditionExpression=Key('pk').eq(f'socw#PROVIDER#{provider_id}'), + )['Items'] + return next(record for record in provider_records if record['type'] == 'provider') + + def _get_cuid_counter(self) -> int | None: + item = self._provider_table.get_item(Key={'pk': 'socw#CUID_COUNT', 'sk': 'socw#CUID_COUNT'}).get('Item') + return item['cuidCount'] if item else None + + def test_no_cuid_generated_for_multi_state_license_only(self): + from common_test.test_constants import DEFAULT_PROVIDER_ID + + self._ingest({'licenseScope': 'multi-state'}) + + provider_record = self._get_provider_record(DEFAULT_PROVIDER_ID) + self.assertNotIn('publicCompactIdentifier', provider_record) + self.assertIsNone(self._get_cuid_counter()) + + def test_no_cuid_generated_for_single_state_license_only(self): + from common_test.test_constants import DEFAULT_PROVIDER_ID + + self._ingest({'licenseScope': 'single-state'}) + + provider_record = self._get_provider_record(DEFAULT_PROVIDER_ID) + self.assertNotIn('publicCompactIdentifier', provider_record) + self.assertIsNone(self._get_cuid_counter()) + + def test_cuid_generated_when_paired_single_state_uploaded_after_multi_state_without_provider_put(self): + """ + Critical case: OH multi-state exists as the provider's home license; uploading the paired OH + single-state license loses to the existing multi-state license for the provider-record Put decision + (multi-state is always preferred), so no provider Put occurs on this upload. CUID assignment must still + happen via the conditional-Update path. + """ + from common_test.test_constants import DEFAULT_PROVIDER_ID + + self._ingest({'licenseScope': 'multi-state'}, message_id='1') + self.assertIsNone(self._get_cuid_counter()) + + self._ingest({'licenseScope': 'single-state', 'licenseNumber': 'PAIRED-SS'}, message_id='2') + + provider_record = self._get_provider_record(DEFAULT_PROVIDER_ID) + self.assertRegex(provider_record['publicCompactIdentifier'], CUID_PATTERN) + self.assertEqual(1, self._get_cuid_counter()) + + def test_cuid_generated_when_paired_multi_state_uploaded_after_single_state_with_provider_put(self): + """Single-state OH license first (home), then paired multi-state OH license, which wins the + provider-record Put decision (multi-state is preferred). CUID must be injected into that Put.""" + from common_test.test_constants import DEFAULT_PROVIDER_ID + + self._ingest({'licenseScope': 'single-state'}, message_id='1') + self.assertIsNone(self._get_cuid_counter()) + + self._ingest({'licenseScope': 'multi-state', 'licenseNumber': 'PAIRED-MS'}, message_id='2') + + provider_record = self._get_provider_record(DEFAULT_PROVIDER_ID) + self.assertRegex(provider_record['publicCompactIdentifier'], CUID_PATTERN) + self.assertEqual(1, self._get_cuid_counter()) + + def test_no_cuid_when_pairing_is_in_different_jurisdictions(self): + from common_test.test_constants import DEFAULT_PROVIDER_ID + + self._ingest({'licenseScope': 'single-state', 'jurisdiction': 'oh'}, message_id='1') + self._ingest({'licenseScope': 'multi-state', 'jurisdiction': 'ky', 'licenseNumber': 'KY-MS'}, message_id='2') + + provider_record = self._get_provider_record(DEFAULT_PROVIDER_ID) + self.assertNotIn('publicCompactIdentifier', provider_record) + self.assertIsNone(self._get_cuid_counter()) + + def test_no_cuid_when_pairing_is_of_different_license_types(self): + from common_test.test_constants import DEFAULT_PROVIDER_ID + + self._ingest({'licenseScope': 'single-state', 'licenseType': 'licensed clinical social worker'}, message_id='1') + self._ingest( + { + 'licenseScope': 'multi-state', + 'licenseType': 'licensed master social worker', + 'licenseNumber': 'LMSW-MS', + }, + message_id='2', + ) + + provider_record = self._get_provider_record(DEFAULT_PROVIDER_ID) + self.assertNotIn('publicCompactIdentifier', provider_record) + self.assertIsNone(self._get_cuid_counter()) + + def test_cuid_unchanged_and_counter_not_incremented_on_reupload(self): + from common_test.test_constants import DEFAULT_PROVIDER_ID + + self._ingest({'licenseScope': 'multi-state'}, message_id='1') + self._ingest({'licenseScope': 'single-state', 'licenseNumber': 'PAIRED-SS'}, message_id='2') + + cuid_after_pairing = self._get_provider_record(DEFAULT_PROVIDER_ID)['publicCompactIdentifier'] + self.assertEqual(1, self._get_cuid_counter()) + + # Re-upload the single-state license again; nothing about the CUID should change. + self._ingest({'licenseScope': 'single-state', 'licenseNumber': 'PAIRED-SS'}, message_id='3') + + provider_record = self._get_provider_record(DEFAULT_PROVIDER_ID) + self.assertEqual(cuid_after_pairing, provider_record['publicCompactIdentifier']) + self.assertEqual(1, self._get_cuid_counter()) + + def test_cuid_preserved_across_home_jurisdiction_change(self): + """Once assigned, a CUID must survive a subsequent home jurisdiction change triggered by a new + multi-state/single-state pairing in a different jurisdiction.""" + from common_test.test_constants import DEFAULT_PROVIDER_ID + + self._ingest({'licenseScope': 'multi-state', 'jurisdiction': 'oh'}, message_id='1') + self._ingest({'licenseScope': 'single-state', 'jurisdiction': 'oh', 'licenseNumber': 'OH-SS'}, message_id='2') + cuid_after_pairing = self._get_provider_record(DEFAULT_PROVIDER_ID)['publicCompactIdentifier'] + + # Establish a paired KY single-state license (older), then a newer KY multi-state license to trigger + # the home jurisdiction change. + self._ingest( + { + 'licenseScope': 'single-state', + 'jurisdiction': 'ky', + 'licenseNumber': 'KY-SS', + 'dateOfIssuance': '2019-06-06', + }, + message_id='3', + ) + self._ingest( + { + 'licenseScope': 'multi-state', + 'jurisdiction': 'ky', + 'licenseNumber': 'KY-MS', + 'dateOfIssuance': '2020-06-06', + }, + message_id='4', + ) + + provider_record = self._get_provider_record(DEFAULT_PROVIDER_ID) + self.assertEqual('ky', provider_record['licenseJurisdiction']) + self.assertEqual(cuid_after_pairing, provider_record['publicCompactIdentifier']) + # Still only one CUID has ever been claimed for this provider. + self.assertEqual(1, self._get_cuid_counter()) + + def test_cuid_counters_increment_across_different_providers(self): + from common_test.test_constants import DEFAULT_PROVIDER_ID + + provider_a = DEFAULT_PROVIDER_ID + self._ingest({'licenseScope': 'multi-state', 'providerId': provider_a, 'licenseNumber': 'A-MS'}, message_id='1') + self._ingest( + { + 'licenseScope': 'single-state', + 'providerId': provider_a, + 'licenseNumber': 'A-SS', + }, + message_id='2', + ) + + provider_b = self._OTHER_PROVIDER_ID + self._ingest({'licenseScope': 'multi-state', 'providerId': provider_b, 'licenseNumber': 'B-MS'}, message_id='3') + self._ingest( + { + 'licenseScope': 'single-state', + 'providerId': provider_b, + 'licenseNumber': 'B-SS', + }, + message_id='4', + ) + + cuid_a = self._get_provider_record(provider_a)['publicCompactIdentifier'] + cuid_b = self._get_provider_record(provider_b)['publicCompactIdentifier'] + self.assertNotEqual(cuid_a, cuid_b) + self.assertTrue(cuid_a.endswith('-1')) + self.assertTrue(cuid_b.endswith('-2')) + self.assertEqual(2, self._get_cuid_counter()) + + def test_date_fields_bumped_on_conditional_update_path(self): + """When CUID assignment takes the conditional-Update path (no provider Put), dateOfUpdate and + providerDateOfUpdate must still be bumped so the OpenSearch reindex stream fires.""" + from common_test.test_constants import DEFAULT_PROVIDER_ID + + with patch( + 'cc_common.config._Config.current_standard_datetime', datetime.fromisoformat('2024-01-01T00:00:00+00:00') + ): + self._ingest({'licenseScope': 'multi-state'}, message_id='1') + + with patch( + 'cc_common.config._Config.current_standard_datetime', datetime.fromisoformat('2024-06-15T12:00:00+00:00') + ): + self._ingest({'licenseScope': 'single-state', 'licenseNumber': 'PAIRED-SS'}, message_id='2') + + provider_record = self._get_provider_record(DEFAULT_PROVIDER_ID) + self.assertEqual('2024-06-15T12:00:00+00:00', provider_record['dateOfUpdate']) + self.assertEqual('2024-06-15T12:00:00+00:00', provider_record['providerDateOfUpdate']) + + def test_cuid_format_zero_pads_random_segment(self): + """With secrets.randbelow patched to return 7, the CUID must be SWC-0007-1 (locks zero-padding).""" + import handlers.ingest as ingest_handler + from common_test.test_constants import DEFAULT_PROVIDER_ID + + self._ingest({'licenseScope': 'multi-state'}, message_id='1') + + with patch.object(ingest_handler, 'secrets') as mock_secrets: + mock_secrets.randbelow.return_value = 7 + self._ingest({'licenseScope': 'single-state', 'licenseNumber': 'PAIRED-SS'}, message_id='2') + + provider_record = self._get_provider_record(DEFAULT_PROVIDER_ID) + self.assertEqual('SWC-0007-1', provider_record['publicCompactIdentifier']) + + def test_generate_cuid_claims_counter_exactly_once_per_assignment(self): + """_generate_cuid claims its own counter and takes no counter argument; assignment must claim exactly + once, with no path for a caller-supplied or reused counter.""" + import handlers.ingest as ingest_handler + from common_test.test_constants import DEFAULT_PROVIDER_ID + + self._ingest({'licenseScope': 'multi-state'}, message_id='1') + + original_claim = ingest_handler.config.data_client.claim_cuid_number + with patch.object( + ingest_handler.config.data_client, 'claim_cuid_number', side_effect=original_claim + ) as mock_claim: + self._ingest({'licenseScope': 'single-state', 'licenseNumber': 'PAIRED-SS'}, message_id='2') + + mock_claim.assert_called_once_with('socw') + provider_record = self._get_provider_record(DEFAULT_PROVIDER_ID) + self.assertRegex(provider_record['publicCompactIdentifier'], CUID_PATTERN) + + _PROVIDER_SORT_KEY = 'socw#PROVIDER' + + def _ingest_capturing_transaction_items( + self, + detail_overrides: dict, + message_id: str = '1', + *, + expect_failure: bool = False, + before_write: Callable[[], None] | None = None, + ) -> list[dict]: + """ + Ingest a license message and return the TransactItems submitted to DynamoDB. + + The real client is still called, so the table ends in the same state the handler intended. + + :param expect_failure: When True, assert the message was reported as a batch item failure + (i.e. SQS will retry it) rather than asserting it succeeded. + :param before_write: Invoked after the handler has read provider state but immediately before + its transaction is submitted, so a test can simulate a competing write winning that race. + """ + import handlers.ingest as ingest_handler + from handlers.ingest import ingest_license_message + + captured_transaction_items: list[dict] = [] + real_transact_write_items = ingest_handler.config.dynamodb_client.transact_write_items + + def _capture_and_delegate(**kwargs): + captured_transaction_items.extend(kwargs['TransactItems']) + if before_write is not None: + before_write() + return real_transact_write_items(**kwargs) + + with open('../common/tests/resources/ingest/event-bridge-message.json') as f: + message = json.load(f) + message['detail'].update(detail_overrides) + event = {'Records': [{'messageId': message_id, 'body': json.dumps(message)}]} + + with patch.object( + ingest_handler.config.dynamodb_client, 'transact_write_items', side_effect=_capture_and_delegate + ): + resp = ingest_license_message(event, self.mock_context) + + expected_failures = [{'itemIdentifier': message_id}] if expect_failure else [] + self.assertEqual({'batchItemFailures': expected_failures}, resp) + + return captured_transaction_items + + def _get_provider_put(self, transaction_items: list[dict]) -> dict: + """Return the Put operation targeting the top-level provider record.""" + provider_puts = [ + item['Put'] + for item in transaction_items + if 'Put' in item and item['Put']['Item'].get('sk', {}).get('S') == self._PROVIDER_SORT_KEY + ] + self.assertEqual(1, len(provider_puts), 'Expected exactly one top-level provider Put in the transaction') + return provider_puts[0] + + def test_provider_put_includes_cuid_condition_when_assigning_cuid(self): + """ + The Put that assigns a CUID must be conditional on the field being absent. + + Without it, a provider Put could overwrite a CUID that a competing transaction assigned first. + """ + self._ingest({'licenseScope': 'single-state'}, message_id='1') + + # The paired multi-state license wins the provider-record Put decision, so the CUID is + # assigned via that Put rather than the conditional-Update path. + transaction_items = self._ingest_capturing_transaction_items( + {'licenseScope': 'multi-state', 'licenseNumber': 'PAIRED-MS'}, message_id='2' + ) + + provider_put = self._get_provider_put(transaction_items) + self.assertIn('publicCompactIdentifier', provider_put['Item']) + self.assertEqual('attribute_not_exists(publicCompactIdentifier)', provider_put.get('ConditionExpression')) + + def test_provider_put_has_no_cuid_condition_when_not_assigning_cuid(self): + """A first upload creates the provider record without a CUID, so it must not be conditional.""" + transaction_items = self._ingest_capturing_transaction_items({'licenseScope': 'single-state'}) + + provider_put = self._get_provider_put(transaction_items) + self.assertNotIn('publicCompactIdentifier', provider_put['Item']) + self.assertIsNone(provider_put.get('ConditionExpression')) + + def test_provider_put_has_no_cuid_condition_for_provider_that_already_has_cuid(self): + """ + Once a CUID exists, later provider updates must not carry the condition. + + Otherwise every subsequent upload for that practitioner would fail the condition and retry + forever. + """ + from common_test.test_constants import DEFAULT_PROVIDER_ID + + self._ingest({'licenseScope': 'single-state'}, message_id='1') + self._ingest({'licenseScope': 'multi-state', 'licenseNumber': 'PAIRED-MS'}, message_id='2') + assigned_cuid = self._get_provider_record(DEFAULT_PROVIDER_ID)['publicCompactIdentifier'] + + # A later upload that still updates the provider record, now that a CUID is already present. + transaction_items = self._ingest_capturing_transaction_items( + {'licenseScope': 'multi-state', 'licenseNumber': 'PAIRED-MS', 'dateOfRenewal': '2025-02-02'}, + message_id='3', + ) + + provider_put = self._get_provider_put(transaction_items) + self.assertEqual(assigned_cuid, provider_put['Item']['publicCompactIdentifier']['S']) + self.assertIsNone(provider_put.get('ConditionExpression')) + + def test_cuid_preserved_when_competing_transaction_assigns_first_and_message_is_retried(self): + """ + A losing provider Put must not clobber the CUID the winning transaction assigned. + + Simulates the real race: the competing CUID lands *after* this handler's consistent read but + *before* its transaction write, so the handler still believes it must assign one. Its + conditional Put is cancelled, SQS retries the message, and the retry re-reads, observes the + existing CUID, and leaves it untouched. + """ + from common_test.test_constants import DEFAULT_PROVIDER_ID + + self._ingest({'licenseScope': 'single-state'}, message_id='1') + + competing_cuid = 'SWC-1234-99' + provider_key = {'pk': f'socw#PROVIDER#{DEFAULT_PROVIDER_ID}', 'sk': self._PROVIDER_SORT_KEY} + + def assign_competing_cuid(): + self._provider_table.update_item( + Key=provider_key, + UpdateExpression='SET publicCompactIdentifier = :cuid', + ExpressionAttributeValues={':cuid': competing_cuid}, + ) + + # The handler read a provider record with no CUID, so it mints one and makes its Put + # conditional. The competing assignment lands first, cancelling this transaction. + losing_transaction_items = self._ingest_capturing_transaction_items( + {'licenseScope': 'multi-state', 'licenseNumber': 'PAIRED-MS'}, + message_id='2', + expect_failure=True, + before_write=assign_competing_cuid, + ) + self.assertEqual( + 'attribute_not_exists(publicCompactIdentifier)', + self._get_provider_put(losing_transaction_items).get('ConditionExpression'), + ) + self.assertEqual(competing_cuid, self._get_provider_record(DEFAULT_PROVIDER_ID)['publicCompactIdentifier']) + + # SQS redelivers the same message; the retry sees the existing CUID and assigns nothing new. + retry_transaction_items = self._ingest_capturing_transaction_items( + {'licenseScope': 'multi-state', 'licenseNumber': 'PAIRED-MS'}, message_id='2' + ) + + provider_put = self._get_provider_put(retry_transaction_items) + self.assertIsNone(provider_put.get('ConditionExpression')) + self.assertEqual(competing_cuid, self._get_provider_record(DEFAULT_PROVIDER_ID)['publicCompactIdentifier']) + + def _get_provider_update_records(self, provider_id: str) -> list: + from cc_common.data_model.update_tier_enum import UpdateTierEnum + + provider_records = self.config.data_client.get_provider_user_records( + compact='socw', + provider_id=provider_id, + include_update_tier=UpdateTierEnum.TIER_TWO, + ) + return provider_records.get_all_provider_update_records() + + def test_provider_update_record_written_when_cuid_assigned_without_provider_put(self): + """ + The conditional-Update path must leave the same audit trail as the Put path. + + When the paired license loses the provider-record Put decision, the CUID is assigned by a + standalone Update. That still changes the provider record, so it needs a providerUpdate + history record capturing the assignment. + """ + from cc_common.data_model.schema.common import UpdateCategory + from common_test.test_constants import DEFAULT_PROVIDER_ID + + # Multi-state first, so the later paired single-state license loses the Put decision. + self._ingest({'licenseScope': 'multi-state'}, message_id='1') + self._ingest({'licenseScope': 'single-state', 'licenseNumber': 'PAIRED-SS'}, message_id='2') + + assigned_cuid = self._get_provider_record(DEFAULT_PROVIDER_ID)['publicCompactIdentifier'] + + provider_updates = self._get_provider_update_records(DEFAULT_PROVIDER_ID) + cuid_updates = [update for update in provider_updates if 'publicCompactIdentifier' in update.updatedValues] + self.assertEqual(1, len(cuid_updates), provider_updates) + + cuid_update = cuid_updates[0] + self.assertEqual(UpdateCategory.LICENSE_UPLOAD_UPDATE_OTHER.value, cuid_update.updateType) + self.assertEqual(assigned_cuid, cuid_update.updatedValues['publicCompactIdentifier']) + # The practitioner had no CUID before this upload + self.assertNotIn('publicCompactIdentifier', cuid_update.previous) + + def test_provider_update_record_captures_cuid_when_assigned_via_provider_put(self): + """The Put path assigns the CUID inline, and must record it in the same history record.""" + from common_test.test_constants import DEFAULT_PROVIDER_ID + + # Single-state first, so the paired multi-state license wins the Put decision. + self._ingest({'licenseScope': 'single-state'}, message_id='1') + self._ingest({'licenseScope': 'multi-state', 'licenseNumber': 'PAIRED-MS'}, message_id='2') + + assigned_cuid = self._get_provider_record(DEFAULT_PROVIDER_ID)['publicCompactIdentifier'] + + cuid_updates = [ + update + for update in self._get_provider_update_records(DEFAULT_PROVIDER_ID) + if 'publicCompactIdentifier' in update.updatedValues + ] + self.assertEqual(1, len(cuid_updates)) + self.assertEqual(assigned_cuid, cuid_updates[0].updatedValues['publicCompactIdentifier']) + + def test_no_provider_update_record_for_cuid_on_subsequent_uploads(self): + """A CUID is assigned once, so later uploads must not record it as changing again.""" + from common_test.test_constants import DEFAULT_PROVIDER_ID + + self._ingest({'licenseScope': 'multi-state'}, message_id='1') + self._ingest({'licenseScope': 'single-state', 'licenseNumber': 'PAIRED-SS'}, message_id='2') + self._ingest({'licenseScope': 'single-state', 'licenseNumber': 'PAIRED-SS'}, message_id='3') + + cuid_updates = [ + update + for update in self._get_provider_update_records(DEFAULT_PROVIDER_ID) + if 'publicCompactIdentifier' in update.updatedValues + ] + self.assertEqual(1, len(cuid_updates), 'CUID assignment must be recorded exactly once') + + def test_provider_update_previous_snapshot_includes_existing_cuid(self): + """ + Once a CUID exists, later update records must carry it in their `previous` snapshot. + + Disaster-recovery rollback rebuilds the provider record from this snapshot alone (see + _build_and_execute_revert_transactions in rollback_license_upload.py), so a CUID missing here + would be silently dropped when an upload is reverted. + """ + from common_test.test_constants import DEFAULT_PROVIDER_ID + + self._ingest({'licenseScope': 'single-state'}, message_id='1') + self._ingest({'licenseScope': 'multi-state', 'licenseNumber': 'PAIRED-MS'}, message_id='2') + assigned_cuid = self._get_provider_record(DEFAULT_PROVIDER_ID)['publicCompactIdentifier'] + + # A later upload changing a tracked provider field, so an update record is written for a + # practitioner who already holds a CUID. + self._ingest( + {'licenseScope': 'multi-state', 'licenseNumber': 'PAIRED-MS', 'familyName': 'Changed'}, + message_id='3', + ) + + name_change_updates = [ + update + for update in self._get_provider_update_records(DEFAULT_PROVIDER_ID) + if 'familyName' in update.updatedValues + ] + self.assertEqual(1, len(name_change_updates), 'Expected one update record for the name change') + self.assertEqual(assigned_cuid, name_change_updates[0].previous['publicCompactIdentifier']) diff --git a/backend/social-work-app/lambdas/python/provider-data-v1/tests/function/test_handlers/test_public_lookup.py b/backend/social-work-app/lambdas/python/provider-data-v1/tests/function/test_handlers/test_public_lookup.py index 026b9b8caf..c79cba538e 100644 --- a/backend/social-work-app/lambdas/python/provider-data-v1/tests/function/test_handlers/test_public_lookup.py +++ b/backend/social-work-app/lambdas/python/provider-data-v1/tests/function/test_handlers/test_public_lookup.py @@ -15,6 +15,7 @@ 'licenseJurisdiction': 'oh', 'licenseStatus': 'active', 'compactEligibility': 'eligible', + 'publicCompactIdentifier': 'SWC-1234-1', 'givenName': 'Björk', 'middleName': 'Gunnar', 'familyName': 'Guðmundsdóttir', diff --git a/backend/social-work-app/lambdas/python/search/handlers/provider_update_ingest.py b/backend/social-work-app/lambdas/python/search/handlers/provider_update_ingest.py index cec5f36a75..eef8d594d0 100644 --- a/backend/social-work-app/lambdas/python/search/handlers/provider_update_ingest.py +++ b/backend/social-work-app/lambdas/python/search/handlers/provider_update_ingest.py @@ -70,6 +70,15 @@ def provider_update_ingest_handler(records: list[dict]) -> dict: # Extract compact and providerId from the DynamoDB image deserialized_image = TypeDeserializer().deserialize(value={'M': image}) + + # Check if this is a CUID counter record (used to track the next CUID number). It shares the + # provider table but is not a provider record, so it carries no compact or providerId and is + # skipped here rather than falling through to the missing-fields error below. + pk = deserialized_image.get('pk', '') + if pk and 'CUID_COUNT' in pk: + logger.info('Skipping CUID count record', message_id=message_id, pk=pk) + continue + compact = deserialized_image.get('compact') provider_id = deserialized_image.get('providerId') record_type = deserialized_image.get('type') diff --git a/backend/social-work-app/lambdas/python/search/handlers/public_search.py b/backend/social-work-app/lambdas/python/search/handlers/public_search.py index 52c2448420..d37e6d7076 100644 --- a/backend/social-work-app/lambdas/python/search/handlers/public_search.py +++ b/backend/social-work-app/lambdas/python/search/handlers/public_search.py @@ -10,6 +10,7 @@ PublicQueryProvidersRequestSchema, ) from cc_common.exceptions import CCInvalidRequestException +from cc_common.license_util import LicenseUtility from cc_common.utils import api_handler from marshmallow import ValidationError from opensearch_client import OpenSearchClient @@ -101,6 +102,11 @@ def _public_query_licenses(event: dict, context: LambdaContext): # noqa: ARG001 body = _parse_and_validate_public_query_body(event) query_obj = body.get('query', {}) + # Resolve to the exact, compact-configured casing (rather than assuming a casing convention) up front, + # so both validation (400 for an unconfigured license type) and the OpenSearch term query built below use + # the same canonical value. + if query_obj.get('licenseType'): + query_obj['licenseType'] = LicenseUtility.get_license_type_by_name(compact, query_obj['licenseType']).name pagination = body.get('pagination') or {} page_size = pagination.get('pageSize') or config.default_page_size @@ -138,6 +144,8 @@ def _public_query_licenses(event: dict, context: LambdaContext): # noqa: ARG001 license_fields['givenName'] = source['givenName'] license_fields['familyName'] = source['familyName'] license_fields['licenseEligibility'] = _determine_license_eligibility(provider_source=source) + if source.get('publicCompactIdentifier'): + license_fields['publicCompactIdentifier'] = source['publicCompactIdentifier'] # home state is stored under the 'jurisdiction' field on the license record, but # the frontend expects this to be labeled 'licenseJurisdiction' for parity with other @@ -273,13 +281,23 @@ def _build_public_license_search_body(*, compact: str, body: dict, cursor: dict nested_must.append({'match': {'licenses.familyName': query_obj['familyName']}}) if query_obj.get('givenName'): nested_must.append({'match': {'licenses.givenName': query_obj['givenName']}}) + if query_obj.get('licenseType'): + # Already resolved to the compact-configured canonical casing in _public_query_licenses, which is + # also what's indexed in OpenSearch (licenses.licenseType is a case-sensitive keyword field). + nested_must.append({'term': {'licenses.licenseType': query_obj['licenseType']}}) nested_query = {'nested': {'path': 'licenses', 'query': {'bool': {'must': nested_must}}}} + # Only providers with an assigned CUID are eligible for public search results, regardless of query criteria. must = [ {'term': {'compact': compact}}, - nested_query, + {'exists': {'field': 'publicCompactIdentifier'}}, ] + if query_obj.get('cuid'): + # The schema's CUID_PATTERN validation is case-insensitive, but the field is indexed as a keyword and + # generated uppercase, so normalize here to guarantee an exact-match term query. + must.append({'term': {'publicCompactIdentifier': query_obj['cuid'].upper()}}) + must.append(nested_query) search_body = { 'query': {'bool': {'must': must}}, diff --git a/backend/social-work-app/lambdas/python/search/opensearch_client.py b/backend/social-work-app/lambdas/python/search/opensearch_client.py index 8edb030d8d..705dfd97d9 100644 --- a/backend/social-work-app/lambdas/python/search/opensearch_client.py +++ b/backend/social-work-app/lambdas/python/search/opensearch_client.py @@ -341,6 +341,7 @@ def _get_provider_index_mapping(self, number_of_shards: int, number_of_replicas: 'type': {'type': 'keyword'}, 'dateOfUpdate': {'type': 'date'}, 'compact': {'type': 'keyword'}, + 'publicCompactIdentifier': {'type': 'keyword'}, 'licenseJurisdiction': {'type': 'keyword'}, 'licenseStatus': {'type': 'keyword'}, 'compactEligibility': {'type': 'keyword'}, diff --git a/backend/social-work-app/lambdas/python/search/tests/function/test_manage_opensearch_indices.py b/backend/social-work-app/lambdas/python/search/tests/function/test_manage_opensearch_indices.py index ac743f7be8..3f1e42720e 100644 --- a/backend/social-work-app/lambdas/python/search/tests/function/test_manage_opensearch_indices.py +++ b/backend/social-work-app/lambdas/python/search/tests/function/test_manage_opensearch_indices.py @@ -336,6 +336,7 @@ def test_on_create_creates_versioned_indices_and_aliases_for_all_compacts_when_n 'providerDateOfUpdate': {'type': 'date'}, 'providerFamGivMid': {'type': 'keyword'}, 'providerId': {'type': 'keyword'}, + 'publicCompactIdentifier': {'type': 'keyword'}, 'suffix': {'type': 'keyword'}, 'type': {'type': 'keyword'}, } diff --git a/backend/social-work-app/lambdas/python/search/tests/function/test_provider_update_ingest.py b/backend/social-work-app/lambdas/python/search/tests/function/test_provider_update_ingest.py index dc0ebf445e..6475af6faa 100644 --- a/backend/social-work-app/lambdas/python/search/tests/function/test_provider_update_ingest.py +++ b/backend/social-work-app/lambdas/python/search/tests/function/test_provider_update_ingest.py @@ -703,3 +703,67 @@ def test_cc_not_found_on_non_remove_event_logs_warning_no_reindex(self, mock_ope # No batch failures - this is expected behavior for a race condition self.assertEqual({'batchItemFailures': []}, result) + + def _create_cuid_count_stream_record(self) -> dict: + """Create a DynamoDB stream record for the CUID count record which tracks numbers to issue.""" + + return { + 'eventID': '6827962b93fa0431321996a5cb9d245c', + 'eventName': 'MODIFY', + 'eventVersion': '1.1', + 'eventSource': 'aws:dynamodb', + 'awsRegion': 'us-east-1', + 'dynamodb': { + 'ApproximateCreationDateTime': 1767934990, + 'Keys': {'sk': {'S': 'socw#CUID_COUNT'}, 'pk': {'S': 'socw#CUID_COUNT'}}, + 'NewImage': { + 'cuidCount': {'N': '322'}, + 'sk': {'S': 'socw#CUID_COUNT'}, + 'pk': {'S': 'socw#CUID_COUNT'}, + }, + 'OldImage': { + 'cuidCount': {'N': '321'}, + 'sk': {'S': 'socw#CUID_COUNT'}, + 'pk': {'S': 'socw#CUID_COUNT'}, + }, + 'SequenceNumber': '1275112700001832307719225248', + 'SizeBytes': 166, + 'StreamViewType': 'NEW_AND_OLD_IMAGES', + }, + 'eventSourceARN': 'some-source-arn', + } + + @patch('handlers.provider_update_ingest.logger') + @patch('handlers.provider_update_ingest.opensearch_client') + def test_cuid_count_record_skipped_without_error_log(self, mock_opensearch_client, mock_logger): + """ + The CUID counter lives in the provider table but is not a provider record. + + Its stream events must be skipped quietly. Logging them at ERROR would fire the + ProviderUpdateIngestErrorLogAlarm every time a CUID is issued to a practitioner. + """ + from handlers.provider_update_ingest import provider_update_ingest_handler + + self._when_testing_mock_opensearch_client(mock_opensearch_client) + + event = { + 'Records': [ + { + 'messageId': '12345', + 'body': json.dumps(self._create_cuid_count_stream_record()), + } + ] + } + + mock_context = MagicMock() + result = provider_update_ingest_handler(event, mock_context) + + # Nothing to index or delete for a counter record + mock_opensearch_client.bulk_index.assert_not_called() + mock_opensearch_client.delete_provider_documents.assert_not_called() + + # Skipped, not failed, so the message is not retried + self.assertEqual({'batchItemFailures': []}, result) + + # Skipped quietly: an ERROR here would page on every CUID issuance + mock_logger.error.assert_not_called() diff --git a/backend/social-work-app/lambdas/python/search/tests/function/test_public_search_providers.py b/backend/social-work-app/lambdas/python/search/tests/function/test_public_search_providers.py index 474d87ba94..2a8fddca84 100644 --- a/backend/social-work-app/lambdas/python/search/tests/function/test_public_search_providers.py +++ b/backend/social-work-app/lambdas/python/search/tests/function/test_public_search_providers.py @@ -56,6 +56,7 @@ def _expected_public_search_request_body( 'bool': { 'must': [ {'term': {'compact': compact}}, + {'exists': {'field': 'publicCompactIdentifier'}}, { 'nested': { 'path': 'licenses', @@ -213,6 +214,7 @@ def _minimal_opensearch_provider_source( license_nested: dict, provider_adverse_actions: list | None = None, privileges: list | None = None, + public_compact_identifier: str | None = None, ) -> dict: """Top-level OpenSearch provider document sufficient for ProviderOpenSearchDocumentSchema.""" lic_exp = license_nested['dateOfExpiration'] @@ -234,6 +236,8 @@ def _minimal_opensearch_provider_source( 'privileges': privileges if privileges is not None else [], 'adverseActions': provider_adverse_actions or [], } + if public_compact_identifier is not None: + source['publicCompactIdentifier'] = public_compact_identifier return self._ingest_style_sanitize_opensearch_source(source) def _create_mock_hit( @@ -249,6 +253,7 @@ def _create_mock_hit( license_nested: dict | None = None, provider_adverse_actions: list | None = None, privileges: list | None = None, + public_compact_identifier: str | None = None, ) -> dict: """Create a mock OpenSearch hit for one document per license.""" doc_id = f'{provider_id}#{jurisdiction}#{license_type}' @@ -270,6 +275,7 @@ def _create_mock_hit( license_nested=nested, provider_adverse_actions=provider_adverse_actions, privileges=privileges, + public_compact_identifier=public_compact_identifier, ) hit = { '_index': f'compact_{compact}_providers', @@ -593,6 +599,103 @@ def test_no_search_criteria_returns_200(self, mock_opensearch_client): body, ) + @patch('handlers.public_search.opensearch_client') + def test_cuid_search_builds_top_level_term_query(self, mock_opensearch_client): + """cuid in query builds a top-level exact-match term query on publicCompactIdentifier (uppercased).""" + from handlers.public_search import public_search_api_handler + + mock_opensearch_client.search.return_value = { + 'hits': {'total': {'value': 0, 'relation': 'eq'}, 'hits': []}, + } + event = self._create_public_api_event( + 'socw', + body={'query': {'cuid': 'swc-1234-5678'}, 'pagination': {'pageSize': 10}}, + ) + public_search_api_handler(event, self.mock_context) + call_body = mock_opensearch_client.search.call_args.kwargs['body'] + must = call_body['query']['bool']['must'] + self.assertIn({'term': {'publicCompactIdentifier': 'SWC-1234-5678'}}, must) + + @patch('handlers.public_search.opensearch_client') + def test_license_type_search_builds_nested_term_query(self, mock_opensearch_client): + """licenseType in query builds a nested term query on licenses.licenseType (lowercased).""" + from handlers.public_search import public_search_api_handler + + mock_opensearch_client.search.return_value = { + 'hits': {'total': {'value': 0, 'relation': 'eq'}, 'hits': []}, + } + event = self._create_public_api_event( + 'socw', + body={ + 'query': {'licenseType': 'Licensed Clinical Social Worker'}, + 'pagination': {'pageSize': 10}, + }, + ) + public_search_api_handler(event, self.mock_context) + call_body = mock_opensearch_client.search.call_args.kwargs['body'] + self.assertEqual( + self._expected_public_search_request_body( + licenses_nested_must=[ + {'term': {'licenses.licenseType': 'licensed clinical social worker'}}, + ], + ), + call_body, + ) + + @patch('handlers.public_search.opensearch_client') + def test_license_type_not_valid_for_compact_returns_400(self, mock_opensearch_client): + """licenseType not configured for the path compact returns 400 and does not call OpenSearch.""" + from handlers.public_search import public_search_api_handler + + event = self._create_public_api_event( + 'socw', + body={'query': {'licenseType': 'not-a-real-license-type'}, 'pagination': {'pageSize': 10}}, + ) + response = public_search_api_handler(event, self.mock_context) + self.assertEqual(400, response['statusCode']) + body = json.loads(response['body']) + self.assertIn('Invalid license type', body['message']) + mock_opensearch_client.search.assert_not_called() + + @patch('handlers.public_search.opensearch_client') + def test_malformed_cuid_returns_400(self, mock_opensearch_client): + """cuid that does not match the CUID pattern returns 400 without ever reaching OpenSearch.""" + from handlers.public_search import public_search_api_handler + + malformed_values = [ + 'SWC-*-1', # wildcard + 'SWC-1234', # partial (missing counter) + '1', # counter-only + 'SWC-123-1', # random segment must be exactly 4 digits + 'SWC-1234-0', # counter must be >= 1, cannot start with 0 + ] + for malformed in malformed_values: + with self.subTest(cuid=malformed): + event = self._create_public_api_event( + 'socw', + body={'query': {'cuid': malformed}, 'pagination': {'pageSize': 10}}, + ) + response = public_search_api_handler(event, self.mock_context) + self.assertEqual(400, response['statusCode']) + mock_opensearch_client.search.assert_not_called() + + @patch('handlers.public_search.opensearch_client') + def test_response_includes_public_compact_identifier(self, mock_opensearch_client): + """publicCompactIdentifier from the OpenSearch hit is copied through to the public response.""" + from handlers.public_search import public_search_api_handler + + mock_hit = self._create_mock_hit(public_compact_identifier='SWC-1234-5678') + mock_opensearch_client.search.return_value = { + 'hits': {'total': {'value': 1, 'relation': 'eq'}, 'hits': [mock_hit]}, + } + event = self._create_public_api_event( + 'socw', + body={'query': {'licenseNumber': 'LN123'}, 'pagination': {'pageSize': 10}}, + ) + response = public_search_api_handler(event, self.mock_context) + body = json.loads(response['body']) + self.assertEqual('SWC-1234-5678', body['providers'][0]['publicCompactIdentifier']) + def test_provider_id_in_query_returns_400(self): """Public query must not accept query.providerId (blocked at schema validation).""" from handlers.public_search import public_search_api_handler diff --git a/backend/social-work-app/lambdas/python/search/tests/function/test_search_providers.py b/backend/social-work-app/lambdas/python/search/tests/function/test_search_providers.py index 82b742f0f0..6e51499b27 100644 --- a/backend/social-work-app/lambdas/python/search/tests/function/test_search_providers.py +++ b/backend/social-work-app/lambdas/python/search/tests/function/test_search_providers.py @@ -214,7 +214,7 @@ def test_search_with_sort_parameter(self, mock_opensearch_client): self._when_testing_mock_opensearch_client(mock_opensearch_client) - sort_config = [{'providerId': 'asc'}, {'dateOfUpdate': 'desc'}] + sort_config = [{'providerId': {'order': 'asc'}}, {'dateOfUpdate': {'order': 'desc'}}] search_after_values = ['provider-uuid-123'] event = self._create_api_event( 'socw', @@ -339,7 +339,7 @@ def test_search_response_includes_last_sort_for_pagination(self, mock_opensearch 'socw', body={ 'query': {'match_all': {}}, - 'sort': [{'providerId': 'asc'}, {'dateOfUpdate': 'asc'}], + 'sort': [{'providerId': {'order': 'asc'}}, {'dateOfUpdate': {'order': 'asc'}}], }, ) @@ -407,22 +407,23 @@ def test_query_with_index_key_returns_400(self): self.assertIn("'index'", body['message']) def test_query_with_underscore_index_key_returns_400(self): - """Test that queries containing '_index' key are rejected with 400 error.""" + """ + Test that queries containing an '_index' key are rejected with a 400 error. + + This exercises the cross-index check itself, by placing '_index' inside an allowlisted + clause so it has to be caught on its own merits rather than by the allowlist. + """ from handlers.search import search_api_handler - # Test with '_index' key (more_like_this attack pattern) event = self._create_api_event( 'socw', body={ 'query': { - 'more_like_this': { - 'fields': ['familyName', 'givenName'], - 'like': [ - { - '_index': 'compact_socw_providers', - '_id': 'target-provider-uuid', - } - ], + 'terms': { + 'providerId': { + '_index': 'compact_socw_providers', + '_id': 'target-provider-uuid', + } } } }, @@ -485,7 +486,7 @@ def test_opensearch_request_error_returns_400_with_error_message(self, mock_open 'socw', body={ 'query': {'match_all': {}}, - 'sort': [{'familyName': 'asc'}], # Sorting on text field causes this error + 'sort': [{'familyName.keyword': {'order': 'asc'}}], }, ) @@ -495,6 +496,195 @@ def test_opensearch_request_error_returns_400_with_error_message(self, mock_open body = json.loads(response['body']) self.assertEqual(error_reason, body['message']) + @patch('handlers.search.opensearch_client') + def test_search_with_query_string_clause_returns_400(self, mock_opensearch_client): + """ + A free-form query_string clause must be rejected outright. + + query_string smuggles the field reference inside a string value, which defeats any + field-name-based permission gate (e.g. the dateOfBirth/readPrivate check) and would + otherwise let a readGeneral-only caller use hit-vs-no-hit as an oracle for a provider's + date of birth. + """ + from handlers.search import search_api_handler + + self._when_testing_mock_opensearch_client(mock_opensearch_client) + + event = self._create_api_event( + 'socw', + body={ + 'query': { + 'bool': { + 'must': [ + {'term': {'providerId': ''}}, + {'query_string': {'query': 'licenses.dateOfBirth:[1985-01-01 TO 1985-06-30]'}}, + ] + } + } + }, + ) + + response = search_api_handler(event, self.mock_context) + + self.assertEqual(400, response['statusCode']) + body = json.loads(response['body']) + self.assertIn('query_string', body['message']) + mock_opensearch_client.search.assert_not_called() + + @patch('handlers.search.opensearch_client') + def test_search_with_disallowed_free_form_clauses_returns_400(self, mock_opensearch_client): + """Every free-form/scripted clause type outside the allowlist must be rejected with a 400.""" + from handlers.search import search_api_handler + + self._when_testing_mock_opensearch_client(mock_opensearch_client) + + disallowed_queries = { + 'simple_query_string': {'simple_query_string': {'query': 'licenses.dateOfBirth:1985*'}}, + 'wildcard': {'wildcard': {'licenses.dateOfBirth': {'value': '1985*'}}}, + 'regexp': {'regexp': {'licenses.dateOfBirth': '198[0-9].*'}}, + 'script': {'script': {'script': {'source': "doc['licenses.dateOfBirth'].size() > 0"}}}, + 'exists': {'exists': {'field': 'dateOfBirth'}}, + 'multi_match': {'multi_match': {'query': '1985', 'fields': ['givenName', 'dateOfBirth']}}, + 'more_like_this': {'more_like_this': {'like': [{'_index': 'compact_cosm_providers'}]}}, + } + + for clause_name, query in disallowed_queries.items(): + with self.subTest(clause=clause_name): + event = self._create_api_event('socw', body={'query': query}) + + response = search_api_handler(event, self.mock_context) + + self.assertEqual(400, response['statusCode']) + body = json.loads(response['body']) + self.assertIn(clause_name, body['message']) + + mock_opensearch_client.search.assert_not_called() + + @patch('handlers.search.opensearch_client') + def test_search_with_disallowed_clause_nested_deep_in_query_returns_400(self, mock_opensearch_client): + """A disallowed clause buried inside nested/bool structures must still be rejected.""" + from handlers.search import search_api_handler + + self._when_testing_mock_opensearch_client(mock_opensearch_client) + + event = self._create_api_event( + 'socw', + body={ + 'query': { + 'bool': { + 'must': [ + {'match': {'givenName': 'John'}}, + { + 'nested': { + 'path': 'licenses', + 'query': { + 'bool': { + 'should': [ + {'term': {'licenses.jurisdiction': 'oh'}}, + {'query_string': {'query': 'licenses.dateOfBirth:1985*'}}, + ] + } + }, + } + }, + ] + } + } + }, + ) + + response = search_api_handler(event, self.mock_context) + + self.assertEqual(400, response['statusCode']) + body = json.loads(response['body']) + self.assertIn('query_string', body['message']) + mock_opensearch_client.search.assert_not_called() + + @patch('handlers.search.opensearch_client') + def test_search_accepts_the_full_query_shape_built_by_the_frontend(self, mock_opensearch_client): + """ + Regression guard: the complete clause vocabulary the frontend builds must remain accepted. + + Mirrors prepRequestSearchParams in webroot/src/network/searchApi/data.api.ts, combining every + clause type it can emit into a single request. + """ + from handlers.search import search_api_handler + + self._when_testing_mock_opensearch_client(mock_opensearch_client) + + event = self._create_api_event( + 'socw', + body={ + 'query': { + 'bool': { + 'must': [ + {'match_phrase_prefix': {'givenName': 'Jo'}}, + {'match_phrase_prefix': {'familyName': 'Do'}}, + {'term': {'licenseJurisdiction': 'oh'}}, + {'match': {'licenseNumber': 'A-123'}}, + { + 'nested': { + 'path': 'privileges', + 'inner_hits': {'size': 100}, + 'query': { + 'bool': { + 'must': [{'term': {'privileges.jurisdiction': 'ky'}}], + 'should': [ + {'range': {'privileges.dateOfIssuance': {'gte': '2024-01-01'}}}, + {'range': {'privileges.dateOfRenewal': {'lte': '2024-12-31'}}}, + ], + 'minimum_should_match': 1, + } + }, + } + }, + { + 'bool': { + 'must_not': [ + { + 'nested': { + 'path': 'licenses', + 'query': { + 'nested': { + 'path': 'licenses.investigations', + 'query': { + 'term': {'licenses.investigations.type': 'investigation'} + }, + } + }, + } + } + ] + } + }, + ] + } + }, + 'size': 25, + 'from': 25, + 'sort': [{'familyName.keyword': {'order': 'asc'}}], + }, + ) + + response = search_api_handler(event, self.mock_context) + + self.assertEqual(200, response['statusCode']) + mock_opensearch_client.search.assert_called_once() + + @patch('handlers.search.opensearch_client') + def test_search_with_match_all_query_is_accepted(self, mock_opensearch_client): + """The frontend's no-search-terms default (match_all) must remain accepted.""" + from handlers.search import search_api_handler + + self._when_testing_mock_opensearch_client(mock_opensearch_client) + + event = self._create_api_event('socw', body={'query': {'match_all': {}}}) + + response = search_api_handler(event, self.mock_context) + + self.assertEqual(200, response['statusCode']) + mock_opensearch_client.search.assert_called_once() + @patch('handlers.search.opensearch_client') def test_search_with_date_of_birth_query_allowed_for_compact_level_read_private_scope(self, mock_opensearch_client): """Test that a query containing dateOfBirth succeeds when the caller has compact-level readPrivate scope.""" @@ -597,12 +787,17 @@ def test_search_with_nested_date_of_birth_query_rejected_without_read_private_sc @patch('handlers.search.opensearch_client') def test_search_with_exists_field_date_of_birth_rejected_without_read_private_scope(self, mock_opensearch_client): - """Test that query with dateOfBirth as field value (e.g. exists) is rejected without readPrivate scope.""" + """ + A readGeneral-only caller must not be able to probe dateOfBirth via an "exists" clause. + + "exists" names its field by value ({"exists": {"field": "dateOfBirth"}}) rather than by key. + That vector is now closed structurally: "exists" is not an allowlisted query type, so the + request is rejected before the field-name-based readPrivate check is ever consulted. + """ from handlers.search import search_api_handler self._when_testing_mock_opensearch_client(mock_opensearch_client) - # OpenSearch "exists" query references field by value: {"exists": {"field": "dateOfBirth"}} event = self._create_api_event( 'socw', body={ @@ -614,13 +809,17 @@ def test_search_with_exists_field_date_of_birth_rejected_without_read_private_sc self.assertEqual(400, response['statusCode']) body = json.loads(response['body']) - self.assertIn('dateOfBirth', body['message']) - self.assertIn('readPrivate', body['message']) + self.assertIn('exists', body['message']) mock_opensearch_client.search.assert_not_called() @patch('handlers.search.opensearch_client') def test_search_with_date_of_birth_string_in_list_rejected_without_read_private_scope(self, mock_opensearch_client): - """dateOfBirth as a list element (e.g. multi_match fields) must not bypass readPrivate checks.""" + """ + A readGeneral-only caller must not be able to probe dateOfBirth via multi_match fields. + + dateOfBirth appears only as a list element here, never as a key. Like "exists", multi_match + is not an allowlisted query type, so the request is rejected outright. + """ from handlers.search import search_api_handler self._when_testing_mock_opensearch_client(mock_opensearch_client) @@ -641,13 +840,19 @@ def test_search_with_date_of_birth_string_in_list_rejected_without_read_private_ self.assertEqual(400, response['statusCode']) body = json.loads(response['body']) - self.assertIn('dateOfBirth', body['message']) - self.assertIn('readPrivate', body['message']) + self.assertIn('multi_match', body['message']) mock_opensearch_client.search.assert_not_called() @patch('handlers.search.opensearch_client') def test_search_with_sort_by_date_of_birth_rejected_without_read_private_scope(self, mock_opensearch_client): - """Test that sort clause referencing dateOfBirth is rejected when caller lacks readPrivate scope.""" + """ + A sort clause referencing dateOfBirth is rejected for a readGeneral-only caller. + + dateOfBirth is not in the sortable-field allowlist, so this is now refused on the field + rather than on the caller's scopes. See + test_search_with_sort_by_date_of_birth_rejected_even_with_read_private_scope for the + readPrivate case, which is refused for the same reason. + """ from handlers.search import search_api_handler self._when_testing_mock_opensearch_client(mock_opensearch_client) @@ -657,7 +862,7 @@ def test_search_with_sort_by_date_of_birth_rejected_without_read_private_scope(s 'socw', body={ 'query': {'match_all': {}}, - 'sort': [{'providerId': 'asc'}, {'licenses.dateOfBirth': 'desc'}], + 'sort': [{'providerId': {'order': 'asc'}}, {'licenses.dateOfBirth': {'order': 'desc'}}], }, ) @@ -666,12 +871,158 @@ def test_search_with_sort_by_date_of_birth_rejected_without_read_private_scope(s self.assertEqual(400, response['statusCode']) body = json.loads(response['body']) self.assertIn('dateOfBirth', body['message']) - self.assertIn('readPrivate', body['message']) mock_opensearch_client.search.assert_not_called() @patch('handlers.search.opensearch_client') - def test_search_with_sort_by_date_of_birth_allowed_with_read_private_scope(self, mock_opensearch_client): - """Test that sort by dateOfBirth succeeds when caller has readPrivate scope.""" + def test_search_with_script_sort_returns_400(self, mock_opensearch_client): + """ + A scripted sort must be rejected outright. + + A _script sort computes an arbitrary value per document, and the handler echoes sort values + straight back to the caller as `lastSort` -- outside the response schema that otherwise + strips restricted fields. That makes a scripted sort a direct read primitive for any indexed + field, not merely an oracle, so it must never reach OpenSearch. + """ + from handlers.search import search_api_handler + + self._when_testing_mock_opensearch_client(mock_opensearch_client) + + event = self._create_api_event( + 'socw', + body={ + 'query': {'match_all': {}}, + 'sort': [ + { + '_script': { + 'type': 'string', + 'script': {'source': "doc['licenses.dateOfBirth'].value"}, + 'order': 'asc', + } + } + ], + }, + ) + + response = search_api_handler(event, self.mock_context) + + self.assertEqual(400, response['statusCode']) + body = json.loads(response['body']) + self.assertIn('_script', body['message']) + mock_opensearch_client.search.assert_not_called() + + @patch('handlers.search.opensearch_client') + def test_search_with_nested_filter_in_sort_returns_400(self, mock_opensearch_client): + """ + A sort entry's `nested.filter` is a full query-DSL position, so it must not be accepted. + + Without this, a free-form clause could be smuggled through sort even though the query itself + is allowlisted, re-opening the dateOfBirth oracle through a different door. + """ + from handlers.search import search_api_handler + + self._when_testing_mock_opensearch_client(mock_opensearch_client) + + event = self._create_api_event( + 'socw', + body={ + 'query': {'match_all': {}}, + 'sort': [ + { + 'dateOfExpiration': { + 'order': 'asc', + 'nested': { + 'path': 'licenses', + 'filter': { + 'query_string': {'query': 'licenses.dateOfBirth:[1985-01-01 TO 1985-06-30]'} + }, + }, + } + } + ], + }, + ) + + response = search_api_handler(event, self.mock_context) + + self.assertEqual(400, response['statusCode']) + body = json.loads(response['body']) + self.assertIn('nested', body['message']) + mock_opensearch_client.search.assert_not_called() + + @patch('handlers.search.opensearch_client') + def test_search_with_disallowed_sort_options_returns_400(self, mock_opensearch_client): + """Only the `order` sort option the frontend emits is accepted; every other option is rejected.""" + from handlers.search import search_api_handler + + self._when_testing_mock_opensearch_client(mock_opensearch_client) + + disallowed_sorts = { + 'nested': [{'dateOfExpiration': {'order': 'asc', 'nested': {'path': 'licenses'}}}], + 'missing': [{'familyName.keyword': {'order': 'asc', 'missing': '_last'}}], + 'unmapped_type': [{'familyName.keyword': {'order': 'asc', 'unmapped_type': 'keyword'}}], + 'mode': [{'dateOfExpiration': {'order': 'asc', 'mode': 'max'}}], + 'format': [{'dateOfExpiration': {'order': 'asc', 'format': 'strict_date_optional_time'}}], + } + + for option_name, sort in disallowed_sorts.items(): + with self.subTest(option=option_name): + event = self._create_api_event('socw', body={'query': {'match_all': {}}, 'sort': sort}) + + response = search_api_handler(event, self.mock_context) + + self.assertEqual(400, response['statusCode']) + body = json.loads(response['body']) + self.assertIn(option_name, body['message']) + + mock_opensearch_client.search.assert_not_called() + + @patch('handlers.search.opensearch_client') + def test_search_with_invalid_sort_order_value_returns_400(self, mock_opensearch_client): + """The sort order must be one of the two values the frontend's SortDirection enum can emit.""" + from handlers.search import search_api_handler + + self._when_testing_mock_opensearch_client(mock_opensearch_client) + + event = self._create_api_event( + 'socw', + body={'query': {'match_all': {}}, 'sort': [{'familyName.keyword': {'order': 'sideways'}}]}, + ) + + response = search_api_handler(event, self.mock_context) + + self.assertEqual(400, response['statusCode']) + body = json.loads(response['body']) + self.assertIn('sideways', body['message']) + mock_opensearch_client.search.assert_not_called() + + @patch('handlers.search.opensearch_client') + def test_search_accepts_the_sort_shape_built_by_the_frontend(self, mock_opensearch_client): + """ + Regression guard: the sort shape the frontend builds must remain accepted. + + Mirrors prepRequestSearchParams in webroot/src/network/searchApi/data.api.ts, which emits + [{'.keyword': {'order': 'asc' | 'desc'}}]. + """ + from handlers.search import search_api_handler + + for order in ('asc', 'desc'): + with self.subTest(order=order): + mock_opensearch_client.reset_mock() + self._when_testing_mock_opensearch_client(mock_opensearch_client) + + event = self._create_api_event( + 'socw', + body={'query': {'match_all': {}}, 'sort': [{'familyName.keyword': {'order': order}}]}, + ) + + response = search_api_handler(event, self.mock_context) + + self.assertEqual(200, response['statusCode']) + mock_opensearch_client.search.assert_called_once() + + @patch('handlers.search.opensearch_client') + def test_search_accepts_multi_field_sort_for_search_after_pagination(self, mock_opensearch_client): + """Multiple sort entries must remain accepted, since search_after pagination needs a tiebreaker.""" from handlers.search import search_api_handler self._when_testing_mock_opensearch_client(mock_opensearch_client) @@ -680,12 +1031,236 @@ def test_search_with_sort_by_date_of_birth_allowed_with_read_private_scope(self, 'socw', body={ 'query': {'match_all': {}}, - 'sort': [{'licenses.dateOfBirth': 'desc'}, {'providerId': 'asc'}], + 'sort': [{'familyName.keyword': {'order': 'asc'}}, {'providerId': {'order': 'asc'}}], + 'search_after': ['Doe', '00000000-0000-0000-0000-000000000001'], }, + ) + + response = search_api_handler(event, self.mock_context) + + self.assertEqual(200, response['statusCode']) + mock_opensearch_client.search.assert_called_once() + + @patch('handlers.search.opensearch_client') + def test_search_with_non_allowlisted_sort_field_returns_400(self, mock_opensearch_client): + """ + Sorting is restricted to an allowlist of non-sensitive fields. + + Sort values are echoed back to the caller as `lastSort`, outside the response schema that + strips restricted fields. Restricting which fields can be sorted on is what keeps that echo + from becoming a read primitive for sensitive data. + """ + from handlers.search import search_api_handler + + self._when_testing_mock_opensearch_client(mock_opensearch_client) + + sensitive_sort_fields = [ + 'licenses.dateOfBirth', + 'licenses.emailAddress', + 'licenses.phoneNumber', + 'licenses.homeAddressStreet1', + 'licenses.homeAddressPostalCode', + 'ssnLastFour', + ] + + for field_name in sensitive_sort_fields: + with self.subTest(field=field_name): + event = self._create_api_event( + 'socw', + body={'query': {'match_all': {}}, 'sort': [{field_name: {'order': 'asc'}}]}, + ) + + response = search_api_handler(event, self.mock_context) + + self.assertEqual(400, response['statusCode']) + body = json.loads(response['body']) + self.assertIn(field_name, body['message']) + + mock_opensearch_client.search.assert_not_called() + + @patch('handlers.search.opensearch_client') + def test_search_with_sort_by_date_of_birth_rejected_even_with_read_private_scope(self, mock_opensearch_client): + """ + dateOfBirth is not a sortable field, even for a caller holding readPrivate. + + Sorting is a separate capability from reading: the sort field allowlist applies to everyone, + so the `lastSort` echo cannot surface a date of birth regardless of the caller's scopes. + Querying by dateOfBirth with readPrivate remains supported. + """ + from handlers.search import search_api_handler + + self._when_testing_mock_opensearch_client(mock_opensearch_client) + + event = self._create_api_event( + 'socw', + body={'query': {'match_all': {}}, 'sort': [{'licenses.dateOfBirth': {'order': 'desc'}}]}, scopes_override='openid email socw/readGeneral socw/readPrivate', ) response = search_api_handler(event, self.mock_context) + self.assertEqual(400, response['statusCode']) + body = json.loads(response['body']) + self.assertIn('dateOfBirth', body['message']) + mock_opensearch_client.search.assert_not_called() + + @patch('handlers.search.opensearch_client') + def test_search_accepts_every_allowlisted_sort_field(self, mock_opensearch_client): + """Regression guard: each allowlisted sort field must remain usable.""" + from handlers.search import search_api_handler + + allowed_sort_fields = [ + 'familyName.keyword', + 'givenName.keyword', + 'providerId', + 'dateOfUpdate', + 'dateOfExpiration', + ] + + for field_name in allowed_sort_fields: + with self.subTest(field=field_name): + mock_opensearch_client.reset_mock() + self._when_testing_mock_opensearch_client(mock_opensearch_client) + + event = self._create_api_event( + 'socw', + body={'query': {'match_all': {}}, 'sort': [{field_name: {'order': 'asc'}}]}, + ) + + response = search_api_handler(event, self.mock_context) + + self.assertEqual(200, response['statusCode']) + mock_opensearch_client.search.assert_called_once() + + @patch('handlers.search.opensearch_client') + def test_search_with_excessively_nested_query_returns_400(self, mock_opensearch_client): + """ + A pathologically nested query is refused with a 400 rather than blowing the Python stack. + + The declarative schema recurses through nested/bool clauses, so an arbitrarily deep query + would otherwise raise RecursionError and surface as an unhandled 500. + """ + from handlers.search import search_api_handler + + self._when_testing_mock_opensearch_client(mock_opensearch_client) + + query = {'match_all': {}} + for _ in range(300): + query = {'bool': {'must': [query]}} + + event = self._create_api_event('socw', body={'query': query}) + + response = search_api_handler(event, self.mock_context) + + self.assertEqual(400, response['statusCode']) + body = json.loads(response['body']) + self.assertIn('too deeply nested', body['message']) + mock_opensearch_client.search.assert_not_called() + + @patch('handlers.search.opensearch_client') + def test_search_with_terms_lookup_object_returns_400(self, mock_opensearch_client): + """ + A terms clause must be a plain list of values; the terms-lookup object form is refused. + + This is enforced structurally by the schema, independently of the cross-index key check: + the lookup below names no index at all, so it reads from the current index and carries no + 'index'/'_index' key for that check to find. + """ + from handlers.search import search_api_handler + + self._when_testing_mock_opensearch_client(mock_opensearch_client) + + event = self._create_api_event( + 'socw', + body={'query': {'terms': {'providerId': {'id': 'some-other-provider', 'path': 'providerId'}}}}, + ) + + response = search_api_handler(event, self.mock_context) + + self.assertEqual(400, response['statusCode']) + body = json.loads(response['body']) + self.assertIn('terms', body['message']) + mock_opensearch_client.search.assert_not_called() + + @patch('handlers.search.opensearch_client') + def test_search_with_non_object_request_body_returns_400(self, mock_opensearch_client): + """ + A request body that is not a JSON object is refused with a 400, not an unhandled error. + + The checks that run ahead of field deserialization see the caller's raw input, which is not + guaranteed to be an object, so they must not assume they can index into it. + """ + from handlers.search import search_api_handler + + self._when_testing_mock_opensearch_client(mock_opensearch_client) + + for raw_body in ('[]', '"a string"', 'null', '42'): + with self.subTest(body=raw_body): + event = self._create_api_event('socw', body={'query': {'match_all': {}}}) + event['body'] = raw_body + + response = search_api_handler(event, self.mock_context) + + self.assertEqual(400, response['statusCode']) + + mock_opensearch_client.search.assert_not_called() + + @patch('handlers.search.opensearch_client') + def test_search_accepts_the_deepest_query_the_frontend_can_build(self, mock_opensearch_client): + """ + The deepest query the frontend can emit must stay within the nesting cap. + + This is the encumbrance-date condition from prepRequestSearchParams, which pairs two nested + levels with a range bound and is the frontend's worst case. It pins _MAX_QUERY_DEPTH against + a real query, so the cap cannot be tuned down far enough to break the UI unnoticed. + """ + from handlers.search import search_api_handler + + self._when_testing_mock_opensearch_client(mock_opensearch_client) + + def encumbrance_condition(top_path: str) -> dict: + nested_path = f'{top_path}.adverseActions' + return { + 'nested': { + 'path': top_path, + 'query': { + 'nested': { + 'path': nested_path, + 'query': { + 'range': { + f'{nested_path}.effectiveStartDate': { + 'gte': '2024-01-01', + 'lte': '2024-12-31', + } + } + }, + } + }, + } + } + + event = self._create_api_event( + 'socw', + body={ + 'query': { + 'bool': { + 'must': [ + { + 'bool': { + 'should': [ + encumbrance_condition('licenses'), + encumbrance_condition('privileges'), + ], + 'minimum_should_match': 1, + } + } + ] + } + } + }, + ) + + response = search_api_handler(event, self.mock_context) + self.assertEqual(200, response['statusCode']) mock_opensearch_client.search.assert_called_once() diff --git a/backend/social-work-app/lambdas/python/search/tests/unit/test_opensearch_client.py b/backend/social-work-app/lambdas/python/search/tests/unit/test_opensearch_client.py index fd19769ebc..ca55f884c7 100644 --- a/backend/social-work-app/lambdas/python/search/tests/unit/test_opensearch_client.py +++ b/backend/social-work-app/lambdas/python/search/tests/unit/test_opensearch_client.py @@ -41,6 +41,15 @@ def test_create_index_calls_internal_client_with_expected_arguments(self): body=index_mapping, ) + def test_provider_index_mapping_includes_top_level_public_compact_identifier_keyword_field(self): + """publicCompactIdentifier must be indexed as a top-level exact-match keyword field so that CUID lookups + are exact-match only (no partial/prefix matching).""" + client, _mock_internal_client = self._create_client_with_mock() + + mapping = client._get_provider_index_mapping(number_of_shards=1, number_of_replicas=1) # noqa: SLF001 + + self.assertEqual({'type': 'keyword'}, mapping['mappings']['properties']['publicCompactIdentifier']) + def test_index_exists_calls_internal_client_with_expected_arguments(self): """Test that index_exists calls the internal client's indices.exists method correctly.""" client, mock_internal_client = self._create_client_with_mock() diff --git a/backend/social-work-app/lambdas/python/staff-users/handlers/users.py b/backend/social-work-app/lambdas/python/staff-users/handlers/users.py index 6e22511915..b58ec592c2 100644 --- a/backend/social-work-app/lambdas/python/staff-users/handlers/users.py +++ b/backend/social-work-app/lambdas/python/staff-users/handlers/users.py @@ -3,7 +3,12 @@ from aws_lambda_powertools.utilities.typing import LambdaContext from botocore.exceptions import ClientError from cc_common.config import config, logger, metrics -from cc_common.exceptions import CCAccessDeniedException, CCInternalException, CCNotFoundException +from cc_common.exceptions import ( + CCAccessDeniedException, + CCInternalException, + CCInvalidRequestException, + CCNotFoundException, +) from cc_common.utils import ( api_handler, authorize_compact, @@ -130,8 +135,15 @@ def post_user(event: dict, context: LambdaContext): # noqa: ARG001 unused-argum # this will raise an exception if the caller was disabled _verify_caller_is_active(event) - # Verify that the client has permission to create a user with the requested permissions - for compact, compact_permissions in body['permissions'].items(): + # Verify that the client has permission to create a user with the requested permissions, and that every + # compact key in the request body matches the path compact. A new user can only ever be created within the + # compact named in the URL; without this check, a caller with admin rights in multiple compacts could have + # the created user silently assigned to whichever compact happened to be the last key in the body. + for body_compact, compact_permissions in body['permissions'].items(): + if body_compact != compact: + raise CCInvalidRequestException( + f"Requested permissions for compact '{body_compact}' do not match the path compact '{compact}'." + ) # This method will raise an exception if they request an inappropriate permission for the new user collect_and_authorize_changes(path_compact=compact, scopes=scopes, compact_changes=compact_permissions) diff --git a/backend/social-work-app/lambdas/python/staff-users/tests/function/test_handlers/test_post_user.py b/backend/social-work-app/lambdas/python/staff-users/tests/function/test_handlers/test_post_user.py index 11c72acc3c..f94598e7b2 100644 --- a/backend/social-work-app/lambdas/python/staff-users/tests/function/test_handlers/test_post_user.py +++ b/backend/social-work-app/lambdas/python/staff-users/tests/function/test_handlers/test_post_user.py @@ -113,6 +113,38 @@ def test_post_user_unauthorized(self): self.assertEqual(403, resp['statusCode']) + def test_post_user_rejects_permissions_for_compact_other_than_path_compact(self): + """ + A caller must not be able to create a user under a compact other than the one named in the URL + path, regardless of what other compact keys appear in the request body's 'permissions' object. + """ + from handlers.users import post_user + + self._when_testing_with_valid_jurisdiction() + caller_id = self._when_testing_with_valid_caller() + + with open('tests/resources/api-event.json') as f: + event = json.load(f) + + with open('tests/resources/api/user-post.json') as f: + api_user = json.load(f) + + # api_user already requests socw permissions; add a second, mismatched compact key. + api_user['permissions']['aslp'] = {'actions': {}, 'jurisdictions': {}} + event['body'] = json.dumps(api_user) + + event['requestContext']['authorizer']['claims']['sub'] = caller_id + event['requestContext']['authorizer']['claims']['scope'] = 'openid email socw/admin oh/socw.admin' + event['pathParameters'] = {'compact': 'socw'} + + resp = post_user(event, self.mock_context) + + self.assertEqual(400, resp['statusCode']) + body = json.loads(resp['body']) + self.assertEqual( + {'message': "Requested permissions for compact 'aslp' do not match the path compact 'socw'."}, body + ) + def test_post_user_returns_400_if_invalid_jurisdiction_permission_set(self): from handlers.users import post_user diff --git a/backend/social-work-app/stacks/api_stack/v1_api/api_model.py b/backend/social-work-app/stacks/api_stack/v1_api/api_model.py index 56cfcb0eac..6d7d19af03 100644 --- a/backend/social-work-app/stacks/api_stack/v1_api/api_model.py +++ b/backend/social-work-app/stacks/api_stack/v1_api/api_model.py @@ -9,6 +9,12 @@ # Importing module level to allow lazy loading for typing from common_constructs import compact_connect_api +# Compact Unique Identifier format: SWC-<4-digit random>-, e.g. SWC-4548-1. +# Case-insensitive on the SWC prefix to mirror CUID_PATTERN (with re.IGNORECASE) in cc_common/data_model/schema/ +# common.py; this is a defense-in-depth check at the API Gateway layer, ahead of the authoritative schema +# validation performed by the lambda. +_CUID_API_MODEL_PATTERN = r'^[Ss][Ww][Cc]-[0-9]{4}-[1-9][0-9]*$' + class ApiModel: """This class is responsible for defining the model definitions used in the API endpoints.""" @@ -125,6 +131,66 @@ def query_providers_request_model(self) -> Model: ) return self.api._v1_query_providers_request_model + @property + def public_query_providers_request_model(self) -> Model: + """Return the public query providers request model, which should only be created once per API.""" + if hasattr(self.api, '_v1_public_query_providers_request_model'): + return self.api._v1_public_query_providers_request_model + self.api._v1_public_query_providers_request_model = self.api.add_model( + 'V1PublicQueryProvidersRequestModel', + description='Public query providers request model', + schema=JsonSchema( + type=JsonSchemaType.OBJECT, + additional_properties=False, + required=['query'], + properties={ + 'query': JsonSchema( + type=JsonSchemaType.OBJECT, + description='The query parameters', + additional_properties=False, + properties={ + 'jurisdiction': JsonSchema( + type=JsonSchemaType.STRING, + description='Filter for providers with license in a jurisdiction', + enum=self.api.node.get_context('jurisdictions'), + ), + 'givenName': JsonSchema( + type=JsonSchemaType.STRING, + max_length=100, + description='Filter for providers with a given name (familyName is required if' + ' givenName is provided)', + ), + 'familyName': JsonSchema( + type=JsonSchemaType.STRING, + max_length=100, + description='Filter for providers with a family name', + ), + 'licenseNumber': JsonSchema( + type=JsonSchemaType.STRING, + min_length=1, + max_length=100, + description='Filter for licenses with a specific license number', + ), + 'cuid': JsonSchema( + type=JsonSchemaType.STRING, + description='Filter for the provider with a specific Compact Unique Identifier', + max_length=64, + pattern=_CUID_API_MODEL_PATTERN, + ), + 'licenseType': JsonSchema( + type=JsonSchemaType.STRING, + description='Filter for licenses of a specific license type', + enum=self.stack.license_type_names, + ), + }, + ), + 'pagination': self._pagination_request_schema, + 'sorting': self._sorting_schema, + }, + ), + ) + return self.api._v1_public_query_providers_request_model + @property def query_providers_response_model(self) -> Model: """Return the query providers response model, which should only be created once per API""" @@ -970,6 +1036,7 @@ def _common_provider_properties(self) -> dict: type=JsonSchemaType.STRING, enum=self.stack.node.get_context('jurisdictions') ), 'dateOfUpdate': JsonSchema(type=JsonSchemaType.STRING, format='date-time'), + 'publicCompactIdentifier': JsonSchema(type=JsonSchemaType.STRING, pattern=_CUID_API_MODEL_PATTERN), } @property @@ -1344,6 +1411,17 @@ def public_query_providers_response_model(self) -> Model: max_length=100, description='Filter for licenses with a specific license number', ), + 'cuid': JsonSchema( + type=JsonSchemaType.STRING, + description='Filter for the provider with a specific Compact Unique Identifier', + max_length=64, + pattern=_CUID_API_MODEL_PATTERN, + ), + 'licenseType': JsonSchema( + type=JsonSchemaType.STRING, + description='Filter for licenses of a specific license type', + enum=self.stack.license_type_names, + ), }, ), 'sorting': self._sorting_schema, @@ -1662,6 +1740,7 @@ def _public_license_search_response_schema(self): description='Whether the license is eligible for compact participation in public search results', enum=['eligible', 'ineligible'], ), + 'publicCompactIdentifier': JsonSchema(type=JsonSchemaType.STRING, pattern=_CUID_API_MODEL_PATTERN), }, ) @@ -1694,6 +1773,7 @@ def _common_public_provider_properties(self) -> dict: 'compact': JsonSchema(type=JsonSchemaType.STRING, enum=stack.node.get_context('compacts')), 'licenseJurisdiction': JsonSchema(type=JsonSchemaType.STRING, enum=stack.node.get_context('jurisdictions')), 'dateOfUpdate': JsonSchema(type=JsonSchemaType.STRING, format='date-time'), + 'publicCompactIdentifier': JsonSchema(type=JsonSchemaType.STRING, pattern=_CUID_API_MODEL_PATTERN), } @property diff --git a/backend/social-work-app/stacks/api_stack/v1_api/public_lookup_api.py b/backend/social-work-app/stacks/api_stack/v1_api/public_lookup_api.py index a0fe63568b..fe0f56f223 100644 --- a/backend/social-work-app/stacks/api_stack/v1_api/public_lookup_api.py +++ b/backend/social-work-app/stacks/api_stack/v1_api/public_lookup_api.py @@ -81,7 +81,7 @@ def _add_public_query_providers(self, search_persistent_stack: sps.SearchPersist public_query_provider_method = query_resource.add_method( 'POST', request_validator=self.api.parameter_body_validator, - request_models={'application/json': self.api_model.query_providers_request_model}, + request_models={'application/json': self.api_model.public_query_providers_request_model}, method_responses=[ MethodResponse( status_code='200', diff --git a/backend/social-work-app/stacks/search_persistent_stack/provider_search_domain.py b/backend/social-work-app/stacks/search_persistent_stack/provider_search_domain.py index 187552620b..b7ff2eb964 100644 --- a/backend/social-work-app/stacks/search_persistent_stack/provider_search_domain.py +++ b/backend/social-work-app/stacks/search_persistent_stack/provider_search_domain.py @@ -168,6 +168,10 @@ def __init__( # https://docs.aws.amazon.com/opensearch-service/latest/developerguide/managedomains-configuration-changes.html # consider working with stakeholders to schedule a maintenance window during low-traffic periods where # advanced search may become inaccessible during the update, to give you time to verify changes. + # We determined through testing that setting the `rest.action.multi.allow_explicit_index: false` domain + # setting causes blue/green deployments to fail consistently. The assumption was that there were Kibana + # related queries that AWS uses under the hood as part of the blue/green cut-over which could not be run + # with that setting set to false, though this was never conclusively verified. version=EngineVersion.OPENSEARCH_3_3, capacity=capacity_config, enable_auto_software_update=True, diff --git a/backend/social-work-app/tests/resources/snapshots/GET_PROVIDER_RESPONSE_SCHEMA.json b/backend/social-work-app/tests/resources/snapshots/GET_PROVIDER_RESPONSE_SCHEMA.json index d906462c05..e172e19896 100644 --- a/backend/social-work-app/tests/resources/snapshots/GET_PROVIDER_RESPONSE_SCHEMA.json +++ b/backend/social-work-app/tests/resources/snapshots/GET_PROVIDER_RESPONSE_SCHEMA.json @@ -2140,6 +2140,10 @@ "dateOfUpdate": { "format": "date-time", "type": "string" + }, + "publicCompactIdentifier": { + "pattern": "^[Ss][Ww][Cc]-[0-9]{4}-[1-9][0-9]*$", + "type": "string" } }, "required": [ diff --git a/backend/social-work-app/tests/resources/snapshots/PUBLIC_GET_PROVIDER_RESPONSE_SCHEMA.json b/backend/social-work-app/tests/resources/snapshots/PUBLIC_GET_PROVIDER_RESPONSE_SCHEMA.json index a6367effd3..d0986826f8 100644 --- a/backend/social-work-app/tests/resources/snapshots/PUBLIC_GET_PROVIDER_RESPONSE_SCHEMA.json +++ b/backend/social-work-app/tests/resources/snapshots/PUBLIC_GET_PROVIDER_RESPONSE_SCHEMA.json @@ -678,6 +678,10 @@ "dateOfUpdate": { "format": "date-time", "type": "string" + }, + "publicCompactIdentifier": { + "pattern": "^[Ss][Ww][Cc]-[0-9]{4}-[1-9][0-9]*$", + "type": "string" } }, "required": [ diff --git a/backend/social-work-app/tests/resources/snapshots/PUBLIC_QUERY_PROVIDERS_REQUEST_SCHEMA.json b/backend/social-work-app/tests/resources/snapshots/PUBLIC_QUERY_PROVIDERS_REQUEST_SCHEMA.json index 85f0e61e0e..cb6eb4d7cb 100644 --- a/backend/social-work-app/tests/resources/snapshots/PUBLIC_QUERY_PROVIDERS_REQUEST_SCHEMA.json +++ b/backend/social-work-app/tests/resources/snapshots/PUBLIC_QUERY_PROVIDERS_REQUEST_SCHEMA.json @@ -5,11 +5,6 @@ "additionalProperties": false, "description": "The query parameters", "properties": { - "providerId": { - "description": "Internal UUID for the provider", - "pattern": "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab]{1}[0-9a-f]{3}-[0-9a-f]{12}", - "type": "string" - }, "jurisdiction": { "description": "Filter for providers with license in a jurisdiction", "enum": [ @@ -85,6 +80,21 @@ "maxLength": 100, "minLength": 1, "type": "string" + }, + "cuid": { + "description": "Filter for the provider with a specific Compact Unique Identifier", + "maxLength": 64, + "pattern": "^[Ss][Ww][Cc]-[0-9]{4}-[1-9][0-9]*$", + "type": "string" + }, + "licenseType": { + "description": "Filter for licenses of a specific license type", + "enum": [ + "licensed clinical social worker", + "licensed master social worker", + "licensed bachelors social worker" + ], + "type": "string" } }, "type": "object" diff --git a/backend/social-work-app/tests/resources/snapshots/PUBLIC_QUERY_PROVIDERS_RESPONSE_SCHEMA.json b/backend/social-work-app/tests/resources/snapshots/PUBLIC_QUERY_PROVIDERS_RESPONSE_SCHEMA.json index af27eb7fd6..44547eedb2 100644 --- a/backend/social-work-app/tests/resources/snapshots/PUBLIC_QUERY_PROVIDERS_RESPONSE_SCHEMA.json +++ b/backend/social-work-app/tests/resources/snapshots/PUBLIC_QUERY_PROVIDERS_RESPONSE_SCHEMA.json @@ -105,6 +105,10 @@ "ineligible" ], "type": "string" + }, + "publicCompactIdentifier": { + "pattern": "^[Ss][Ww][Cc]-[0-9]{4}-[1-9][0-9]*$", + "type": "string" } }, "required": [ @@ -231,6 +235,21 @@ "maxLength": 100, "minLength": 1, "type": "string" + }, + "cuid": { + "description": "Filter for the provider with a specific Compact Unique Identifier", + "maxLength": 64, + "pattern": "^[Ss][Ww][Cc]-[0-9]{4}-[1-9][0-9]*$", + "type": "string" + }, + "licenseType": { + "description": "Filter for licenses of a specific license type", + "enum": [ + "licensed clinical social worker", + "licensed master social worker", + "licensed bachelors social worker" + ], + "type": "string" } }, "type": "object" diff --git a/backend/social-work-app/tests/resources/snapshots/QUERY_PROVIDERS_RESPONSE_SCHEMA.json b/backend/social-work-app/tests/resources/snapshots/QUERY_PROVIDERS_RESPONSE_SCHEMA.json index 1265d5fe20..7dccb18273 100644 --- a/backend/social-work-app/tests/resources/snapshots/QUERY_PROVIDERS_RESPONSE_SCHEMA.json +++ b/backend/social-work-app/tests/resources/snapshots/QUERY_PROVIDERS_RESPONSE_SCHEMA.json @@ -148,6 +148,10 @@ "dateOfUpdate": { "format": "date-time", "type": "string" + }, + "publicCompactIdentifier": { + "pattern": "^[Ss][Ww][Cc]-[0-9]{4}-[1-9][0-9]*$", + "type": "string" } }, "required": [ diff --git a/backend/social-work-app/tests/smoke/license_upload_smoke_tests.py b/backend/social-work-app/tests/smoke/license_upload_smoke_tests.py index e72ec4c33b..d1c2077e73 100644 --- a/backend/social-work-app/tests/smoke/license_upload_smoke_tests.py +++ b/backend/social-work-app/tests/smoke/license_upload_smoke_tests.py @@ -1,5 +1,6 @@ # ruff: noqa: T201 we use print statements for smoke testing #!/usr/bin/env python3 +import re import time from datetime import UTC, datetime @@ -10,6 +11,7 @@ from smoke_common import ( SmokeTestFailureException, call_provider_details_endpoint, + call_public_query_providers, create_test_app_client, create_test_staff_user, delete_test_app_client, @@ -57,6 +59,48 @@ class PractitionerTestState: provider_id: str | None = None start_time: datetime | None = None + # Compact Unique Identifier, assigned once the practitioner holds a paired single-state and + # multi-state license. Captured during the home state change test and consumed by the CUID + # public search test. + public_compact_identifier: str | None = None + + +def _assert_cuid_not_assigned(provider_details: dict, *, after: str) -> None: + """ + Assert no CUID has been issued yet. + + A CUID is only issued once the practitioner holds a paired single-state and multi-state license + for the same jurisdiction and license type, so it must be absent before that pairing exists. + """ + public_compact_identifier = provider_details.get('publicCompactIdentifier') + if public_compact_identifier is not None: + raise SmokeTestFailureException( + f'Expected no publicCompactIdentifier on the provider record after {after}, ' + f'found {public_compact_identifier!r}' + ) + + logger.info(f'Verified publicCompactIdentifier is not assigned after {after}') + + +def _assert_cuid_assigned(provider_details: dict) -> str: + """Assert a well-formed CUID has been issued, and return it.""" + from cc_common.data_model.schema.common import CUID_PATTERN + + public_compact_identifier = provider_details.get('publicCompactIdentifier') + if not public_compact_identifier: + raise SmokeTestFailureException( + 'Expected publicCompactIdentifier to be assigned once the paired multi-state license was ' + 'uploaded, but the field is missing from the provider record.' + ) + + if not re.match(CUID_PATTERN, public_compact_identifier): + raise SmokeTestFailureException( + f'publicCompactIdentifier {public_compact_identifier!r} does not match the expected ' + f'CUID format {CUID_PATTERN}' + ) + + logger.info(f'Verified publicCompactIdentifier {public_compact_identifier} was assigned') + return public_compact_identifier def _cleanup_test_generated_records(provider_id: str, license_ingest_record_response: dict): @@ -201,8 +245,13 @@ def _wait_for_oh_license_scope( *, max_wait_seconds: int = 720, poll_interval_seconds: int = 60, -): - """Wait until OH license with the given scope is visible before uploading the paired scope.""" +) -> dict: + """ + Wait until the OH license with the given scope is visible before uploading the paired scope. + + Returns the provider details fetched on the successful attempt, so callers can assert against + the provider record at that point without issuing a second request. + """ max_attempts = max_wait_seconds // poll_interval_seconds for attempt in range(1, max_attempts + 1): staff_headers = get_staff_user_auth_headers(TEST_STAFF_USER_EMAIL) @@ -218,7 +267,7 @@ def _wait_for_oh_license_scope( ] if matching_licenses: logger.info(f'Found OH {HOME_STATE_CHANGE_LICENSE_TYPE} {license_scope} license for provider {provider_id}') - return + return provider_details if attempt < max_attempts: logger.info( @@ -347,13 +396,17 @@ def test_home_state_change_notification( ), ) - _wait_for_oh_license_scope( + oh_single_state_provider_details = _wait_for_oh_license_scope( provider_id=provider_id, license_scope='single-state', max_wait_seconds=750, poll_interval_seconds=60, ) + # The practitioner now holds two single-state licenses and no multi-state license, so there is + # still no pairing and no CUID should have been issued. + _assert_cuid_not_assigned(oh_single_state_provider_details, after='the OH single-state license upload') + if _query_home_state_change_events_for_provider(provider_id): raise SmokeTestFailureException( 'provider.homeStateChange must not fire after OH single-state upload without a paired multi-state license.' @@ -416,6 +469,10 @@ def test_home_state_change_notification( f'{HOME_STATE_CHANGE_NEW_JURISDICTION}, found {updated_provider_details.get("licenseJurisdiction")}' ) + # The OH multi-state license completes the pairing with the OH single-state license, so a CUID + # is issued at this point. Captured for the public search test that follows. + practitioner_state.public_compact_identifier = _assert_cuid_assigned(updated_provider_details) + logger.info( 'MANUAL VERIFICATION REQUIRED: check inbox for ' f'{config.smoke_test_notification_email}. Verify a provider home state change email was sent to ' @@ -515,6 +572,73 @@ def test_provider_privileges_exclude_unrecognized_license_type_jurisdictions(pro ) +def _wait_for_cuid_search_results( + public_compact_identifier: str, + *, + max_wait_seconds: int = 300, + poll_interval_seconds: int = 30, +) -> list[dict]: + """ + Poll the public search endpoint until the practitioner's CUID is searchable. + + The CUID reaches OpenSearch through the provider table stream, so it is not queryable the + instant the provider record is written. + """ + max_attempts = max_wait_seconds // poll_interval_seconds + for attempt in range(1, max_attempts + 1): + matching_rows = call_public_query_providers(COMPACT, cuid_filter=public_compact_identifier) + if matching_rows: + return matching_rows + + if attempt < max_attempts: + logger.info( + f'CUID {public_compact_identifier} not searchable yet. ' + f'Attempt {attempt}/{max_attempts}. Retrying in {poll_interval_seconds} seconds.' + ) + time.sleep(poll_interval_seconds) + + raise SmokeTestFailureException( + f'Timed out waiting for CUID {public_compact_identifier} to become searchable via public search.' + ) + + +def test_provider_is_searchable_by_cuid(practitioner_state: PractitionerTestState) -> None: + """Verify the public search endpoint returns exactly this practitioner when queried by their CUID.""" + provider_id = practitioner_state.provider_id + public_compact_identifier = practitioner_state.public_compact_identifier + if not provider_id or not public_compact_identifier: + raise SmokeTestFailureException( + 'Provider id and publicCompactIdentifier must both be set by the home state change test ' + 'before the practitioner can be searched by CUID.' + ) + + matching_rows = _wait_for_cuid_search_results(public_compact_identifier) + + if len(matching_rows) != 1: + raise SmokeTestFailureException( + f'Expected exactly one practitioner for CUID {public_compact_identifier}, found {len(matching_rows)} ' + f'with provider ids {[row.get("providerId") for row in matching_rows]}' + ) + + matching_row = matching_rows[0] + if matching_row.get('providerId') != provider_id: + raise SmokeTestFailureException( + f'Expected CUID {public_compact_identifier} to resolve to provider {provider_id}, ' + f'found {matching_row.get("providerId")}' + ) + + if matching_row.get('publicCompactIdentifier') != public_compact_identifier: + raise SmokeTestFailureException( + f'Expected the returned row to carry publicCompactIdentifier {public_compact_identifier}, ' + f'found {matching_row.get("publicCompactIdentifier")!r}' + ) + + logger.info( + f'Successfully verified CUID {public_compact_identifier} returns only provider {provider_id} ' + 'from the public search endpoint' + ) + + def _query_home_state_change_events_for_provider(provider_id: str): data_events_table = get_data_events_dynamodb_table() event_pk = f'COMPACT#{COMPACT}#JURISDICTION#{HOME_STATE_CHANGE_NEW_JURISDICTION}' @@ -575,6 +699,7 @@ def _query_home_state_change_events_for_provider(provider_id: str): practitioner_state=practitioner_state, ) test_provider_privileges_exclude_unrecognized_license_type_jurisdictions(practitioner_state.provider_id) + test_provider_is_searchable_by_cuid(practitioner_state) logger.info('License upload smoke tests passed') except SmokeTestFailureException as e: logger.error(f'License record upload smoke test failed: {str(e)}') diff --git a/backend/social-work-app/tests/smoke/smoke_common.py b/backend/social-work-app/tests/smoke/smoke_common.py index 4bcb0c723d..6a47a0d512 100644 --- a/backend/social-work-app/tests/smoke/smoke_common.py +++ b/backend/social-work-app/tests/smoke/smoke_common.py @@ -247,17 +247,20 @@ def get_most_recently_issued_or_renewed_license(licenses: list[dict]) -> dict | def call_public_query_providers( compact: str, *, - provider_id_filter: str, + provider_id_filter: str | None = None, first_name_filter: str | None = None, last_name_filter: str | None = None, license_number_filter: str | None = None, + cuid_filter: str | None = None, page_size: int = 100, timeout: int = 30, ) -> list[dict]: """ POST /v1/public/compacts/{compact}/providers/query (no auth). - Builds query body from provided filters + Builds the query body from the provided filters. ``provider_id_filter`` is applied client side + to the returned rows; pass None to get every row the endpoint returned, which is what a caller + needs in order to assert on the total number of matches. """ url = f'{config.api_base_url}/v1/public/compacts/{compact}/providers/query' headers = {'Content-Type': 'application/json'} @@ -268,6 +271,8 @@ def call_public_query_providers( query_parameters['familyName'] = last_name_filter if license_number_filter: query_parameters['licenseNumber'] = license_number_filter + if cuid_filter: + query_parameters['cuid'] = cuid_filter pagination_state: dict = {'pageSize': page_size} matching_license_rows: list[dict] = [] @@ -280,7 +285,7 @@ def call_public_query_providers( page_response_body = response.json() for provider_license_row in page_response_body.get('providers') or []: - if provider_id_filter and provider_license_row.get('providerId') == provider_id_filter: + if provider_id_filter is None or provider_license_row.get('providerId') == provider_id_filter: matching_license_rows.append(provider_license_row) last_pagination_key = (page_response_body.get('pagination') or {}).get('lastKey')