Completes the first milestone of The Pet Loft ecommerce platform: - apps/storefront: full customer-facing Next.js app with HeroUI (cart, checkout, orders, wishlist, product detail, shop, search, auth) - convex/: serverless backend with schema, queries, mutations, actions, HTTP routes, Stripe/Shippo integrations, and co-located tests - packages/types, packages/utils, packages/convex: shared workspace packages Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
86 lines
2.3 KiB
TypeScript
86 lines
2.3 KiB
TypeScript
import { QueryCtx } from "../_generated/server";
|
|
import { Id, Doc } from "../_generated/dataModel";
|
|
|
|
export async function getOrderWithItems(
|
|
ctx: QueryCtx,
|
|
orderId: Id<"orders">,
|
|
) {
|
|
const order = await ctx.db.get(orderId);
|
|
if (!order) return null;
|
|
|
|
const items = await ctx.db
|
|
.query("orderItems")
|
|
.withIndex("by_order", (q) => q.eq("orderId", orderId))
|
|
.collect();
|
|
|
|
return { ...order, items };
|
|
}
|
|
|
|
/**
|
|
* Determines whether a customer is allowed to cancel a given order.
|
|
*
|
|
* NOTE: Cancellation only updates order status and restores stock.
|
|
* Stripe refund processing is a separate concern handled via the admin
|
|
* dashboard or a future automated flow. This helper does NOT trigger a refund.
|
|
*/
|
|
export function canCustomerCancel(order: Doc<"orders">): {
|
|
allowed: boolean;
|
|
reason?: string;
|
|
} {
|
|
switch (order.status) {
|
|
case "confirmed":
|
|
return { allowed: true };
|
|
case "pending":
|
|
return {
|
|
allowed: false,
|
|
reason: "Order is still awaiting payment confirmation.",
|
|
};
|
|
case "cancelled":
|
|
return { allowed: false, reason: "Order is already cancelled." };
|
|
case "refunded":
|
|
return { allowed: false, reason: "Order has already been refunded." };
|
|
default:
|
|
return {
|
|
allowed: false,
|
|
reason:
|
|
"Order has progressed past the cancellation window. Please contact support.",
|
|
};
|
|
}
|
|
}
|
|
|
|
export interface OutOfStockItem {
|
|
variantId: Id<"productVariants">;
|
|
requested: number;
|
|
available: number;
|
|
}
|
|
|
|
/**
|
|
* Check each cart item for sufficient stock. Returns list of out-of-stock entries.
|
|
*/
|
|
export async function validateCartItems(
|
|
ctx: Pick<QueryCtx, "db">,
|
|
items: { variantId?: Id<"productVariants">; quantity: number }[]
|
|
): Promise<OutOfStockItem[]> {
|
|
const outOfStock: OutOfStockItem[] = [];
|
|
for (const item of items) {
|
|
if (!item.variantId) continue;
|
|
const variant = await ctx.db.get(item.variantId);
|
|
if (!variant) {
|
|
outOfStock.push({
|
|
variantId: item.variantId,
|
|
requested: item.quantity,
|
|
available: 0,
|
|
});
|
|
continue;
|
|
}
|
|
if (variant.stockQuantity < item.quantity) {
|
|
outOfStock.push({
|
|
variantId: item.variantId,
|
|
requested: item.quantity,
|
|
available: variant.stockQuantity,
|
|
});
|
|
}
|
|
}
|
|
return outOfStock;
|
|
}
|