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
39 changes: 29 additions & 10 deletions AI_LOG.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,44 @@
# AI Usage Log
# 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.)*

-
-
- Codex (GPT-5.6 Sol, high reasoning)

## How did you use AI for this assessment?

I had already gone through the challenge requirements and understood the three problem areas before using Codex for the implementation work. I mainly used Codex for repository exploration, implementation assistance, and extensive testing and verification.

**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 used Codex to explore the relevant pricing code and help implement the fix in `computeOrderTotals`.
- The implementation keeps free shipping represented by `shippingCents = 0` without applying the same shipping benefit again through `discountCents`.
- Codex was used heavily for regression testing. Tests were added for the seeded `PROSHIP15` scenario and related combinations involving Pro/standard users, the free-shipping threshold, and normal paid shipping.

**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 used Codex to explore the dashboard/order query flow and help implement the optimized Prisma query.
- The final implementation loads orders with their items/products without per-order item queries, applies the required 30-day order-history window, preserves newest-first ordering, and adds a compound `(userId, createdAt)` index.
- Codex helped create and run a focused test that verifies the query shape, history cutoff, relation loading, ordering, and that the N+1 pattern is removed.

**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 used Codex to help implement both the API route and the client-side dashboard action.
- The API follows the existing authentication and ownership behavior, validates current product availability and stock, and preserves the existing `{ data, error }` response format.
- The client integrates with the existing Zustand cart and merges duplicate-order items into the current cart instead of replacing it.
- Codex was also used to create tests for authentication, ownership checks, successful duplication, unavailable products, and stock handling.

## Testing and Verification

Testing was the main area where I relied on Codex. It helped write the regression tests, run the project checks, inspect failures, and verify the final implementation after the changes were committed.

The final verification included:

- `npm test` — 9/9 tests passed
- `npx tsc --noEmit` — passed
- focused ESLint checks on the changed source/test files — passed
- `npx prisma validate` — passed
- `npm run build` — production build passed
- `git diff --check` — passed

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

Codex was most useful for quickly navigating the repository, assisting with implementation, and giving the changes a much more thorough testing pass. I reviewed the resulting changes and test output before keeping them.
105 changes: 105 additions & 0 deletions app/api/orders/[id]/duplicate/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
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';

function toClientProduct(product: {
id: string;
name: string;
description: string;
priceCents: number;
stock: number;
category: string;
imageUrl: string | null;
active: boolean;
}): Product {
return {
id: product.id,
name: product.name,
description: product.description,
priceCents: product.priceCents,
stock: product.stock,
category: product.category,
imageUrl: product.imageUrl,
active: product.active,
};
}

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 result: DuplicateOrderResult = {
items: [],
skippedItems: [],
};

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

if (!product.active) {
result.skippedItems.push({
productId: product.id,
name: product.name,
reason: 'inactive',
requestedQuantity: item.quantity,
availableQuantity: product.stock,
});
continue;
}

if (product.stock < item.quantity) {
result.skippedItems.push({
productId: product.id,
name: product.name,
reason: 'insufficient_stock',
requestedQuantity: item.quantity,
availableQuantity: product.stock,
});
continue;
}

result.items.push({
product: toClientProduct(product),
quantity: item.quantity,
});
}

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 }
);
}
}
79 changes: 79 additions & 0 deletions components/dashboard/DuplicateOrderButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
'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 addItems = useCartStore((state) => state.addItems);
const [loading, setLoading] = useState(false);
const [message, setMessage] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);

async function handleDuplicate() {
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 duplicate this order.');
return;
}

addItems(result.data.items);

const addedCount = result.data.items.reduce((sum, item) => sum + item.quantity, 0);
const skippedCount = result.data.skippedItems.length;

if (addedCount === 0) {
setMessage('None of the items in this order are currently available.');
} else if (skippedCount > 0) {
setMessage(
`Added ${addedCount} item${addedCount === 1 ? '' : 's'} to cart; ${skippedCount} unavailable product${skippedCount === 1 ? '' : 's'} skipped.`
);
} else {
setMessage(`Added ${addedCount} item${addedCount === 1 ? '' : 's'} to cart.`);
}
} catch {
setError('Could not duplicate this order. Please try again.');
} 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-rose-400" role="alert">
{error}
</p>
)}
</div>
);
}
5 changes: 4 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,9 @@ export function OrderCard({ order }: OrderCardProps) {
</div>
</div>

{}
<div className="border-t border-space-700 pt-3 flex justify-end">
<DuplicateOrderButton orderId={order.id} />
</div>
</Card>
);
}
29 changes: 14 additions & 15 deletions lib/orders/queries.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,22 @@
import { prisma } from '@/lib/db/client';

const DASHBOARD_HISTORY_DAYS = 30;

export async function listOrders(userId: string) {

const orders = await prisma.order.findMany({
where: { userId },
orderBy: { createdAt: 'desc' },
});
const historyStart = new Date(Date.now() - DASHBOARD_HISTORY_DAYS * 24 * 60 * 60 * 1000);

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

return ordersWithItems;
},
},
});
}

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

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

// Free shipping is represented only by shippingCents = 0. Crediting the
// shipping rate again through discountCents would apply the same benefit twice.
const totalCents = Math.max(0, subtotalCents - discountCents + shippingCents);

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

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

Expand Down
34 changes: 32 additions & 2 deletions store/cart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ import type { CartItem, Product } from '@/types';

interface CartState {
items: CartItem[];

addItem: (product: Product, quantity?: number) => void;
addItems: (items: CartItem[]) => void;
removeItem: (productId: string) => void;
updateQuantity: (productId: string, quantity: number) => void;
clearCart: () => void;

itemCount: () => number;
subtotalCents: () => number;
}
Expand All @@ -35,6 +36,35 @@ export const useCartStore = create<CartState>()(
});
},

addItems: (newItems: CartItem[]) => {
set((state) => {
const additions = new Map<string, CartItem>();

for (const item of newItems) {
const queued = additions.get(item.product.id);
additions.set(item.product.id, {
product: item.product,
quantity: (queued?.quantity ?? 0) + item.quantity,
});
}

const merged = state.items.map((item) => {
const addition = additions.get(item.product.id);
if (!addition) return item;

additions.delete(item.product.id);
return {
product: addition.product,
quantity: item.quantity + addition.quantity,
};
});

return {
items: [...merged, ...additions.values()],
};
});
},

removeItem: (productId: string) => {
set((state) => ({
items: state.items.filter((i) => i.product.id !== productId),
Expand Down
Loading