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:
92
convex/checkout.ts
Normal file
92
convex/checkout.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
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;
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user