feat: initial commit — storefront, convex backend, and shared packages
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>
This commit is contained in:
85
convex/model/orders.ts
Normal file
85
convex/model/orders.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user