Skip to content

Commit 72a709c

Browse files
authored
Merge pull request #317 from GTBitsOfGood/gia/306-wishlist-priority-suggest-allocations
Gia/306 wishlist priority suggest allocations
2 parents 044e844 + 4f23876 commit 72a709c

2 files changed

Lines changed: 145 additions & 51 deletions

File tree

‎src/components/Tutorial.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2-
import Joyride, {
2+
import Joyride, {
33
Step,
44
CallBackProps,
55
STATUS,

‎src/services/allocationSuggestionService.ts‎

Lines changed: 144 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -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

1112
export 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

Comments
 (0)