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
16 changes: 9 additions & 7 deletions AI_LOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,23 @@ 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.)*

-
-
- ChatGPT Codex (GPT-5.6)
- Command-line tools: Git, npm, Vitest, ESLint, TypeScript, Prisma and Next.js

## 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 gave ChatGPT Codex the full challenge brief and repository URL. It inspected the pricing calculator and identified that the Pro/free-shipping branch incorrectly added the standard shipping rate to `discountCents` after shipping was already zeroed.
- AI wrote the small fix and regression tests. I reviewed the expected totals for Pro, standard, and non-free-shipping cases.

**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?*
- AI identified the N+1 database pattern: one query loaded all orders and then one additional query ran for every order. It replaced this with one Prisma `findMany` query using a nested `include`, and added a compound `(userId, createdAt DESC)` index matching the dashboard filter and sort.

**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?*
- AI implemented both the authenticated API route and client component, plus a bulk cart-store action. The endpoint verifies order ownership and current product activity/stock, returns only fully available order lines, and reports skipped products.
- The implementation was validated with TypeScript, ESLint, Vitest, Prisma validation, and a Next.js production build. The initial npm install required changing the cache directory because the default cache was unavailable in the execution environment; this was an environment issue, not an application-code issue.

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

AI accelerated repository exploration, implementation, test design, and documentation. I used the generated changes as a starting point and relied on automated checks to verify that the final code remained type-safe and buildable.
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,38 @@ Welcome to OrbitStack! This is a modern, Next.js-based e-commerce platform built
npm run dev
```

4. Open [http://localhost:3000](http://localhost:3000). Use one of the seeded user accounts shown by `npm run seed` to sign in.

## Local database setup

Start PostgreSQL with Docker, configure the connection, create the schema, and seed sample data:

```bash
docker compose up -d
cp .env.example .env
npx prisma db push
npm run seed
```

Run the verification suite with:

```bash
npm test
npm run lint
npx tsc --noEmit
npm run build
```

## Implemented fixes

- Corrected stacked free-shipping logic so shipping is waived exactly once and never becomes a product discount.
- Removed the dashboard's N+1 order-item queries by loading orders, items, and products in one Prisma query; added an index matching the user/date query.
- Added an authenticated **Duplicate Order** action. It verifies ownership and current inventory, adds available order lines to the Zustand cart in one update, and clearly reports unavailable lines.

## Remaining limitations

- Duplicate Order validates stock at click time. As with any cart, availability can change before checkout, where the existing order endpoint validates stock again.

## The Challenge

There are three tasks to complete. Please review the codebase and implement the fixes and features described below.
Expand Down
1 change: 0 additions & 1 deletion app/(auth)/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { Button } from '@/components/ui/Button';

const SEEDED_USERS = [
{ email: 'alice@orbitstack.dev', name: 'Alice Nakamura', tier: 'Pro' },
Expand Down
73 changes: 73 additions & 0 deletions app/api/orders/[id]/duplicate/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { NextResponse } from 'next/server';
import { getSession } from '@/lib/auth/session';
import { prisma } from '@/lib/db/client';
import type { ApiResponse, CartItem } from '@/types';

interface DuplicateOrderResult {
items: CartItem[];
unavailableItems: string[];
}

export async function POST(
_request: Request,
{ 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 prisma.order.findFirst({
where: { id, userId: session.id },
select: {
items: {
select: {
quantity: true,
product: true,
},
},
},
});

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

const availableItems: CartItem[] = [];
const unavailableItems: string[] = [];

for (const item of order.items) {
if (item.product.active && item.product.stock >= item.quantity) {
availableItems.push({ product: item.product, quantity: item.quantity });
} else {
unavailableItems.push(item.product.name);
}
}

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

return NextResponse.json<ApiResponse<DuplicateOrderResult>>({
data: { items: availableItems, unavailableItems },
error: null,
});
} catch (error) {
console.error('[POST /api/orders/[id]/duplicate]', error);
return NextResponse.json<ApiResponse<null>>(
{ data: null, error: 'Failed to duplicate order' },
{ status: 500 }
);
}
}
2 changes: 1 addition & 1 deletion app/api/orders/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { listOrders } from '@/lib/orders/queries';
import { computeOrderTotals } from '@/lib/pricing/calculator';
import type { ApiResponse } from '@/types';

export async function GET(request: NextRequest) {
export async function GET() {
try {
const session = await getSession();
if (!session) {
Expand Down
1 change: 0 additions & 1 deletion app/cart/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { Metadata } from 'next';
import { CartItem } from '@/components/cart/CartItem';
import { CartSummaryPage } from '@/components/cart/CartSummaryPage';

export const metadata: Metadata = {
Expand Down
6 changes: 3 additions & 3 deletions app/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Metadata } from 'next';
import Link from 'next/link';
import { redirect } from 'next/navigation';
import { getSession } from '@/lib/auth/session';
import { listOrders } from '@/lib/orders/queries';
Expand Down Expand Up @@ -43,9 +44,9 @@ export default async function DashboardPage() {
<div className="rounded-xl border border-space-700 bg-space-900 py-16 text-center text-lunar-400">
<div className="mb-3 text-4xl">🌑</div>
<p>No orders yet.</p>
<a href="/products" className="mt-2 block text-sm text-moon-gold hover:underline">
<Link href="/products" className="mt-2 block text-sm text-moon-gold hover:underline">
Start shopping →
</a>
</Link>
</div>
) : (
<ul className="space-y-4">
Expand All @@ -59,4 +60,3 @@ export default async function DashboardPage() {
</div>
);
}

9 changes: 5 additions & 4 deletions app/products/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Metadata } from 'next';
import Link from 'next/link';
import { prisma } from '@/lib/db/client';
import { ProductGrid } from '@/components/products/ProductGrid';
import type { Product } from '@/types';
Expand Down Expand Up @@ -40,16 +41,16 @@ export default async function ProductsPage({

{}
<div className="flex flex-wrap gap-2">
<a
<Link
href="/products"
className={`rounded-lg px-3 py-1.5 text-sm transition-colors ${
!category ? 'bg-moon-gold text-space-950 font-medium' : 'bg-space-800 text-lunar-400 hover:text-lunar-100'
}`}
>
All
</a>
</Link>
{categories.map((cat) => (
<a
<Link
key={cat}
href={`/products?category=${cat}`}
className={`rounded-lg px-3 py-1.5 text-sm capitalize transition-colors ${
Expand All @@ -59,7 +60,7 @@ export default async function ProductsPage({
}`}
>
{cat}
</a>
</Link>
))}
</div>
</div>
Expand Down
8 changes: 2 additions & 6 deletions components/cart/CartDrawer.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client';

import { useState, useEffect } from 'react';
import { useCartStore } from '@/store/cart';
import { useHydrated } from '@/lib/utils/useHydrated';
import { CartItem } from './CartItem';
import { CartSummary } from './CartSummary';
import { Button } from '@/components/ui/Button';
Expand All @@ -13,14 +13,10 @@ interface CartDrawerProps {
}

export function CartDrawer({ open, onClose }: CartDrawerProps) {
const [mounted, setMounted] = useState(false);
const mounted = useHydrated();
const { items, itemCount } = useCartStore();
const count = itemCount();

useEffect(() => {
setMounted(true);
}, []);

const displayedItems = mounted ? items : [];
const displayedCount = mounted ? count : 0;

Expand Down
8 changes: 2 additions & 6 deletions components/cart/CartSummaryPage.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,16 @@
'use client';

import { useState, useEffect } from 'react';
import { useCartStore } from '@/store/cart';
import { useHydrated } from '@/lib/utils/useHydrated';
import { CartItem } from './CartItem';
import { CartSummary } from './CartSummary';
import { Button } from '@/components/ui/Button';
import Link from 'next/link';

export function CartSummaryPage() {
const [mounted, setMounted] = useState(false);
const mounted = useHydrated();
const { items, clearCart } = useCartStore();

useEffect(() => {
setMounted(true);
}, []);

const displayedItems = mounted ? items : [];

if (displayedItems.length === 0) {
Expand Down
12 changes: 5 additions & 7 deletions components/checkout/CheckoutForm.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,25 @@
'use client';

import { useState, useEffect } from 'react';
import { useState } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { useCartStore } from '@/store/cart';
import { CartSummary } from '@/components/cart/CartSummary';
import { DiscountInput } from './DiscountInput';
import { Button } from '@/components/ui/Button';
import { formatCents } from '@/lib/utils/format';
import type { PricingResult } from '@/types';
import { useHydrated } from '@/lib/utils/useHydrated';

export function CheckoutForm() {
const router = useRouter();
const [mounted, setMounted] = useState(false);
const mounted = useHydrated();
const { items, clearCart } = useCartStore();
const [pricing, setPricing] = useState<PricingResult | null>(null);
const [appliedCode, setAppliedCode] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);

useEffect(() => {
setMounted(true);
}, []);

const handleDiscountApplied = (code: string, result: PricingResult) => {
setAppliedCode(code);
setPricing(result);
Expand Down Expand Up @@ -61,7 +59,7 @@ export function CheckoutForm() {
if (!mounted || items.length === 0) {
return (
<div className="py-12 text-center text-lunar-400">
Your cart is empty. <a href="/products" className="text-moon-gold hover:underline">Browse products</a>
Your cart is empty. <Link href="/products" className="text-moon-gold hover:underline">Browse products</Link>
</div>
);
}
Expand Down
56 changes: 56 additions & 0 deletions components/dashboard/DuplicateOrderButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
'use client';

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

interface DuplicateOrderButtonProps {
orderId: string;
}

interface DuplicateOrderResult {
items: CartItem[];
unavailableItems: string[];
}

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

async function duplicateOrder() {
setLoading(true);
setMessage(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) {
throw new Error(result.error ?? 'Unable to duplicate order');
}

addItems(result.data.items);
const skipped = result.data.unavailableItems.length;
setMessage(
skipped > 0
? `Added available items; ${skipped} unavailable item${skipped === 1 ? '' : 's'} skipped.`
: 'Order added to cart.'
);
} catch (error) {
setMessage(error instanceof Error ? error.message : 'Unable to duplicate order');
} finally {
setLoading(false);
}
}

return (
<div className="flex flex-col items-end gap-2">
<Button type="button" size="sm" loading={loading} onClick={duplicateOrder}>
Duplicate Order
</Button>
{message && <p className="text-right text-xs text-lunar-400" role="status">{message}</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="flex justify-end border-t border-space-700 pt-4">
<DuplicateOrderButton orderId={order.id} />
</div>
</Card>
);
}
Loading