Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions backend/src/models/cart-item.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,10 @@ const cartItemSchema = new Schema(
},
)

cartItemSchema.index(
{ user_id: 1, food_id: 1, status: 1 },
{ unique: true, partialFilterExpression: { status: "active" } },
)

export type CartItemDocument = InferSchemaType<typeof cartItemSchema>
export const CartItemModel = model<CartItemDocument>("CartItem", cartItemSchema)
2 changes: 2 additions & 0 deletions backend/src/models/user-swipe.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,7 @@ const userSwipeSchema = new Schema(
},
)

userSwipeSchema.index({ user_id: 1, food_id: 1 }, { unique: true })

export type UserSwipeDocument = InferSchemaType<typeof userSwipeSchema>
export const UserSwipeModel = model<UserSwipeDocument>("UserSwipe", userSwipeSchema)
30 changes: 18 additions & 12 deletions backend/src/routes/swipe.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,20 +51,26 @@ swipeRouter.post("/", validateRequest({ body: swipeSchema }), async (req, res, n
const userId = new Types.ObjectId(userIdRaw)
const foodId = new Types.ObjectId(body.foodId)

const swipe = await UserSwipeModel.create({
user_id: userId,
food_id: foodId,
action: body.action,
timestamp: new Date(),
})
const swipe = await UserSwipeModel.findOneAndUpdate(
{ user_id: userId, food_id: foodId },
{ action: body.action, timestamp: new Date() },
{
upsert: true,
new: true,
setDefaultsOnInsert: true,
},
)

if (body.action === "like") {
await CartItemModel.create({
user_id: userId,
food_id: foodId,
quantity: 1,
status: "active",
})
await CartItemModel.findOneAndUpdate(
{ user_id: userId, food_id: foodId, status: "active" },
{ $setOnInsert: { quantity: 1, status: "active" } },
{
upsert: true,
new: true,
setDefaultsOnInsert: true,
},
)
}

res.status(201).json({
Expand Down
56 changes: 30 additions & 26 deletions backend/test/swipe.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,23 @@ test("POST /api/swipe with action=pass stores swipe and does not create cart ite
const app = createApp()

const originalFindById = FoodItemModel.findById
const originalSwipeCreate = UserSwipeModel.create
const originalCartCreate = CartItemModel.create
const originalSwipeUpsert = UserSwipeModel.findOneAndUpdate
const originalCartUpsert = CartItemModel.findOneAndUpdate

let cartCreateCalls = 0
let cartUpsertCalls = 0

;(FoodItemModel.findById as unknown as (...args: unknown[]) => Promise<unknown>) = async () => ({
_id: "660000000000000000000100",
})
;(UserSwipeModel.create as unknown as (...args: unknown[]) => Promise<unknown>) = async () => ({
_id: "770000000000000000000001",
})
;(CartItemModel.create as unknown as (...args: unknown[]) => Promise<unknown>) = async () => {
cartCreateCalls += 1
return {}
}
;(UserSwipeModel.findOneAndUpdate as unknown as (...args: unknown[]) => Promise<unknown>) =
async () => ({
_id: "770000000000000000000001",
})
;(CartItemModel.findOneAndUpdate as unknown as (...args: unknown[]) => Promise<unknown>) =
async () => {
cartUpsertCalls += 1
return {}
}

const response = await request(app)
.post("/api/swipe")
Expand All @@ -31,32 +33,34 @@ test("POST /api/swipe with action=pass stores swipe and does not create cart ite

assert.equal(response.status, 201)
assert.equal(response.body.swipe.action, "pass")
assert.equal(cartCreateCalls, 0)
assert.equal(cartUpsertCalls, 0)

FoodItemModel.findById = originalFindById
UserSwipeModel.create = originalSwipeCreate
CartItemModel.create = originalCartCreate
UserSwipeModel.findOneAndUpdate = originalSwipeUpsert
CartItemModel.findOneAndUpdate = originalCartUpsert
})

test("POST /api/swipe with action=like stores swipe and creates cart item", async () => {
const app = createApp()

const originalFindById = FoodItemModel.findById
const originalSwipeCreate = UserSwipeModel.create
const originalCartCreate = CartItemModel.create
const originalSwipeUpsert = UserSwipeModel.findOneAndUpdate
const originalCartUpsert = CartItemModel.findOneAndUpdate

let cartCreateCalls = 0
let cartUpsertCalls = 0

;(FoodItemModel.findById as unknown as (...args: unknown[]) => Promise<unknown>) = async () => ({
_id: "660000000000000000000100",
})
;(UserSwipeModel.create as unknown as (...args: unknown[]) => Promise<unknown>) = async () => ({
_id: "770000000000000000000001",
})
;(CartItemModel.create as unknown as (...args: unknown[]) => Promise<unknown>) = async () => {
cartCreateCalls += 1
return {}
}
;(UserSwipeModel.findOneAndUpdate as unknown as (...args: unknown[]) => Promise<unknown>) =
async () => ({
_id: "770000000000000000000001",
})
;(CartItemModel.findOneAndUpdate as unknown as (...args: unknown[]) => Promise<unknown>) =
async () => {
cartUpsertCalls += 1
return {}
}

const response = await request(app)
.post("/api/swipe")
Expand All @@ -65,11 +69,11 @@ test("POST /api/swipe with action=like stores swipe and creates cart item", asyn

assert.equal(response.status, 201)
assert.equal(response.body.swipe.action, "like")
assert.equal(cartCreateCalls, 1)
assert.equal(cartUpsertCalls, 1)

FoodItemModel.findById = originalFindById
UserSwipeModel.create = originalSwipeCreate
CartItemModel.create = originalCartCreate
UserSwipeModel.findOneAndUpdate = originalSwipeUpsert
CartItemModel.findOneAndUpdate = originalCartUpsert
})

test("POST /api/swipe returns 404 when foodId does not exist", async () => {
Expand Down
1 change: 1 addition & 0 deletions mobile/.env.example
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
EXPO_PUBLIC_API_BASE_URL=http://localhost:5000
EXPO_PUBLIC_DEMO_USER_ID=660000000000000000000001
4 changes: 4 additions & 0 deletions mobile/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ npm run start

- Deep link scheme is `discoverly`.
- EAS profiles are defined in `eas.json`.
- Discovery tab pulls from `GET /api/foods/discover`.
- Swipe actions post to `POST /api/swipe`.
- Feed prefetch starts when only 3 cards remain.
- `expo-image` is used for image caching/prefetch to reduce flicker.
- Do not hardcode secrets in source files. Keep sensitive values in EAS secrets
or CI environment variables.
- Only `EXPO_PUBLIC_*` variables should be exposed to client runtime code.
Loading
Loading