- Introduced a comprehensive markdown document outlining implementation rules for Convex functions, including syntax, registration, HTTP endpoints, and TypeScript usage. - Created a new configuration file for the Convex app, integrating the Resend service. - Added a new HTTP route for handling Shippo webhooks to ensure proper response handling. - Implemented integration tests for order timeline events, covering various scenarios including order fulfillment and status changes. - Enhanced existing functions with type safety improvements and additional validation logic. This commit establishes clear guidelines for backend development and improves the overall structure and reliability of the Convex application.
135 lines
3.6 KiB
TypeScript
135 lines
3.6 KiB
TypeScript
import { QueryCtx, MutationCtx } from "../_generated/server";
|
|
import { Id, Doc } from "../_generated/dataModel";
|
|
|
|
export async function recordOrderTimelineEvent(
|
|
ctx: MutationCtx,
|
|
args: {
|
|
orderId: Id<"orders">;
|
|
eventType: string;
|
|
source: string;
|
|
fromStatus?: string;
|
|
toStatus?: string;
|
|
payload?: string;
|
|
userId?: Id<"users">;
|
|
},
|
|
): Promise<void> {
|
|
await ctx.db.insert("orderTimelineEvents", {
|
|
...args,
|
|
createdAt: Date.now(),
|
|
});
|
|
}
|
|
|
|
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.",
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Determines whether a customer is allowed to request a return for a given order.
|
|
*
|
|
* Eligibility: order must be `delivered` (customer has received the goods),
|
|
* return not yet requested, and not already refunded.
|
|
* Returns and cancellations are separate flows — cancellation is only available
|
|
* on `confirmed` orders (before fulfilment begins).
|
|
*/
|
|
export function canCustomerRequestReturn(order: Doc<"orders">): {
|
|
allowed: boolean;
|
|
reason?: string;
|
|
} {
|
|
if (order.status !== "delivered") {
|
|
return {
|
|
allowed: false,
|
|
reason:
|
|
"Returns are only available for delivered orders.",
|
|
};
|
|
}
|
|
if (order.returnRequestedAt) {
|
|
return {
|
|
allowed: false,
|
|
reason: "A return has already been requested for this order.",
|
|
};
|
|
}
|
|
if (order.paymentStatus === "refunded") {
|
|
return { allowed: false, reason: "This order has already been refunded." };
|
|
}
|
|
return { allowed: true };
|
|
}
|
|
|
|
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;
|
|
}
|