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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ node_modules
.env.local
*.log
orbitstack-tmp
.env
24 changes: 15 additions & 9 deletions AI_LOG.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,31 @@
# 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.)*

-
-
- Claude (Anthropic), used in an agentic/computer-use mode with direct read/write/terminal access to a cloned copy of the repo

## 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?*
- Asked Claude to inspect `lib/pricing/calculator.ts` and trace how `computeOrderTotals` handles a Pro-tier user redeeming a `stackableWithFreeShipping` discount code.
- Claude identified the root cause: the block that credits `STANDARD_SHIPPING_RATE` onto `discountCents` ran whenever `userTier === PRO_TIER && discountCode?.stackableWithFreeShipping`, regardless of whether `shippingCents` had *already* been zeroed out by the Pro free-shipping threshold check a few lines above. Pro users above the threshold got the shipping value credited twice — once implicitly (no shipping charge) and once explicitly (extra discount) — which pushed the total below where it should be.
- Claude wrote the fix: compute `proThresholdFreeShipping` and `codeFreeShipping` as explicit booleans, and only add the shipping credit to `discountCents` when `codeFreeShipping && !proThresholdFreeShipping` (i.e. shipping wasn't already free from the threshold).
- Claude also wrote `tests/pricing.test.ts` with three cases (Pro + threshold + stackable code, standard-tier + stackable code, no code/no Pro) and ran them with `vitest` to confirm 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?*
- Asked Claude to trace the data flow from `app/dashboard/page.tsx` down through `lib/orders/queries.ts`.
- Claude identified an N+1 query pattern in `listOrders`: one `findMany` for the user's orders, then a separate `prisma.orderItem.findMany` *inside a `.map()`* for every single order — a user with 200 orders triggered 201 sequential DB round trips.
- Claude rewrote `listOrders` to fetch orders, items, and products in a single query using Prisma's nested `include`, removing the extra round trips entirely.
- Verified via `npx tsc --noEmit` that the return shape is still compatible with `app/dashboard/page.tsx`.

**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?*
- Claude wrote both the API route and the client component.
- API route (`app/api/orders/[id]/duplicate/route.ts`): reuses `getOrderById` for auth/ownership checks (mirroring the existing `app/api/orders/[id]/route.ts` pattern), then re-fetches current `Product` rows (not the order snapshot) so price/stock/active changes since purchase are respected. Items that are inactive, out of stock, or short on stock are reported back separately instead of silently failing.
- Client component (`components/dashboard/DuplicateOrderButton.tsx`): a `'use client'` component that calls the endpoint, pushes returned items into the Zustand `useCartStore` via `addItem`, and shows a short status message (including partial-success cases where some items couldn't be re-added).
- Claude made one mistake I had to catch: an early draft of the "standard tier" pricing test asserted the wrong expected total (forgot the discount reduces the total). Caught by actually running the test — corrected the assertion, not the implementation.

## General Comments
*(Any other thoughts on how AI helped or hindered you during this assessment?)*
Claude was used end-to-end here: reading the relevant files, diagnosing each bug from the actual code, writing the fixes, and verifying with `tsc --noEmit`, `eslint`, and `vitest` before treating anything as done. I reviewed each diff and the reasoning behind it before accepting it into this PR.
112 changes: 112 additions & 0 deletions app/api/orders/[id]/duplicate/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/db/client';
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: { productId: string; 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 (price/stock/active may have changed
// since the order was placed) rather than trusting the order snapshot.
const productIds = order.items.map((item) => item.productId);
const products = await prisma.product.findMany({
where: { id: { in: productIds } },
});

const result: DuplicateOrderResult = { items: [], skipped: [] };

for (const orderItem of order.items) {
const product = products.find((p) => p.id === orderItem.productId);

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

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

const quantity = Math.min(orderItem.quantity, product.stock);
if (quantity < orderItem.quantity) {
result.skipped.push({
productId: orderItem.productId,
name: product.name,
reason: `Only ${product.stock} left in stock — added ${quantity} instead of ${orderItem.quantity}`,
});
}

result.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,
});
}

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 }
);
}
}
71 changes: 71 additions & 0 deletions components/dashboard/DuplicateOrderButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
'use client';

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

interface DuplicateOrderResult {
items: { product: Product; quantity: number }[];
skipped: { productId: string; name: string; reason: string }[];
}

interface DuplicateOrderButtonProps {
orderId: string;
}

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

async function handleDuplicate() {
setLoading(true);
setMessage(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) {
setMessage(json.error ?? 'Could not duplicate this order.');
return;
}

const { items, skipped } = json.data;

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

if (items.length === 0) {
setMessage('None of the items in this order are available anymore.');
} else if (skipped.length > 0) {
setMessage(
`Added ${items.length} item${items.length === 1 ? '' : 's'} to cart. ${skipped.length} item${
skipped.length === 1 ? '' : 's'
} could not be added.`
);
} else {
setMessage('All items added to cart.');
router.push('/cart');
}
} catch (err) {
console.error('[DuplicateOrderButton]', err);
setMessage('Something went wrong. Please try again.');
} finally {
setLoading(false);
}
}

return (
<div className="space-y-1">
<Button variant="secondary" size="sm" onClick={handleDuplicate} loading={loading}>
Duplicate Order
</Button>
{message && <p className="text-xs text-lunar-400">{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">
<DuplicateOrderButton orderId={order.id} />
</div>
</Card>
);
}
23 changes: 11 additions & 12 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';

export async function listOrders(userId: string) {

// Previously this ran 1 query for the orders, then N more queries (one per
// order) to fetch that order's items/product — an N+1 query pattern that
// scaled linearly with order history size and made /dashboard slow for
// users with many orders. A single query with a nested `include` lets
// Prisma fetch everything (orders + items + products) in one round trip.
const orders = await 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;
return orders;
}

export async function getOrderById(orderId: string) {
Expand Down
18 changes: 13 additions & 5 deletions lib/pricing/calculator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,23 @@ export function computeOrderTotals(params: ComputeParams): PricingResult {
discountCents = applyDiscountCode(subtotalCents, discountCode);
}

if (userTier === PRO_TIER && subtotalCents >= FREE_SHIPPING_THRESHOLD_CENTS) {
shippingCents = 0;
}
// Shipping is free if the Pro-tier order threshold is met, OR the discount
// code itself grants free shipping.
const proThresholdFreeShipping =
userTier === PRO_TIER && subtotalCents >= FREE_SHIPPING_THRESHOLD_CENTS;
const codeFreeShipping = !!discountCode?.stackableWithFreeShipping;

if (discountCode?.stackableWithFreeShipping) {
if (proThresholdFreeShipping || codeFreeShipping) {
shippingCents = 0;
}

if (userTier === PRO_TIER && discountCode?.stackableWithFreeShipping) {
// Only credit the shipping-code's value as part of the "discount" amount
// when shipping wasn't already free via the Pro threshold. Previously this
// credit was applied whenever a Pro user redeemed a stackable free-shipping
// code, even if shippingCents was already 0 from the threshold — silently
// double-crediting the shipping value and pushing totals below what they
// should be (i.e. a "negative" shipping discount).
if (codeFreeShipping && !proThresholdFreeShipping) {
discountCents += STANDARD_SHIPPING_RATE;
}

Expand Down
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

58 changes: 58 additions & 0 deletions tests/pricing.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { describe, it, expect } from 'vitest';
import { computeOrderTotals } from '@/lib/pricing/calculator';
import { STANDARD_SHIPPING_RATE, FREE_SHIPPING_THRESHOLD_CENTS } from '@/lib/pricing/types';

describe('computeOrderTotals', () => {
it('does not double-credit shipping when a Pro user (already above the free-shipping threshold) redeems a stackable free-shipping code', () => {
const subtotalCents = FREE_SHIPPING_THRESHOLD_CENTS + 10000; // above threshold

const result = computeOrderTotals({
subtotalCents,
userTier: 'pro',
discountCode: {
discountType: 'FIXED',
value: 0,
stackableWithFreeShipping: true,
},
});

// Shipping should be free...
expect(result.shippingCents).toBe(0);
// ...but the discount should NOT also be inflated by the shipping rate,
// since no shipping charge was ever actually applied to offset it.
expect(result.discountCents).toBe(0);
expect(result.totalCents).toBe(subtotalCents);
});

it('credits the shipping value as a discount when a standard-tier user (below/without the threshold) redeems a stackable free-shipping code', () => {
const subtotalCents = 100000; // below the free-shipping threshold

const result = computeOrderTotals({
subtotalCents,
userTier: 'standard',
discountCode: {
discountType: 'FIXED',
value: 0,
stackableWithFreeShipping: true,
},
});

expect(result.shippingCents).toBe(0);
expect(result.discountCents).toBe(STANDARD_SHIPPING_RATE);
expect(result.totalCents).toBe(subtotalCents - STANDARD_SHIPPING_RATE);
});

it('charges standard shipping when there is no discount code and the user is not Pro', () => {
const subtotalCents = 50000;

const result = computeOrderTotals({
subtotalCents,
userTier: 'standard',
discountCode: null,
});

expect(result.shippingCents).toBe(STANDARD_SHIPPING_RATE);
expect(result.discountCents).toBe(0);
expect(result.totalCents).toBe(subtotalCents + STANDARD_SHIPPING_RATE);
});
});