Skip to content
Open
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
23 changes: 16 additions & 7 deletions AI_LOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,30 @@ Please be honest and detailed about your AI usage. Using AI is perfectly fine an
## Tools Used
*(e.g., GitHub Copilot, ChatGPT, Claude, Cursor, etc.)*

-
-
- OpenAI Codex (GPT-5), used as a coding assistant in the repository workspace.
- Codex terminal tools: `rg`/`sed` for code inspection, `apply_patch` for edits, and npm/TypeScript commands for verification.

## How did you use AI for this assessment?

**Task 1: Pricing Logic Bug**
- *Did you use AI to find the bug? If so, what prompt did you use?*
- *Did you use AI to write the fix?*
- I asked Codex to trace the cart/checkout pricing path, fix the free-shipping discount calculation, and run relevant checks.
- Codex located the issue in `lib/pricing/calculator.ts`: when a Pro-tier free-shipping benefit and a free-shipping discount code were both present, the code set shipping to zero and then incorrectly added the standard shipping amount to `discountCents`.
- Codex removed that extra shipping-rate addition and added Vitest regression tests for stacked free-shipping benefits, code-only free shipping, and ordinary paid shipping.
- Verification performed: `npm test` passed (4 tests) and `npx tsc --noEmit` passed. The repository-wide lint command has unrelated existing UI errors; the production build could not fetch its Google Fonts in this environment.

**Task 2: Dashboard Performance**
- *How did you use AI here? Did you use it to identify the N+1 issue, or just to write the optimized query?*
- I asked Codex to inspect the dashboard order-loading path and optimize the data access for large order histories.
- Codex identified an N+1 query pattern in `listOrders`: it fetched all of a user's orders, then ran a separate item/product query for every order.
- Codex replaced this with one Prisma relation query, scoped the dashboard history to the most recent 30 days, selected only fields rendered by the dashboard, and added a composite `(userId, createdAt)` index to support the filtered history query.
- Verification performed: `npm test` passed (4 tests) and `npx tsc --noEmit` passed.

**Task 3: Duplicate Order Feature**
- *Did you use AI to write the API route, the client component, or both?*
- *Did AI make any mistakes you had to fix manually?*
- I asked Codex to implement the duplicate-order feature end to end, including a protected API endpoint and the dashboard/cart interaction.
- Codex examined the existing session helper, order-detail lookup, API response envelope, and Zustand cart store before implementing the feature.
- Codex added `POST /api/orders/[id]/duplicate`, which verifies authentication and order ownership, returns eligible active products with enough current stock, and reports skipped unavailable items. Codex also added a client-side dashboard button that calls this endpoint and adds the returned items to the cart.
- Verification performed: `npm test` passed (4 tests) and `npx tsc --noEmit` passed.

## General Comments
*(Any other thoughts on how AI helped or hindered you during this assessment?)*

Codex accelerated repository navigation, implementation, and focused test creation. I reviewed the identified pricing behavior and kept the change limited to Task 1. No external submission website actions were performed.
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,24 @@ The /dashboard page is loading extremely slowly for users who have a large order
- Identify the performance bottleneck when loading the order history.
- Optimize the data fetching so the dashboard loads quickly regardless of how many orders the user has.

#### Implemented fix

- Replaced the N+1 order-history lookup with a single Prisma query that loads each order's items and products together.
- Limited dashboard history to orders placed in the last 30 days.
- Selected only the fields rendered in the dashboard and added a composite `Order(userId, createdAt)` index for the filtered, newest-first query.
- Verified with `npm test` and `npx tsc --noEmit`.

### Task 3: "Duplicate Order" Feature
Add a new "Duplicate Order" button to the past orders displayed on the dashboard.
- Clicking the button should add the same items from the past order directly into the user's cart (provided they are still in stock).
- You will need to build the API endpoint and the client-side logic to update the cart store.

#### Implemented feature

- Added the authenticated `POST /api/orders/[id]/duplicate` endpoint, including 401, 403, and 404 responses where appropriate.
- The endpoint returns only active products with sufficient current stock, and reports how many unavailable items were skipped.
- Added a **Duplicate Order** button to each dashboard order. It adds eligible current products and their original quantities to the persisted Zustand cart, and gives clear loading, success, partial-stock, and failure feedback.

Good luck!

## Submission
Expand Down
59 changes: 59 additions & 0 deletions app/api/orders/[id]/duplicate/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { NextRequest, NextResponse } from 'next/server';
import { getSession } from '@/lib/auth/session';
import { getOrderById } from '@/lib/orders/queries';
import type { ApiResponse, DuplicateOrderResult, Product } from '@/types';

export async function POST(
_request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const session = await getSession();
if (!session) {
return NextResponse.json<ApiResponse<null>>(
{ data: null, error: 'Unauthorized' },
{ status: 401 }
);
}

const { id } = await params;
const order = await getOrderById(id);

if (!order) {
return NextResponse.json<ApiResponse<null>>(
{ data: null, error: 'Order not found' },
{ status: 404 }
);
}

if (order.userId !== session.id) {
return NextResponse.json<ApiResponse<null>>(
{ data: null, error: 'Forbidden' },
{ status: 403 }
);
}

const items = order.items
.filter((item) => item.product.active && item.product.stock >= item.quantity)
.map((item) => ({
product: item.product as Product,
quantity: item.quantity,
}));

const result: DuplicateOrderResult = {
items,
skippedItemCount: order.items.length - items.length,
};

return NextResponse.json<ApiResponse<DuplicateOrderResult>>({
data: result,
error: null,
});
} catch (err) {
console.error('[POST /api/orders/[id]/duplicate]', err);
return NextResponse.json<ApiResponse<null>>(
{ data: null, error: 'Failed to duplicate order' },
{ status: 500 }
);
}
}
8 changes: 1 addition & 7 deletions app/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,9 @@ export default async function DashboardPage() {
const serialized = orders.map((o) => ({
...o,
createdAt: o.createdAt.toISOString(),
updatedAt: o.updatedAt.toISOString(),
items: o.items.map((item) => ({
...item,
product: {
...item.product,
createdAt: item.product.createdAt.toISOString(),
updatedAt: item.product.updatedAt.toISOString(),
},
product: item.product,
})),
})) as (Order & { items: (OrderItem & { product: Product })[] })[];

Expand Down Expand Up @@ -59,4 +54,3 @@ export default async function DashboardPage() {
</div>
);
}

68 changes: 68 additions & 0 deletions components/dashboard/DuplicateOrderButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
'use client';

import { useState } from 'react';
import { Button } from '@/components/ui/Button';
import { useCartStore } from '@/store/cart';
import type { ApiResponse, DuplicateOrderResult } from '@/types';

interface DuplicateOrderButtonProps {
orderId: string;
}

export function DuplicateOrderButton({ orderId }: DuplicateOrderButtonProps) {
const { addItem } = useCartStore();
const [loading, setLoading] = useState(false);
const [message, setMessage] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);

const handleDuplicate = async () => {
setLoading(true);
setMessage(null);
setError(null);

try {
const response = await fetch(`/api/orders/${orderId}/duplicate`, {
method: 'POST',
});
const result = (await response.json()) as ApiResponse<DuplicateOrderResult>;

if (!response.ok || !result.data) {
setError(result.error ?? 'Could not add this order to your cart.');
return;
}

result.data.items.forEach((item) => addItem(item.product, item.quantity));

if (result.data.items.length === 0) {
setMessage('No items from this order are currently in stock.');
} else if (result.data.skippedItemCount > 0) {
setMessage(
`${result.data.items.length} item${result.data.items.length === 1 ? '' : 's'} added; ${result.data.skippedItemCount} unavailable item${result.data.skippedItemCount === 1 ? ' was' : 's were'} skipped.`
);
} else {
setMessage('Order items added to your cart.');
}
} catch {
setError('Could not add this order to your cart. Check your connection.');
} finally {
setLoading(false);
}
};

return (
<div className="space-y-2">
<Button
id={`duplicate-order-${orderId}`}
type="button"
variant="secondary"
size="sm"
loading={loading}
onClick={handleDuplicate}
>
Duplicate Order
</Button>
{message && <p className="text-xs text-emerald-400" role="status">{message}</p>}
{error && <p className="text-xs text-red-400" role="alert">{error}</p>}
</div>
);
}
3 changes: 2 additions & 1 deletion components/dashboard/OrderCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { formatCents, formatDate, relativeDate, shortOrderId } from '@/lib/utils
import { Badge } from '@/components/ui/Badge';
import { Card } from '@/components/ui/Card';
import { OrderItemRow } from './OrderItemRow';
import { DuplicateOrderButton } from './DuplicateOrderButton';
import type { Order, OrderStatus } from '@/types';

interface OrderCardProps {
Expand Down Expand Up @@ -67,7 +68,7 @@ export function OrderCard({ order }: OrderCardProps) {
</div>
</div>

{}
<DuplicateOrderButton orderId={order.id} />
</Card>
);
}
56 changes: 41 additions & 15 deletions lib/orders/queries.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,49 @@
import { prisma } from '@/lib/db/client';

const DASHBOARD_ORDER_HISTORY_DAYS = 30;

export async function listOrders(userId: string) {

const orders = await prisma.order.findMany({
where: { userId },
const orderHistoryStart = new Date();
orderHistoryStart.setDate(orderHistoryStart.getDate() - DASHBOARD_ORDER_HISTORY_DAYS);

// Load the order history and its displayed relations together. The previous
// implementation loaded every order and then issued one query per order for
// its items, which becomes prohibitively slow for customers with many orders.
return prisma.order.findMany({
where: {
userId,
createdAt: { gte: orderHistoryStart },
},
orderBy: { createdAt: 'desc' },
select: {
id: true,
userId: true,
status: true,
subtotalCents: true,
discountCents: true,
shippingCents: true,
totalCents: true,
discountCodeId: true,
createdAt: true,
items: {
select: {
id: true,
productId: true,
quantity: true,
priceAtPurchase: true,
product: {
select: {
id: true,
name: true,
priceCents: true,
category: true,
imageUrl: true,
},
},
},
},
},
});

const ordersWithItems = await Promise.all(
orders.map(async (order) => {
const items = await prisma.orderItem.findMany({
where: { orderId: order.id },
include: { product: true },
});
return { ...order, items };
})
);

return ordersWithItems;
}

export async function getOrderById(orderId: string) {
Expand Down
6 changes: 3 additions & 3 deletions lib/pricing/calculator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@ export function computeOrderTotals(params: ComputeParams): PricingResult {
shippingCents = 0;
}

if (userTier === PRO_TIER && discountCode?.stackableWithFreeShipping) {
discountCents += STANDARD_SHIPPING_RATE;
}
// Free-shipping benefits are not monetary discounts. Multiple benefits may
// make shipping free, but they must never be applied again to the product
// discount total.

const totalCents = Math.max(0, subtotalCents - discountCents + shippingCents);

Expand Down
3 changes: 1 addition & 2 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,7 @@ model Order {
discountCode DiscountCode? @relation(fields: [discountCodeId], references: [id])
items OrderItem[]

@@index([userId])
@@index([createdAt])
@@index([userId, createdAt])
}

model OrderItem {
Expand Down
50 changes: 50 additions & 0 deletions tests/pricing-calculator.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { describe, expect, it } from 'vitest';

import { computeOrderTotals } from '@/lib/pricing/calculator';
import { STANDARD_SHIPPING_RATE } from '@/lib/pricing/types';

const proShippingCode = {
discountType: 'PERCENTAGE' as const,
value: 1500,
stackableWithFreeShipping: true,
};

describe('computeOrderTotals', () => {
it('does not double-count shipping when Pro free shipping and a free-shipping code stack', () => {
const pricing = computeOrderTotals({
subtotalCents: 600_000,
discountCode: proShippingCode,
userTier: 'pro',
});

expect(pricing).toEqual({
subtotalCents: 600_000,
discountCents: 90_000,
shippingCents: 0,
totalCents: 510_000,
});
});

it('keeps the product discount unchanged when only the code grants free shipping', () => {
const pricing = computeOrderTotals({
subtotalCents: 300_000,
discountCode: proShippingCode,
userTier: 'pro',
});

expect(pricing.discountCents).toBe(45_000);
expect(pricing.shippingCents).toBe(0);
expect(pricing.totalCents).toBe(255_000);
});

it('still charges standard shipping when no free-shipping benefit applies', () => {
const pricing = computeOrderTotals({
subtotalCents: 300_000,
discountCode: null,
userTier: 'standard',
});

expect(pricing.shippingCents).toBe(STANDARD_SHIPPING_RATE);
expect(pricing.totalCents).toBe(300_000 + STANDARD_SHIPPING_RATE);
});
});
5 changes: 5 additions & 0 deletions types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ export interface Order {
items?: OrderItem[];
}

export interface DuplicateOrderResult {
items: CartItem[];
skippedItemCount: number;
}

// ---------------------------------------------------------------------------
// Cart (client-side only, managed by Zustand)
// ---------------------------------------------------------------------------
Expand Down