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
+53 -1
View File
@@ -43,6 +43,7 @@ export type OpportunityAvgAggregateOutputType = {
locationCwId: number | null
departmentCwId: number | null
closedByCwId: number | null
productSequence: number | null
}
export type OpportunitySumAggregateOutputType = {
@@ -62,6 +63,7 @@ export type OpportunitySumAggregateOutputType = {
locationCwId: number | null
departmentCwId: number | null
closedByCwId: number | null
productSequence: number[]
}
export type OpportunityMinAggregateOutputType = {
@@ -206,6 +208,7 @@ export type OpportunityCountAggregateOutputType = {
closedByName: number
closedByCwId: number
companyId: number
productSequence: number
cwLastUpdated: number
createdAt: number
updatedAt: number
@@ -230,6 +233,7 @@ export type OpportunityAvgAggregateInputType = {
locationCwId?: true
departmentCwId?: true
closedByCwId?: true
productSequence?: true
}
export type OpportunitySumAggregateInputType = {
@@ -249,6 +253,7 @@ export type OpportunitySumAggregateInputType = {
locationCwId?: true
departmentCwId?: true
closedByCwId?: true
productSequence?: true
}
export type OpportunityMinAggregateInputType = {
@@ -393,6 +398,7 @@ export type OpportunityCountAggregateInputType = {
closedByName?: true
closedByCwId?: true
companyId?: true
productSequence?: true
cwLastUpdated?: true
createdAt?: true
updatedAt?: true
@@ -529,6 +535,7 @@ export type OpportunityGroupByOutputType = {
closedByName: string | null
closedByCwId: number | null
companyId: string | null
productSequence: number[]
cwLastUpdated: Date | null
createdAt: Date
updatedAt: Date
@@ -601,6 +608,7 @@ export type OpportunityWhereInput = {
closedByName?: Prisma.StringNullableFilter<"Opportunity"> | string | null
closedByCwId?: Prisma.IntNullableFilter<"Opportunity"> | number | null
companyId?: Prisma.StringNullableFilter<"Opportunity"> | string | null
productSequence?: Prisma.IntNullableListFilter<"Opportunity">
cwLastUpdated?: Prisma.DateTimeNullableFilter<"Opportunity"> | Date | string | null
createdAt?: Prisma.DateTimeFilter<"Opportunity"> | Date | string
updatedAt?: Prisma.DateTimeFilter<"Opportunity"> | Date | string
@@ -651,6 +659,7 @@ export type OpportunityOrderByWithRelationInput = {
closedByName?: Prisma.SortOrderInput | Prisma.SortOrder
closedByCwId?: Prisma.SortOrderInput | Prisma.SortOrder
companyId?: Prisma.SortOrderInput | Prisma.SortOrder
productSequence?: Prisma.SortOrder
cwLastUpdated?: Prisma.SortOrderInput | Prisma.SortOrder
createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
@@ -704,6 +713,7 @@ export type OpportunityWhereUniqueInput = Prisma.AtLeast<{
closedByName?: Prisma.StringNullableFilter<"Opportunity"> | string | null
closedByCwId?: Prisma.IntNullableFilter<"Opportunity"> | number | null
companyId?: Prisma.StringNullableFilter<"Opportunity"> | string | null
productSequence?: Prisma.IntNullableListFilter<"Opportunity">
cwLastUpdated?: Prisma.DateTimeNullableFilter<"Opportunity"> | Date | string | null
createdAt?: Prisma.DateTimeFilter<"Opportunity"> | Date | string
updatedAt?: Prisma.DateTimeFilter<"Opportunity"> | Date | string
@@ -754,6 +764,7 @@ export type OpportunityOrderByWithAggregationInput = {
closedByName?: Prisma.SortOrderInput | Prisma.SortOrder
closedByCwId?: Prisma.SortOrderInput | Prisma.SortOrder
companyId?: Prisma.SortOrderInput | Prisma.SortOrder
productSequence?: Prisma.SortOrder
cwLastUpdated?: Prisma.SortOrderInput | Prisma.SortOrder
createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
@@ -811,6 +822,7 @@ export type OpportunityScalarWhereWithAggregatesInput = {
closedByName?: Prisma.StringNullableWithAggregatesFilter<"Opportunity"> | string | null
closedByCwId?: Prisma.IntNullableWithAggregatesFilter<"Opportunity"> | number | null
companyId?: Prisma.StringNullableWithAggregatesFilter<"Opportunity"> | string | null
productSequence?: Prisma.IntNullableListFilter<"Opportunity">
cwLastUpdated?: Prisma.DateTimeNullableWithAggregatesFilter<"Opportunity"> | Date | string | null
createdAt?: Prisma.DateTimeWithAggregatesFilter<"Opportunity"> | Date | string
updatedAt?: Prisma.DateTimeWithAggregatesFilter<"Opportunity"> | Date | string
@@ -859,6 +871,7 @@ export type OpportunityCreateInput = {
closedFlag?: boolean
closedByName?: string | null
closedByCwId?: number | null
productSequence?: Prisma.OpportunityCreateproductSequenceInput | number[]
cwLastUpdated?: Date | string | null
createdAt?: Date | string
updatedAt?: Date | string
@@ -909,6 +922,7 @@ export type OpportunityUncheckedCreateInput = {
closedByName?: string | null
closedByCwId?: number | null
companyId?: string | null
productSequence?: Prisma.OpportunityCreateproductSequenceInput | number[]
cwLastUpdated?: Date | string | null
createdAt?: Date | string
updatedAt?: Date | string
@@ -957,6 +971,7 @@ export type OpportunityUpdateInput = {
closedFlag?: Prisma.BoolFieldUpdateOperationsInput | boolean
closedByName?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
closedByCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
productSequence?: Prisma.OpportunityUpdateproductSequenceInput | number[]
cwLastUpdated?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
@@ -1007,6 +1022,7 @@ export type OpportunityUncheckedUpdateInput = {
closedByName?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
closedByCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
companyId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
productSequence?: Prisma.OpportunityUpdateproductSequenceInput | number[]
cwLastUpdated?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
@@ -1056,6 +1072,7 @@ export type OpportunityCreateManyInput = {
closedByName?: string | null
closedByCwId?: number | null
companyId?: string | null
productSequence?: Prisma.OpportunityCreateproductSequenceInput | number[]
cwLastUpdated?: Date | string | null
createdAt?: Date | string
updatedAt?: Date | string
@@ -1104,6 +1121,7 @@ export type OpportunityUpdateManyMutationInput = {
closedFlag?: Prisma.BoolFieldUpdateOperationsInput | boolean
closedByName?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
closedByCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
productSequence?: Prisma.OpportunityUpdateproductSequenceInput | number[]
cwLastUpdated?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
@@ -1153,6 +1171,7 @@ export type OpportunityUncheckedUpdateManyInput = {
closedByName?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
closedByCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
companyId?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
productSequence?: Prisma.OpportunityUpdateproductSequenceInput | number[]
cwLastUpdated?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
@@ -1168,6 +1187,14 @@ export type OpportunityOrderByRelationAggregateInput = {
_count?: Prisma.SortOrder
}
export type IntNullableListFilter<$PrismaModel = never> = {
equals?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel> | null
has?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
hasEvery?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
hasSome?: number[] | Prisma.ListIntFieldRefInput<$PrismaModel>
isEmpty?: boolean
}
export type OpportunityCountOrderByAggregateInput = {
id?: Prisma.SortOrder
cwOpportunityId?: Prisma.SortOrder
@@ -1212,6 +1239,7 @@ export type OpportunityCountOrderByAggregateInput = {
closedByName?: Prisma.SortOrder
closedByCwId?: Prisma.SortOrder
companyId?: Prisma.SortOrder
productSequence?: Prisma.SortOrder
cwLastUpdated?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
@@ -1234,6 +1262,7 @@ export type OpportunityAvgOrderByAggregateInput = {
locationCwId?: Prisma.SortOrder
departmentCwId?: Prisma.SortOrder
closedByCwId?: Prisma.SortOrder
productSequence?: Prisma.SortOrder
}
export type OpportunityMaxOrderByAggregateInput = {
@@ -1351,6 +1380,7 @@ export type OpportunitySumOrderByAggregateInput = {
locationCwId?: Prisma.SortOrder
departmentCwId?: Prisma.SortOrder
closedByCwId?: Prisma.SortOrder
productSequence?: Prisma.SortOrder
}
export type OpportunityCreateNestedManyWithoutCompanyInput = {
@@ -1395,6 +1425,15 @@ export type OpportunityUncheckedUpdateManyWithoutCompanyNestedInput = {
deleteMany?: Prisma.OpportunityScalarWhereInput | Prisma.OpportunityScalarWhereInput[]
}
export type OpportunityCreateproductSequenceInput = {
set: number[]
}
export type OpportunityUpdateproductSequenceInput = {
set?: number[]
push?: number | number[]
}
export type OpportunityCreateWithoutCompanyInput = {
id?: string
cwOpportunityId: number
@@ -1438,6 +1477,7 @@ export type OpportunityCreateWithoutCompanyInput = {
closedFlag?: boolean
closedByName?: string | null
closedByCwId?: number | null
productSequence?: Prisma.OpportunityCreateproductSequenceInput | number[]
cwLastUpdated?: Date | string | null
createdAt?: Date | string
updatedAt?: Date | string
@@ -1486,6 +1526,7 @@ export type OpportunityUncheckedCreateWithoutCompanyInput = {
closedFlag?: boolean
closedByName?: string | null
closedByCwId?: number | null
productSequence?: Prisma.OpportunityCreateproductSequenceInput | number[]
cwLastUpdated?: Date | string | null
createdAt?: Date | string
updatedAt?: Date | string
@@ -1564,6 +1605,7 @@ export type OpportunityScalarWhereInput = {
closedByName?: Prisma.StringNullableFilter<"Opportunity"> | string | null
closedByCwId?: Prisma.IntNullableFilter<"Opportunity"> | number | null
companyId?: Prisma.StringNullableFilter<"Opportunity"> | string | null
productSequence?: Prisma.IntNullableListFilter<"Opportunity">
cwLastUpdated?: Prisma.DateTimeNullableFilter<"Opportunity"> | Date | string | null
createdAt?: Prisma.DateTimeFilter<"Opportunity"> | Date | string
updatedAt?: Prisma.DateTimeFilter<"Opportunity"> | Date | string
@@ -1612,6 +1654,7 @@ export type OpportunityCreateManyCompanyInput = {
closedFlag?: boolean
closedByName?: string | null
closedByCwId?: number | null
productSequence?: Prisma.OpportunityCreateproductSequenceInput | number[]
cwLastUpdated?: Date | string | null
createdAt?: Date | string
updatedAt?: Date | string
@@ -1660,6 +1703,7 @@ export type OpportunityUpdateWithoutCompanyInput = {
closedFlag?: Prisma.BoolFieldUpdateOperationsInput | boolean
closedByName?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
closedByCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
productSequence?: Prisma.OpportunityUpdateproductSequenceInput | number[]
cwLastUpdated?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
@@ -1708,6 +1752,7 @@ export type OpportunityUncheckedUpdateWithoutCompanyInput = {
closedFlag?: Prisma.BoolFieldUpdateOperationsInput | boolean
closedByName?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
closedByCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
productSequence?: Prisma.OpportunityUpdateproductSequenceInput | number[]
cwLastUpdated?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
@@ -1756,6 +1801,7 @@ export type OpportunityUncheckedUpdateManyWithoutCompanyInput = {
closedFlag?: Prisma.BoolFieldUpdateOperationsInput | boolean
closedByName?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
closedByCwId?: Prisma.NullableIntFieldUpdateOperationsInput | number | null
productSequence?: Prisma.OpportunityUpdateproductSequenceInput | number[]
cwLastUpdated?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
@@ -1807,6 +1853,7 @@ export type OpportunitySelect<ExtArgs extends runtime.Types.Extensions.InternalA
closedByName?: boolean
closedByCwId?: boolean
companyId?: boolean
productSequence?: boolean
cwLastUpdated?: boolean
createdAt?: boolean
updatedAt?: boolean
@@ -1857,6 +1904,7 @@ export type OpportunitySelectCreateManyAndReturn<ExtArgs extends runtime.Types.E
closedByName?: boolean
closedByCwId?: boolean
companyId?: boolean
productSequence?: boolean
cwLastUpdated?: boolean
createdAt?: boolean
updatedAt?: boolean
@@ -1907,6 +1955,7 @@ export type OpportunitySelectUpdateManyAndReturn<ExtArgs extends runtime.Types.E
closedByName?: boolean
closedByCwId?: boolean
companyId?: boolean
productSequence?: boolean
cwLastUpdated?: boolean
createdAt?: boolean
updatedAt?: boolean
@@ -1957,12 +2006,13 @@ export type OpportunitySelectScalar = {
closedByName?: boolean
closedByCwId?: boolean
companyId?: boolean
productSequence?: boolean
cwLastUpdated?: boolean
createdAt?: boolean
updatedAt?: boolean
}
export type OpportunityOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "cwOpportunityId" | "name" | "notes" | "typeName" | "typeCwId" | "stageName" | "stageCwId" | "statusName" | "statusCwId" | "priorityName" | "priorityCwId" | "ratingName" | "ratingCwId" | "source" | "campaignName" | "campaignCwId" | "primarySalesRepName" | "primarySalesRepIdentifier" | "primarySalesRepCwId" | "secondarySalesRepName" | "secondarySalesRepIdentifier" | "secondarySalesRepCwId" | "companyCwId" | "companyName" | "contactCwId" | "contactName" | "siteCwId" | "siteName" | "customerPO" | "totalSalesTax" | "locationName" | "locationCwId" | "departmentName" | "departmentCwId" | "expectedCloseDate" | "pipelineChangeDate" | "dateBecameLead" | "closedDate" | "closedFlag" | "closedByName" | "closedByCwId" | "companyId" | "cwLastUpdated" | "createdAt" | "updatedAt", ExtArgs["result"]["opportunity"]>
export type OpportunityOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "cwOpportunityId" | "name" | "notes" | "typeName" | "typeCwId" | "stageName" | "stageCwId" | "statusName" | "statusCwId" | "priorityName" | "priorityCwId" | "ratingName" | "ratingCwId" | "source" | "campaignName" | "campaignCwId" | "primarySalesRepName" | "primarySalesRepIdentifier" | "primarySalesRepCwId" | "secondarySalesRepName" | "secondarySalesRepIdentifier" | "secondarySalesRepCwId" | "companyCwId" | "companyName" | "contactCwId" | "contactName" | "siteCwId" | "siteName" | "customerPO" | "totalSalesTax" | "locationName" | "locationCwId" | "departmentName" | "departmentCwId" | "expectedCloseDate" | "pipelineChangeDate" | "dateBecameLead" | "closedDate" | "closedFlag" | "closedByName" | "closedByCwId" | "companyId" | "productSequence" | "cwLastUpdated" | "createdAt" | "updatedAt", ExtArgs["result"]["opportunity"]>
export type OpportunityInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
company?: boolean | Prisma.Opportunity$companyArgs<ExtArgs>
}
@@ -2022,6 +2072,7 @@ export type $OpportunityPayload<ExtArgs extends runtime.Types.Extensions.Interna
closedByName: string | null
closedByCwId: number | null
companyId: string | null
productSequence: number[]
cwLastUpdated: Date | null
createdAt: Date
updatedAt: Date
@@ -2492,6 +2543,7 @@ export interface OpportunityFieldRefs {
readonly closedByName: Prisma.FieldRef<"Opportunity", 'String'>
readonly closedByCwId: Prisma.FieldRef<"Opportunity", 'Int'>
readonly companyId: Prisma.FieldRef<"Opportunity", 'String'>
readonly productSequence: Prisma.FieldRef<"Opportunity", 'Int[]'>
readonly cwLastUpdated: Prisma.FieldRef<"Opportunity", 'DateTime'>
readonly createdAt: Prisma.FieldRef<"Opportunity", 'DateTime'>
readonly updatedAt: Prisma.FieldRef<"Opportunity", 'DateTime'>