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
tsconfig.tsbuildinfo
71 changes: 57 additions & 14 deletions AI_LOG.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,68 @@
# 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.)*
## AI Tools Used

-
-
- Antigravity (Google DeepMind Agentic AI Coding Assistant running Gemini 3.6 Flash)

## Development and Testing Tools

- Vitest
- Next.js CLI
- TypeScript Compiler

## 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 1: Pricing Logic Bug

**Did you use AI to find the bug? If so, what prompt did you use?**

Yes. AI was used to inspect `lib/pricing/calculator.ts` and analyze the shipping discount calculation. It identified that the following logic was incorrectly adding `STANDARD_SHIPPING_RATE` to `discountCents` when shipping had already been set to zero:

`if (userTier === PRO_TIER && discountCode?.stackableWithFreeShipping)`

This resulted in shipping being effectively discounted twice for the affected case.

**Did you use AI to write the fix?**

Yes. AI was used to propose and implement the targeted fix by removing the redundant shipping discount addition while preserving the existing subtotal discount calculation. AI also helped create `tests/pricing.test.ts` to verify the pricing behavior.

I reviewed the implementation and verified it using the project's test suite and TypeScript checks.

### 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 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 was used for both analysis and implementation. It inspected `lib/orders/queries.ts` and the dashboard code and identified the N+1 query pattern caused by fetching order items separately for every order.

**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 then proposed and implemented a Prisma relation query using nested `include` statements, along with the required 30-day order-history filter.

The implementation was reviewed and verified using the project's tests, TypeScript compiler, and production build.

### Task 3: Duplicate Order Feature

**Did you use AI to write the API route, the client component, or both?**

AI was used to implement both the API route at `app/api/orders/[id]/duplicate/route.ts` and the client component at `components/dashboard/DuplicateOrderButton.tsx`.

The implementation includes authentication and ownership checks, stock/inactive-product filtering, quantity handling, and integration with the existing Zustand cart store.

AI also helped create the associated tests.

I reviewed the implementation and verified it through the project's automated tests, TypeScript checks, and production build.

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

AI was used throughout the assessment for repository discovery, debugging, implementation assistance, test creation, and code review.

The workflow was:

1. Inspect the repository and understand the existing architecture.
2. Identify the problems related to each assessment task.
3. Use AI to propose targeted fixes.
4. Review the generated changes against the existing codebase and requirements.
5. Run automated tests and static checks.
6. Run the production build and review the final changes before submission.

All AI-generated changes were reviewed before being included in the submission.
74 changes: 74 additions & 0 deletions app/api/orders/[id]/duplicate/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { NextRequest, NextResponse } from 'next/server';
import { getSession } from '@/lib/auth/session';
import { getOrderById } from '@/lib/orders/queries';
import type { ApiResponse, 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 availableItems: { product: Product; quantity: number }[] = [];
let skippedCount = 0;

for (const item of order.items) {
if (item.product && item.product.active && item.product.stock > 0) {
const qty = Math.min(item.quantity, item.product.stock);
const productData: Product = {
id: item.product.id,
name: item.product.name,
description: item.product.description,
priceCents: item.product.priceCents,
stock: item.product.stock,
category: item.product.category,
imageUrl: item.product.imageUrl,
active: item.product.active,
};
availableItems.push({ product: productData, quantity: qty });
} else {
skippedCount++;
}
}

return NextResponse.json<
ApiResponse<{ items: { product: Product; quantity: number }[]; skippedCount: number }>
>({
data: {
items: availableItems,
skippedCount,
},
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 }
);
}
}
90 changes: 90 additions & 0 deletions components/dashboard/DuplicateOrderButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
'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;
}

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

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

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

const json = (await res.json()) as ApiResponse<{
items: { product: Product; quantity: number }[];
skippedCount: number;
}>;

if (!res.ok || json.error || !json.data) {
setFeedback({
message: json.error || 'Failed to duplicate order',
type: 'error',
});
return;
}

const { items, skippedCount } = json.data;

if (items.length === 0) {
setFeedback({
message: 'All products in this order are currently out of stock',
type: 'error',
});
return;
}

for (const item of items) {
addItem(item.product, item.quantity);
}

const successMsg =
skippedCount > 0
? `Added ${items.length} item(s) to cart (${skippedCount} unavailable item(s) skipped)`
: '✓ Items added to cart';

setFeedback({ message: successMsg, type: 'success' });
setTimeout(() => setFeedback(null), 3000);
} catch (err) {
console.error('Failed to duplicate order:', err);
setFeedback({ message: 'Error duplicating order', type: 'error' });
} finally {
setLoading(false);
}
};

return (
<div className="flex flex-col gap-1 items-end">
<Button
id={`duplicate-order-${orderId}`}
variant="secondary"
size="sm"
loading={loading}
onClick={handleDuplicate}
>
Duplicate Order
</Button>
{feedback && (
<span
className={`text-xs font-mono ${
feedback.type === 'success' ? 'text-emerald-400' : 'text-rose-400'
}`}
>
{feedback.message}
</span>
)}
</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';

export async function listOrders(userId: string) {

const orders = await prisma.order.findMany({
where: { userId },
orderBy: { createdAt: 'desc' },
});
const thirtyDaysAgo = new Date(Date.now() - 30 * 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: thirtyDaysAgo,
},
},
orderBy: { createdAt: 'desc' },
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
Loading