feat: add product to opportunity route, local product sequencing

- Add POST /v1/sales/opportunities/:identifier/products with field-level permission gating
- Add CWForecastItemCreate type for forecast item creation
- Store product display order locally (productSequence Int[] on Opportunity)
- Rewrite resequenceProducts to be local-only (no CW PUT, stable IDs)
- Remove reorderProducts CW util (PUT regenerated IDs & broke procurement)
- Update fetchProducts to apply local ordering with CW sequenceNumber fallback
- Add productSequence to OpportunityController.toJson()
- Update API_ROUTES.md, PERMISSIONS.md, PermissionNodes.ts
This commit is contained in:
2026-03-01 18:01:02 -06:00
parent d7b374f8ab
commit 30b408e0db
19 changed files with 1030 additions and 107 deletions
+3 -3
View File
@@ -243,9 +243,9 @@ describe("catalogCategories", () => {
// -------------------------------------------------------------------
describe("matchesEcosystem()", () => {
test("matches Ubiquiti Network-Switch to Networking", () => {
expect(
matchesEcosystem("Networking", "Ubiquiti", "Network-Switch"),
).toBe(true);
expect(matchesEcosystem("Networking", "Ubiquiti", "Network-Switch")).toBe(
true,
);
});
test("matches Uniview Surveillance-CamerasIP to Video Surveillance", () => {
+13 -2
View File
@@ -35,7 +35,15 @@ describe("memberCache", () => {
test("stores and retrieves members", async () => {
const members = new Collection<string, CWMember>();
members.set("jroberts", buildTestMember());
members.set("asmith", buildTestMember({ id: 20, identifier: "asmith", firstName: "Alice", lastName: "Smith" }));
members.set(
"asmith",
buildTestMember({
id: 20,
identifier: "asmith",
firstName: "Alice",
lastName: "Smith",
}),
);
await setMemberCache(members);
const cached = await getMemberCache();
@@ -67,7 +75,10 @@ describe("memberCache", () => {
test("falls back to identifier if name parts are empty", async () => {
const members = new Collection<string, CWMember>();
members.set("empty", buildTestMember({ identifier: "empty", firstName: "", lastName: "" }));
members.set(
"empty",
buildTestMember({ identifier: "empty", firstName: "", lastName: "" }),
);
await setMemberCache(members);
expect(resolveMemberName("empty")).toBe("empty");