Skip to content

Commit 044e844

Browse files
authored
Merge pull request #305 from GTBitsOfGood/gia/296-populate-wishlist-summary
Tested/Working Wishlist Population
2 parents 1946995 + d2f6aec commit 044e844

5 files changed

Lines changed: 245 additions & 23 deletions

File tree

‎scripts/seed.ts‎

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,116 @@ async function buildSeedData() {
267267
console.log(` - Distribution Lead: ${distributionLead.email}`);
268268
console.log(` - Partner 1: ${partner1.email}`);
269269
console.log(` - Partner 2: ${partner2.email}`);
270+
271+
// ============================================================================
272+
// Test: partner has an existing manual wishlist entry for an item,
273+
// then a distribution only partially fulfills their request for that same item.
274+
// Expected: existing wishlist entry quantity increases by the unfulfilled amount.
275+
//
276+
// Partner 1 manually added "Gauze Pads" to their wishlist (qty: 50) — this
277+
// simulates a wishlist entry they created themselves before any donor offer.
278+
// A donor offer comes in for Gauze Pads, Partner 1 requests 200.
279+
// Distribution only allocates 1 line item (75 units) → unfulfilled = 125.
280+
// Expected final wishlist qty: 50 + 125 = 175
281+
// ============================================================================
282+
283+
// Step 1: Partner 1 manually creates a wishlist entry beforehand
284+
await db.wishlist.create({
285+
data: {
286+
name: "Gauze Pads",
287+
quantity: 50,
288+
priority: "MEDIUM",
289+
comments: "We are always in need of these",
290+
partnerId: partner1.id,
291+
},
292+
});
293+
294+
console.log("✓ Created manual wishlist entry for Partner 1: Gauze Pads qty 50");
295+
296+
// Step 2: Donor offer comes in with Gauze Pads
297+
const gauzeOffer = await db.donorOffer.create({
298+
data: {
299+
offerName: "Gauze Pads Offer",
300+
donorName: "Test Donor",
301+
state: "FINALIZED",
302+
partnerResponseDeadline: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
303+
donorResponseDeadline: new Date(Date.now() + 14 * 24 * 60 * 60 * 1000),
304+
partnerVisibilities: {
305+
connect: [{ id: partner1.id }],
306+
},
307+
},
308+
});
309+
310+
const gauzeItem = await db.generalItem.create({
311+
data: {
312+
donorOfferId: gauzeOffer.id,
313+
title: "Gauze Pads",
314+
unitType: "Box",
315+
initialQuantity: 200,
316+
weight: 0.3,
317+
},
318+
});
319+
320+
// Two line items: 75 units and 125 units
321+
const gauzeLineItem1 = await db.lineItem.create({
322+
data: {
323+
generalItemId: gauzeItem.id,
324+
donorName: "Test Donor",
325+
quantity: 75,
326+
lotNumber: "GLOT-1",
327+
palletNumber: "GPAL-1",
328+
boxNumber: "GBOX-1",
329+
unitPrice: 3.0,
330+
allowAllocations: true,
331+
visible: true,
332+
gik: true,
333+
},
334+
});
335+
336+
await db.lineItem.create({
337+
data: {
338+
generalItemId: gauzeItem.id,
339+
donorName: "Test Donor",
340+
quantity: 125,
341+
lotNumber: "GLOT-2",
342+
palletNumber: "GPAL-1",
343+
boxNumber: "GBOX-2",
344+
unitPrice: 3.0,
345+
allowAllocations: true,
346+
visible: true,
347+
gik: true,
348+
},
349+
});
350+
351+
// Step 3: Partner 1 requests 200 units
352+
await db.generalItemRequest.create({
353+
data: {
354+
generalItemId: gauzeItem.id,
355+
partnerId: partner1.id,
356+
quantity: 200,
357+
priority: "MEDIUM",
358+
finalQuantity: 200,
359+
comments: "Requested from donor offer",
360+
},
361+
});
362+
363+
// Step 4: Distribution created — only allocates the 75 unit line item
364+
const gauzeDist = await db.distribution.create({
365+
data: {
366+
partnerId: partner1.id,
367+
pending: true,
368+
allocations: {
369+
create: [{ lineItemId: gauzeLineItem1.id, partnerId: partner1.id }],
370+
},
371+
},
372+
});
373+
374+
console.log("✓ Created Gauze Pads distribution test");
375+
console.log(` - Distribution ID: ${gauzeDist.id} (pending)`);
376+
console.log(` - Partner 1 wishlist BEFORE approval: Gauze Pads qty 50`);
377+
console.log(` - After approving dist ${gauzeDist.id}:`);
378+
console.log(` unfulfilled = 200 - 75 = 125`);
379+
console.log(` merged wishlist qty = 50 + 125 = 175`);
270380
}
271381

272382
buildSeedData()

‎src/app/api/generalItems/[generalItemId]/requests/[requestId]/route.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { NextRequest, NextResponse } from "next/server";
22
import { z } from "zod";
33

44
import { auth } from "@/auth";
5-
import DonorOfferService from "@/services/donorOfferService";
65
import { GeneralItemRequestService } from "@/services/generalItemRequestService";
76
import UserService from "@/services/userService";
87
import {
@@ -95,10 +94,11 @@ export async function PATCH(
9594
throw new ArgumentError(updateParsed.error.message);
9695
}
9796

98-
await DonorOfferService.updateRequestFinalQuantity(
97+
await GeneralItemRequestService.updateWishlist(
9998
parsed.data.requestId,
10099
updateParsed.data.finalQuantity
101100
);
101+
console.log(updateParsed.data.finalQuantity)
102102

103103
return NextResponse.json(ok());
104104
}

‎src/services/distributionService.ts‎

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -367,9 +367,6 @@ export default class DistributionService {
367367

368368
for (const distribution of distributions) {
369369
for (const allocation of distribution.allocations) {
370-
// const item = await db.item.findUnique({
371-
// where: { id: allocation.itemId },
372-
// });
373370
const item = await db.lineItem.findUnique({
374371
where: { id: allocation.lineItemId },
375372
});
@@ -672,10 +669,7 @@ export default class DistributionService {
672669
data,
673670
});
674671

675-
if (
676-
data.pending === false &&
677-
currentDistribution.pending === true
678-
) {
672+
if (data.pending === false && currentDistribution.pending === true) {
679673
await this.createShipmentsForDistribution(distributionId);
680674
}
681675

@@ -766,4 +760,4 @@ export default class DistributionService {
766760
where: { id: distributionId },
767761
});
768762
}
769-
}
763+
}

‎src/services/generalItemRequestService.ts‎

Lines changed: 64 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
} from "@/types/api/donorOffer.types";
99
import { Filters } from "@/types/api/filter.types";
1010
import { buildQueryWithPagination, buildWhereFromFilters } from "@/util/table";
11+
import { WishlistService } from "./wishlistService";
1112

1213
export class GeneralItemRequestService {
1314
static async getById(id: number) {
@@ -198,6 +199,36 @@ export class GeneralItemRequestService {
198199
}
199200
}
200201

202+
static async updateWishlist(requestId: number, newFinalQuantity: number) {
203+
const existingRequest = await db.generalItemRequest.findUnique({
204+
where: { id: requestId },
205+
select: { finalQuantity: true, id: true }
206+
});
207+
208+
if (!existingRequest) throw new NotFoundError("Request not found");
209+
210+
const oldFinalQuantity = existingRequest.finalQuantity ?? 0;
211+
const unfulfilledDelta = oldFinalQuantity - newFinalQuantity;
212+
213+
return await db.$transaction(async (tx) => {
214+
const updated = await tx.generalItemRequest.update({
215+
where: { id: requestId },
216+
data: { finalQuantity: newFinalQuantity }
217+
});
218+
219+
if (unfulfilledDelta !== 0) {
220+
await WishlistService.createUnfulfilledWishlistEntries([
221+
{
222+
requestId: requestId,
223+
unfulfilledQuantity: unfulfilledDelta,
224+
},
225+
]);
226+
}
227+
228+
return updated;
229+
});
230+
}
231+
201232
static async bulkUpdateRequests(
202233
updates: Array<{ requestId: number; finalQuantity: number }>
203234
) {
@@ -241,14 +272,35 @@ export class GeneralItemRequestService {
241272
throw new ArgumentError("Cannot update requests after the partner response deadline has passed.");
242273
}
243274

244-
const results = await db.$transaction(
245-
updates.map(({ requestId, finalQuantity }) =>
246-
db.generalItemRequest.update({
247-
where: { id: requestId },
248-
data: { finalQuantity },
249-
})
250-
)
251-
);
275+
const wishlistDeltas = updates.map(u => {
276+
const existingRequest = requests.find(r => r.id === u.requestId);
277+
if (!existingRequest) return null;
278+
279+
const delta = (existingRequest.finalQuantity ?? 0) - u.finalQuantity;
280+
281+
return delta !== 0
282+
? { requestId: u.requestId, unfulfilledQuantity: delta }
283+
: null;
284+
}).filter((update): update is { requestId: number; unfulfilledQuantity: number } => update !== null);
285+
286+
const results = await db.$transaction(async (tx) => {
287+
// Execute the request updates
288+
const updateResults = await Promise.all(
289+
updates.map(({ requestId, finalQuantity }) =>
290+
tx.generalItemRequest.update({
291+
where: { id: requestId },
292+
data: { finalQuantity },
293+
})
294+
)
295+
);
296+
297+
// 3. Sync the wishlists using the deltas
298+
if (wishlistDeltas.length > 0) {
299+
await WishlistService.createUnfulfilledWishlistEntries(wishlistDeltas);
300+
}
301+
302+
return updateResults;
303+
});
252304

253305
return results;
254306
}
@@ -311,10 +363,10 @@ export class GeneralItemRequestService {
311363
);
312364
}
313365

314-
const duplicateRequest = await tx.generalItemRequest.findFirst({
366+
const duplicateRequest = await tx.generalItemRequest.findUnique({
315367
where: {
316-
generalItemId: targetGeneralItemId,
317-
partnerId: existingRequest.partnerId,
368+
generalItemId_partnerId: {generalItemId: targetGeneralItemId,
369+
partnerId: existingRequest.partnerId,}
318370
},
319371
});
320372

@@ -537,4 +589,4 @@ export class GeneralItemRequestService {
537589
total,
538590
};
539591
}
540-
}
592+
}

‎src/services/wishlistService.ts‎

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import { Prisma } from "@prisma/client";
1111
import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";
1212
import { MatchingService } from "./matchingService";
1313

14+
const AUTO_GENERATED_COMMENT = "Automatically generated due to unfulfilled request";
15+
1416
export class WishlistService {
1517
static async createWishlist(data: CreateWishlistData) {
1618
const partner = await db.user.findUnique({
@@ -42,6 +44,70 @@ export class WishlistService {
4244
return wishlist;
4345
}
4446

47+
/**
48+
* For each request where finalQuantity < quantity, automatically creates or
49+
* updates a wishlist entry for the partner with the unfulfilled quantity.
50+
* If an entry with the same name already exists for the partner, the
51+
* unfulfilled quantity is added to it.
52+
*/
53+
static async createUnfulfilledWishlistEntries(updates: { requestId: number; unfulfilledQuantity: number }[]) {
54+
const fullRequests = await db.generalItemRequest.findMany({
55+
where: { id: { in: updates.map(u => u.requestId) } },
56+
include: { partner: true, generalItem: true }
57+
});
58+
59+
for (const update of updates) {
60+
const request = fullRequests.find((r) => r.id === update.requestId);
61+
if (!request || !request.partner.enabled || request.partner.pending) continue;
62+
63+
// Use the delta (unfulfilledQuantity) passed from the service
64+
const delta = update.unfulfilledQuantity;
65+
66+
// IMPORTANT: Search by generalItemId to avoid duplicates
67+
const existing = await db.wishlist.findFirst({
68+
where: {
69+
partnerId: request.partnerId,
70+
generalItemId: request.generalItemId,
71+
}
72+
});
73+
74+
if (existing) {
75+
const newQty = (existing.quantity ?? 0) + delta;
76+
if (newQty <= 0) {
77+
await db.wishlist.delete({ where: { id: existing.id } });
78+
} else {
79+
await db.wishlist.update({
80+
where: { id: existing.id },
81+
data: { quantity: newQty },
82+
});
83+
}
84+
} else if (delta > 0) {
85+
const wishlist = await db.wishlist.create({
86+
data: {
87+
name: request.generalItem.title,
88+
quantity: delta,
89+
priority: request.priority ?? "LOW",
90+
comments: AUTO_GENERATED_COMMENT,
91+
partnerId: request.partnerId,
92+
generalItemId: request.generalItemId, // KEY: Link it!
93+
},
94+
});
95+
96+
try {
97+
await MatchingService.add({
98+
wishlistId: wishlist.id,
99+
title: wishlist.name,
100+
});
101+
} catch (error) {
102+
console.warn(
103+
"Failed to generate embedding for auto-created wishlist entry:",
104+
error
105+
);
106+
}
107+
}
108+
}
109+
}
110+
45111
/**
46112
* Automatically updates lastUpdated
47113
*/
@@ -247,4 +313,4 @@ export class WishlistService {
247313
}
248314
}
249315
}
250-
}
316+
}

0 commit comments

Comments
 (0)