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
66 changes: 43 additions & 23 deletions AI_LOG.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,45 @@
# AI Usage Log

Please be honest and detailed about your AI usage. Using AI is perfectly fine and expected, but we want to understand your workflow!

## Tools Used
*(e.g., GitHub Copilot, ChatGPT, Claude, Cursor, etc.)*

-
-

## 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?*

**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?*

**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?*

## General Comments
*(Any other thoughts on how AI helped or hindered you during this assessment?)*
## AI Tool Used
Claude (Anthropic) — used via chat to analyze the repo, locate bugs, and implement fixes.

## Task 1: Pricing Logic Bug
Asked Claude to inspect the pricing computation logic. It found that in
lib/pricing/calculator.ts, when a Pro-tier user already qualified for free
shipping (subtotal over threshold) AND also applied a discount code marked
stackableWithFreeShipping, the code was zeroing shippingCents AND separately
adding STANDARD_SHIPPING_RATE to discountCents — double-counting the shipping
value and driving the total artificially low. Claude removed the redundant
discount addition since shippingCents=0 already reflects free shipping.
Verified with new unit tests in tests/pricing.test.ts covering standard
shipping, Pro + stackable code over/under threshold, and percentage discount
combined with free shipping — all pass.

## Task 2: Dashboard Performance
Asked Claude to find the bottleneck in the /dashboard order history load. It
identified an N+1 query in lib/orders/queries.ts: listOrders() fetched all
orders in one query, then ran a separate orderItem.findMany() per order
inside Promise.all — scaling linearly with order count. Claude replaced this
with a single prisma.order.findMany() call using nested include for items
and product, matching the pattern already used correctly in getOrderById.

## Task 3: Duplicate Order Feature
Asked Claude to design and implement a "Duplicate Order" button. It added:
- POST /api/orders/[id]/duplicate — validates session/ownership of the
order, re-checks current stock/availability for each product using the
live join already provided by getOrderById (avoiding an extra query),
skips inactive or out-of-stock items, clamps quantity to available stock,
and returns items to add plus a list of skipped items with reasons.
- components/dashboard/DuplicateOrderButton.tsx — client component that
calls the endpoint and pushes returned items into the existing Zustand
cart store via addItem(), with inline error/skipped-item messaging.
- Wired the button into the previously empty slot in OrderCard.tsx.

## Remaining / Known Limitations
- Pre-existing lint issues in files unrelated to the three tasks (e.g.
setState-in-effect warnings in CartDrawer.tsx, Header.tsx, and
no-html-link-for-pages in a couple of pages) were left untouched, as
they were out of scope for this assignment.
- Verified locally with `npm run test` and `npm run lint`, and manually
confirmed all three fixes (pricing calculation, single-query dashboard
load, and end-to-end duplicate-order flow) via the dev server.
108 changes: 108 additions & 0 deletions app/api/orders/[id]/duplicate/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { NextRequest, NextResponse } from 'next/server';
import { getSession } from '@/lib/auth/session';
import { getOrderById } from '@/lib/orders/queries';
import type { ApiResponse, Product } from '@/types';

interface DuplicateOrderItem {
product: Product;
quantity: number;
}

interface DuplicateOrderResult {
items: DuplicateOrderItem[];
skipped: { name: string; reason: string }[];
}

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 }
);
}

// Re-fetch current product state — stock/price/availability may have
// changed since the order was originally placed.
const items: DuplicateOrderItem[] = [];
const skipped: { name: string; reason: string }[] = [];

for (const orderItem of order.items) {
const product = orderItem.product;

if (!product || !product.active) {
skipped.push({
name: orderItem.product.name,
reason: 'No longer available',
});
continue;
}

if (product.stock <= 0) {
skipped.push({ name: product.name, reason: 'Out of stock' });
continue;
}

const quantity = Math.min(orderItem.quantity, product.stock);
if (quantity < orderItem.quantity) {
skipped.push({
name: product.name,
reason: `Only ${product.stock} left — added what's available`,
});
}

items.push({
product: {
id: product.id,
name: product.name,
description: product.description,
priceCents: product.priceCents,
stock: product.stock,
category: product.category,
imageUrl: product.imageUrl,
active: product.active,
},
quantity,
});
}

if (items.length === 0) {
return NextResponse.json<ApiResponse<null>>(
{ data: null, error: 'None of the items in this order are still available' },
{ status: 400 }
);
}

return NextResponse.json<ApiResponse<DuplicateOrderResult>>({
data: { items, skipped },
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 }
);
}
}
89 changes: 89 additions & 0 deletions components/dashboard/DuplicateOrderButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
'use client';

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

interface DuplicateOrderButtonProps {
orderId: string;
}

interface DuplicateOrderItem {
product: Product;
quantity: number;
}

interface DuplicateOrderResult {
items: DuplicateOrderItem[];
skipped: { name: string; reason: string }[];
}

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

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

try {
const res = await fetch(`/api/orders/${orderId}/duplicate`, {
method: 'POST',
});

const json: ApiResponse<DuplicateOrderResult> = await res.json();

if (!res.ok || json.error || !json.data) {
setError(json.error ?? 'Could not duplicate this order');
return;
}

json.data.items.forEach(({ product, quantity }) => {
addItem(product, quantity);
});

if (json.data.skipped.length > 0) {
setNotice(
json.data.skipped.map((s) => `${s.name} (${s.reason})`).join(', ')
);
}

setAdded(true);
setTimeout(() => setAdded(false), 2000);
} catch {
setError('Could not duplicate this order. Check your connection.');
} finally {
setLoading(false);
}
};

return (
<div className="space-y-1.5">
<Button
id={`duplicate-order-${orderId}`}
variant={added ? 'secondary' : 'ghost'}
size="sm"
className="w-full"
loading={loading}
onClick={handleDuplicate}
>
{added ? '✓ Added to cart' : 'Duplicate Order'}
</Button>
{error && (
<p className="text-xs text-red-400" id={`duplicate-order-error-${orderId}`}>
{error}
</p>
)}
{notice && (
<p className="text-xs text-lunar-500" id={`duplicate-order-notice-${orderId}`}>
Skipped: {notice}
</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>
);
}
20 changes: 6 additions & 14 deletions lib/orders/queries.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,15 @@
import { prisma } from '@/lib/db/client';

export async function listOrders(userId: string) {

const orders = await prisma.order.findMany({
return prisma.order.findMany({
where: { userId },
orderBy: { createdAt: 'desc' },
});

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

return ordersWithItems;
},
},
});
}

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

if (userTier === PRO_TIER && discountCode?.stackableWithFreeShipping) {
discountCents += STANDARD_SHIPPING_RATE;
}

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

return {
Expand Down
4 changes: 2 additions & 2 deletions next-env.d.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
import "./.next/types/root-params.d.ts";
import "./.next/dev/types/routes.d.ts";
import "./.next/dev/types/root-params.d.ts";

// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
Loading