@@ -6,9 +6,43 @@ type NormalizedItem = {
66 id : number ;
77 lineItems : { lineItemId : number ; quantity : number } [ ] ;
88 requests : { partnerId : number ; quantity : number } [ ] ;
9+ wishlistMatches : Map < number , Map < number , boolean > > ;
910} ;
1011
1112export class AllocationSuggestionService {
13+ private static async buildWishlistMatches (
14+ generalItemTitle : string ,
15+ lineItemIds : number [ ] ,
16+ partnerIds : number [ ]
17+ ) : Promise < Map < number , Map < number , boolean > > > {
18+ const wishlistMatches = new Map < number , Map < number , boolean > > ( ) ;
19+
20+ if ( ! partnerIds . length ) return wishlistMatches ;
21+
22+ const wishlists = await db . wishlist . findMany ( {
23+ where : {
24+ partnerId : { in : partnerIds } ,
25+ generalItemId : null , // only unfulfilled/unlinked wishlists
26+ name : generalItemTitle ,
27+ } ,
28+ select : {
29+ partnerId : true ,
30+ } ,
31+ } ) ;
32+
33+ const partnersWithWishlist = new Set ( wishlists . map ( ( w ) => w . partnerId ) ) ;
34+
35+ for ( const lineItemId of lineItemIds ) {
36+ const partnerMatches = new Map < number , boolean > ( ) ;
37+ for ( const partnerId of partnerIds ) {
38+ partnerMatches . set ( partnerId , partnersWithWishlist . has ( partnerId ) ) ;
39+ }
40+ wishlistMatches . set ( lineItemId , partnerMatches ) ;
41+ }
42+
43+ return wishlistMatches ;
44+ }
45+
1246 static async suggestForDonorOffer (
1347 donorOfferId : number
1448 ) : Promise < AllocationSuggestionProgram [ ] > {
@@ -90,32 +124,41 @@ export class AllocationSuggestionService {
90124 lineItemsByGeneralItemId . set ( lineItem . generalItemId , list ) ;
91125 }
92126
93- return itemsWithRequests . reduce < NormalizedItem [ ] > ( ( acc , item ) => {
94- const lineItemsForItem =
95- lineItemsByGeneralItemId . get ( item . id ) ?? [ ] ;
96- if ( ! lineItemsForItem . length ) {
97- return acc ;
98- }
99-
100- const requests = ( item . requests ?? [ ] )
101- . filter ( ( request ) => request . quantity > 0 )
102- . map ( ( request ) => ( {
103- partnerId : request . partnerId ,
104- quantity : request . quantity ,
105- } ) ) ;
106-
107- if ( ! requests . length ) {
108- return acc ;
109- }
110-
111- acc . push ( {
112- id : item . id ,
113- lineItems : lineItemsForItem ,
114- requests,
115- } ) ;
116-
117- return acc ;
118- } , [ ] ) ;
127+ return Promise . all (
128+ itemsWithRequests . map ( async ( item ) => {
129+ const lineItemsForItem =
130+ lineItemsByGeneralItemId . get ( item . id ) ?? [ ] ;
131+ if ( ! lineItemsForItem . length ) {
132+ return null ;
133+ }
134+
135+ const requests = ( item . requests ?? [ ] )
136+ . filter ( ( request ) => request . quantity > 0 )
137+ . map ( ( request ) => ( {
138+ partnerId : request . partnerId ,
139+ quantity : request . quantity ,
140+ } ) ) ;
141+
142+ if ( ! requests . length ) {
143+ return null ;
144+ }
145+
146+ const lineItemIds = lineItemsForItem . map ( ( li ) => li . lineItemId ) ;
147+ const partnerIds = requests . map ( ( r ) => r . partnerId ) ;
148+ const wishlistMatches = await this . buildWishlistMatches (
149+ item . title ,
150+ lineItemIds ,
151+ partnerIds
152+ ) ;
153+
154+ return {
155+ id : item . id ,
156+ lineItems : lineItemsForItem ,
157+ requests,
158+ wishlistMatches,
159+ } ;
160+ } )
161+ ) . then ( ( items ) => items . filter ( ( item ) => item !== null ) as NormalizedItem [ ] ) ;
119162 }
120163
121164 private static async fetchItemsForGeneralItems (
@@ -146,43 +189,57 @@ export class AllocationSuggestionService {
146189 } ,
147190 } ) ;
148191
149- return generalItems . reduce < NormalizedItem [ ] > ( ( acc , item ) => {
150- if ( ! item . items . length ) {
151- return acc ;
152- }
192+ return Promise . all (
193+ generalItems . map ( async ( item ) => {
194+ if ( ! item . items . length ) {
195+ return null ;
196+ }
153197
154- const requests = item . requests
155- . filter ( ( request ) => request . quantity > 0 )
156- . map ( ( request ) => ( {
157- partnerId : request . partnerId ,
158- quantity : request . quantity ,
159- } ) ) ;
198+ const requests = item . requests
199+ . filter ( ( request ) => request . quantity > 0 )
200+ . map ( ( request ) => ( {
201+ partnerId : request . partnerId ,
202+ quantity : request . quantity ,
203+ } ) ) ;
160204
161- if ( ! requests . length ) {
162- return acc ;
163- }
205+ if ( ! requests . length ) {
206+ return null ;
207+ }
164208
165- acc . push ( {
166- id : item . id ,
167- lineItems : item . items . map ( ( lineItem ) => ( {
209+ const lineItems = item . items . map ( ( lineItem ) => ( {
168210 lineItemId : lineItem . id ,
169211 quantity : lineItem . quantity ,
170- } ) ) ,
171- requests,
172- } ) ;
212+ } ) ) ;
173213
174- return acc ;
175- } , [ ] ) ;
214+ const lineItemIds = lineItems . map ( ( li ) => li . lineItemId ) ;
215+ const partnerIds = requests . map ( ( r ) => r . partnerId ) ;
216+ const wishlistMatches = await this . buildWishlistMatches (
217+ item . title ,
218+ lineItemIds ,
219+ partnerIds
220+ ) ;
221+
222+ return {
223+ id : item . id ,
224+ lineItems,
225+ requests,
226+ wishlistMatches,
227+ } ;
228+ } )
229+ ) . then ( ( items ) => items . filter ( ( item ) => item !== null ) as NormalizedItem [ ] ) ;
176230 }
177231
178232 private static buildLinearProgram (
179233 lineItems : { lineItemId : number ; quantity : number } [ ] ,
180- targets : { partnerId : number ; target : number } [ ]
234+ targets : { partnerId : number ; target : number } [ ] ,
235+ wishlistMatches : Map < number , Map < number , boolean > >
181236 ) : string {
182237 const itemCount = lineItems . length ;
183238 const partnerCount = targets . length ;
239+ const WISHLIST_WEIGHT = 0.001 ;
184240
185241 const variableX = ( iIdx : number , pIdx : number ) => `x_i${ iIdx } _p${ pIdx } ` ;
242+ const variableW = ( iIdx : number , pIdx : number ) => `w_i${ iIdx } _p${ pIdx } ` ;
186243 const variableDevPos = ( pIdx : number ) => `dpos_p${ pIdx } ` ;
187244 const variableDevNeg = ( pIdx : number ) => `dneg_p${ pIdx } ` ;
188245
@@ -195,7 +252,23 @@ export class AllocationSuggestionService {
195252 objectiveTerms . push ( variableDevPos ( p ) ) ;
196253 objectiveTerms . push ( variableDevNeg ( p ) ) ;
197254 }
198- lines . push ( " " + objectiveTerms . join ( " + " ) ) ;
255+
256+ // wishlist incentive terms (negative, so subtract from objective to incentivize)
257+ const negativeTerms : string [ ] = [ ] ;
258+ for ( let i = 0 ; i < itemCount ; i ++ ) {
259+ const lineItemId = lineItems [ i ] . lineItemId ;
260+ const matches = wishlistMatches . get ( lineItemId ) ;
261+ for ( let p = 0 ; p < partnerCount ; p ++ ) {
262+ const targetPartnerId = targets [ p ] . partnerId ;
263+ if ( matches ?. get ( targetPartnerId ) ) {
264+ negativeTerms . push ( `- ${ WISHLIST_WEIGHT } ${ variableW ( i , p ) } ` ) ;
265+ }
266+ }
267+ }
268+
269+ const allTerms = objectiveTerms . join ( " + " ) ;
270+ const finalObjective = allTerms + ( negativeTerms . length > 0 ? " " + negativeTerms . join ( " " ) : "" ) ;
271+ lines . push ( " " + finalObjective ) ;
199272
200273 lines . push ( "Subject To" ) ;
201274 for ( let itemIdx = 0 ; itemIdx < itemCount ; itemIdx ++ ) {
@@ -218,6 +291,19 @@ export class AllocationSuggestionService {
218291 lines . push ( ` balance_p${ partnerIdx } : ` + terms . join ( " " ) + ` = ${ t . target } ` ) ;
219292 }
220293
294+ // Link wishlist indicator variables to allocation variables
295+ // w_i_p can only be 1 if x_i_p is 1
296+ for ( let i = 0 ; i < itemCount ; i ++ ) {
297+ const lineItemId = lineItems [ i ] . lineItemId ;
298+ const matches = wishlistMatches . get ( lineItemId ) ;
299+ for ( let p = 0 ; p < partnerCount ; p ++ ) {
300+ const targetPartnerId = targets [ p ] . partnerId ;
301+ if ( matches ?. get ( targetPartnerId ) ) {
302+ lines . push ( ` wishlist_link_i${ i } _p${ p } : ${ variableW ( i , p ) } - ${ variableX ( i , p ) } <= 0` ) ;
303+ }
304+ }
305+ }
306+
221307 lines . push ( "Bounds" ) ;
222308 for ( let partnerIdx = 0 ; partnerIdx < partnerCount ; partnerIdx ++ ) {
223309 lines . push ( ` 0 <= ${ variableDevPos ( partnerIdx ) } ` ) ;
@@ -229,6 +315,14 @@ export class AllocationSuggestionService {
229315 for ( let itemIdx = 0 ; itemIdx < itemCount ; itemIdx ++ ) {
230316 for ( let partnerIdx = 0 ; partnerIdx < partnerCount ; partnerIdx ++ ) {
231317 binaries . push ( variableX ( itemIdx , partnerIdx ) ) ;
318+
319+ // wishlist indicator variables for wishlist matches
320+ const lineItemId = lineItems [ itemIdx ] . lineItemId ;
321+ const matches = wishlistMatches . get ( lineItemId ) ;
322+ const targetPartnerId = targets [ partnerIdx ] . partnerId ;
323+ if ( matches ?. get ( targetPartnerId ) ) {
324+ binaries . push ( variableW ( itemIdx , partnerIdx ) ) ;
325+ }
232326 }
233327 }
234328 lines . push ( " " + binaries . join ( "\n " ) ) ;
@@ -276,7 +370,7 @@ export class AllocationSuggestionService {
276370 return null ;
277371 }
278372
279- const lp = this . buildLinearProgram ( usableLineItems , targets ) ;
373+ const lp = this . buildLinearProgram ( usableLineItems , targets , item . wishlistMatches ) ;
280374
281375 return {
282376 itemId : item . id ,
0 commit comments