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>
93 lines
2.5 KiB
TypeScript
93 lines
2.5 KiB
TypeScript
import { query, internalQuery } from "./_generated/server";
|
|
import { v } from "convex/values";
|
|
import * as Users from "./model/users";
|
|
import * as CartsModel from "./model/carts";
|
|
import { validateAndEnrichCart } from "./model/checkout";
|
|
import type { CartValidationResult } from "./model/checkout";
|
|
|
|
const EMPTY_RESULT_INVALID: CartValidationResult = {
|
|
valid: false,
|
|
items: [],
|
|
issues: [],
|
|
subtotal: 0,
|
|
};
|
|
|
|
const EMPTY_RESULT_VALID: CartValidationResult = {
|
|
valid: true,
|
|
items: [],
|
|
issues: [],
|
|
subtotal: 0,
|
|
};
|
|
|
|
export const validateCart = query({
|
|
args: { sessionId: v.optional(v.string()) },
|
|
handler: async (ctx, args) => {
|
|
const user = await Users.getCurrentUser(ctx);
|
|
const userId = user?._id ?? null;
|
|
|
|
if (!userId && !args.sessionId) {
|
|
return EMPTY_RESULT_INVALID;
|
|
}
|
|
|
|
const cart = await CartsModel.getCart(ctx, userId ?? undefined, args.sessionId);
|
|
if (!cart || cart.items.length === 0) {
|
|
return EMPTY_RESULT_VALID;
|
|
}
|
|
|
|
return await validateAndEnrichCart(ctx, cart.items);
|
|
},
|
|
});
|
|
|
|
export const getShippingAddresses = query({
|
|
args: {},
|
|
handler: async (ctx) => {
|
|
const user = await Users.getCurrentUserOrThrow(ctx);
|
|
const addresses = await ctx.db
|
|
.query("addresses")
|
|
.withIndex("by_user_and_type", (q) =>
|
|
q.eq("userId", user._id).eq("type", "shipping"),
|
|
)
|
|
.collect();
|
|
addresses.sort((a, b) => {
|
|
if (a.isDefault !== b.isDefault) return a.isDefault ? -1 : 1;
|
|
return b._creationTime - a._creationTime;
|
|
});
|
|
return addresses;
|
|
},
|
|
});
|
|
|
|
// ─── Internal Queries (consumed by actions in checkoutActions.ts) ────────────
|
|
|
|
export const getAddressById = internalQuery({
|
|
args: { addressId: v.id("addresses") },
|
|
handler: async (ctx, args) => {
|
|
const address = await ctx.db.get(args.addressId);
|
|
if (!address) throw new Error("Address not found");
|
|
return address;
|
|
},
|
|
});
|
|
|
|
export const validateCartInternal = internalQuery({
|
|
args: {
|
|
userId: v.optional(v.id("users")),
|
|
sessionId: v.optional(v.string()),
|
|
},
|
|
handler: async (ctx, args) => {
|
|
const cart = await CartsModel.getCart(
|
|
ctx,
|
|
args.userId ?? undefined,
|
|
args.sessionId,
|
|
);
|
|
if (!cart || cart.items.length === 0) return null;
|
|
return await validateAndEnrichCart(ctx, cart.items);
|
|
},
|
|
});
|
|
|
|
export const getCurrentUserId = internalQuery({
|
|
args: {},
|
|
handler: async (ctx) => {
|
|
const user = await Users.getCurrentUserOrThrow(ctx);
|
|
return user._id;
|
|
},
|
|
});
|