Files
the-pet-loft/convex/checkout.ts
ianshaloom cc15338ad9 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>
2026-03-04 09:31:18 +03:00

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;
},
});