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
+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");