-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1087 from Enterprise-CMCS/main
Release to val
- Loading branch information
Showing
26 changed files
with
1,969 additions
and
103 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import { search } from "libs/opensearch-lib"; | ||
import { getDomainAndNamespace } from "libs/utils"; | ||
import { cpocs } from "lib/packages/shared-types/opensearch"; | ||
|
||
export const getNextSplitSPAId = async (spaId: string) => { | ||
const { domain, index } = getDomainAndNamespace("main"); | ||
const query = { | ||
size: 50, | ||
query: { | ||
regexp: { | ||
"id.keyword": `${spaId}-[A-Z]`, | ||
}, | ||
}, | ||
}; | ||
// Get existing split SPAs for this package id | ||
const { hits } = await search(domain, index, query); | ||
// Extract suffixes from existing split SPA IDs | ||
// If there are no split SPAs yet, start at the ASCII character before "A" ("@") | ||
// Convert to ASCII char codes to get latest suffix | ||
const latestSuffixCharCode = hits.hits.reduce((maxCharCode: number, hit: cpocs.ItemResult) => { | ||
const suffix = hit._source.id.toString().split("-").at(-1) ?? "@"; | ||
return Math.max(maxCharCode, suffix.charCodeAt(0)); | ||
}, "@".charCodeAt(0)); | ||
|
||
// Increment letter but not past "Z" | ||
// "A-Z" is 65-90 in ASCII | ||
if (latestSuffixCharCode >= 90) { | ||
throw new Error("This package can't be further split."); | ||
} | ||
const nextSuffix = String.fromCharCode(latestSuffixCharCode + 1); | ||
|
||
return `${spaId}-${nextSuffix}`; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
import { describe, it, expect, vi, beforeEach } from "vitest"; | ||
import { handler } from "./submitSplitSPA"; | ||
import { APIGatewayEvent } from "node_modules/shared-types"; | ||
import { | ||
getRequestContext, | ||
TEST_CHIP_SPA_ITEM, | ||
TEST_MED_SPA_ITEM, | ||
TEST_SPA_ITEM_TO_SPLIT, | ||
} from "mocks"; | ||
|
||
describe("handler", () => { | ||
beforeEach(() => { | ||
vi.clearAllMocks(); | ||
process.env.topicName = "test-topic"; | ||
}); | ||
|
||
it("should return 400 if event body is missing", async () => { | ||
const event = {} as APIGatewayEvent; | ||
|
||
const result = await handler(event); | ||
|
||
expect(result?.statusCode).toEqual(400); | ||
}); | ||
|
||
it("should return 404 if package ID is not found", async () => { | ||
const invalidPackage = { | ||
body: JSON.stringify({ packageId: "MD-25-9999" }), | ||
} as unknown as APIGatewayEvent; | ||
|
||
const result = await handler(invalidPackage); | ||
|
||
expect(result?.statusCode).toEqual(404); | ||
}); | ||
|
||
it("should throw an error if not Medicaid SPA", async () => { | ||
const chipSPAPackage = { | ||
body: JSON.stringify({ packageId: TEST_CHIP_SPA_ITEM._id }), | ||
requestContext: getRequestContext(), | ||
} as APIGatewayEvent; | ||
|
||
const result = await handler(chipSPAPackage); | ||
|
||
expect(result.body).toEqual(JSON.stringify({ message: "Record must be a Medicaid SPA" })); | ||
}); | ||
|
||
it("should return 400 if package ID not provided", async () => { | ||
const invalidPackage = { | ||
body: JSON.stringify({}), | ||
} as unknown as APIGatewayEvent; | ||
|
||
const result = await handler(invalidPackage); | ||
|
||
expect(result?.statusCode).toEqual(400); | ||
}); | ||
|
||
it("should fail to split a package with no topic name", async () => { | ||
delete process.env.topicName; | ||
|
||
const noActionevent = { | ||
body: JSON.stringify({ | ||
packageId: TEST_MED_SPA_ITEM._id, | ||
}), | ||
} as APIGatewayEvent; | ||
|
||
await expect(handler(noActionevent)).rejects.toThrow("Topic name is not defined"); | ||
}); | ||
|
||
it("should create a split SPA", async () => { | ||
const medSPAPackage = { | ||
body: JSON.stringify({ packageId: TEST_MED_SPA_ITEM._id }), | ||
} as unknown as APIGatewayEvent; | ||
|
||
const result = await handler(medSPAPackage); | ||
expect(result?.statusCode).toEqual(200); | ||
}); | ||
|
||
it("should fail if unable to get next split SPA suffix", async () => { | ||
const medSPAPackage = { | ||
body: JSON.stringify({ packageId: TEST_SPA_ITEM_TO_SPLIT }), | ||
} as unknown as APIGatewayEvent; | ||
|
||
await expect(handler(medSPAPackage)).rejects.toThrow("This package can't be further split."); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
import { response } from "libs/handler-lib"; | ||
import { APIGatewayEvent } from "aws-lambda"; | ||
import { getPackage } from "libs/api/package"; | ||
import { produceMessage } from "libs/api/kafka"; | ||
import { ItemResult } from "shared-types/opensearch/main"; | ||
import { events } from "shared-types/events"; | ||
import { getNextSplitSPAId } from "./getNextSplitSPAId"; | ||
import { z } from "zod"; | ||
|
||
/* | ||
EXAMPLE EVENT JSON: | ||
{ | ||
"body": { | ||
"packageId": "MD-25-9999", | ||
} | ||
} | ||
*/ | ||
|
||
const sendSubmitSplitSPAMessage = async (currentPackage: ItemResult) => { | ||
const topicName = process.env.topicName as string; | ||
if (!topicName) { | ||
throw new Error("Topic name is not defined"); | ||
} | ||
const newId = await getNextSplitSPAId(currentPackage._id); | ||
if (!newId) { | ||
throw new Error("Error getting next Split SPA Id"); | ||
} | ||
|
||
// ID and changeMade are excluded; the rest of the object has to be spread into the new package | ||
const { | ||
id: _id, | ||
changeMade: _changeMade, | ||
origin: _origin, | ||
...remainingFields | ||
} = currentPackage._source; | ||
|
||
await produceMessage( | ||
topicName, | ||
newId, | ||
JSON.stringify({ | ||
id: newId, | ||
idToBeUpdated: currentPackage._id, | ||
...remainingFields, | ||
makoChangedDate: Date.now(), | ||
changedDate: Date.now(), | ||
origin: "OneMAC", | ||
changeMade: "OneMAC Admin has added a package to OneMAC.", | ||
changeReason: "Per request from CMS, this package was added to OneMAC.", | ||
mockEvent: "new-medicaid-submission", | ||
isAdminChange: true, | ||
adminChangeType: "split-spa", | ||
}), | ||
); | ||
|
||
return response({ | ||
statusCode: 200, | ||
body: { message: `New Medicaid Split SPA ${newId} has been created.` }, | ||
}); | ||
}; | ||
|
||
const splitSPAEventBodySchema = z.object({ | ||
packageId: events["new-medicaid-submission"].baseSchema.shape.id, | ||
}); | ||
|
||
export const handler = async (event: APIGatewayEvent) => { | ||
if (!event.body) { | ||
return response({ | ||
statusCode: 400, | ||
body: { message: "Event body required" }, | ||
}); | ||
} | ||
try { | ||
const body = typeof event.body === "string" ? JSON.parse(event.body) : event.body; | ||
const { packageId } = splitSPAEventBodySchema.parse(body); | ||
|
||
const currentPackage = await getPackage(packageId); | ||
if (!currentPackage || currentPackage.found == false) { | ||
return response({ | ||
statusCode: 404, | ||
body: { message: "No record found for the given id" }, | ||
}); | ||
} | ||
|
||
if (currentPackage._source.authority !== "Medicaid SPA") { | ||
return response({ | ||
statusCode: 400, | ||
body: { message: "Record must be a Medicaid SPA" }, | ||
}); | ||
} | ||
|
||
return sendSubmitSplitSPAMessage(currentPackage); | ||
} catch (err) { | ||
console.error("Error has occured modifying package:", err); | ||
if (err instanceof z.ZodError) { | ||
return response({ | ||
statusCode: 400, | ||
body: { message: err.errors }, | ||
}); | ||
} | ||
return response({ | ||
statusCode: 500, | ||
body: { message: err.message || "Internal Server Error" }, | ||
}); | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.