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>
169 lines
5.4 KiB
TypeScript
169 lines
5.4 KiB
TypeScript
import { query, mutation } from "./_generated/server";
|
|
import { v } from "convex/values";
|
|
import * as Users from "./model/users";
|
|
|
|
const addressTypeValidator = v.union(
|
|
v.literal("shipping"),
|
|
v.literal("billing"),
|
|
);
|
|
|
|
const addressFieldsValidator = {
|
|
type: addressTypeValidator,
|
|
fullName: v.optional(v.string()),
|
|
firstName: v.string(),
|
|
lastName: v.string(),
|
|
phone: v.string(),
|
|
addressLine1: v.string(),
|
|
additionalInformation: v.optional(v.string()),
|
|
city: v.string(),
|
|
postalCode: v.string(),
|
|
country: v.string(),
|
|
isDefault: v.boolean(),
|
|
isValidated: v.optional(v.boolean()),
|
|
};
|
|
|
|
export const list = query({
|
|
args: {},
|
|
handler: async (ctx) => {
|
|
const user = await Users.getCurrentUserOrThrow(ctx);
|
|
const addresses = await ctx.db
|
|
.query("addresses")
|
|
.withIndex("by_user", (q) => q.eq("userId", user._id))
|
|
.collect();
|
|
addresses.sort((a, b) => {
|
|
if (a.isDefault !== b.isDefault) return a.isDefault ? -1 : 1;
|
|
return b._creationTime - a._creationTime;
|
|
});
|
|
return addresses;
|
|
},
|
|
});
|
|
|
|
export const add = mutation({
|
|
args: addressFieldsValidator,
|
|
handler: async (ctx, args) => {
|
|
const user = await Users.getCurrentUserOrThrow(ctx);
|
|
|
|
const existingOfType = await ctx.db
|
|
.query("addresses")
|
|
.withIndex("by_user_and_type", (q) =>
|
|
q.eq("userId", user._id).eq("type", args.type),
|
|
)
|
|
.collect();
|
|
|
|
const isDefault = existingOfType.length === 0 ? true : args.isDefault;
|
|
|
|
if (isDefault && existingOfType.length > 0) {
|
|
for (const addr of existingOfType) {
|
|
await ctx.db.patch(addr._id, { isDefault: false });
|
|
}
|
|
}
|
|
|
|
return await ctx.db.insert("addresses", {
|
|
userId: user._id,
|
|
type: args.type,
|
|
fullName: args.fullName || `${args.firstName} ${args.lastName}`,
|
|
firstName: args.firstName,
|
|
lastName: args.lastName,
|
|
phone: args.phone,
|
|
addressLine1: args.addressLine1,
|
|
additionalInformation: args.additionalInformation,
|
|
city: args.city,
|
|
postalCode: args.postalCode,
|
|
country: args.country,
|
|
isDefault,
|
|
isValidated: args.isValidated ?? false,
|
|
});
|
|
},
|
|
});
|
|
|
|
export const update = mutation({
|
|
args: {
|
|
id: v.id("addresses"),
|
|
type: v.optional(addressTypeValidator),
|
|
fullName: v.optional(v.string()),
|
|
firstName: v.optional(v.string()),
|
|
lastName: v.optional(v.string()),
|
|
phone: v.optional(v.string()),
|
|
addressLine1: v.optional(v.string()),
|
|
additionalInformation: v.optional(v.string()),
|
|
city: v.optional(v.string()),
|
|
postalCode: v.optional(v.string()),
|
|
country: v.optional(v.string()),
|
|
isDefault: v.optional(v.boolean()),
|
|
isValidated: v.optional(v.boolean()),
|
|
},
|
|
handler: async (ctx, args) => {
|
|
const { id, ...updates } = args;
|
|
const address = await ctx.db.get(id);
|
|
if (!address) throw new Error("Address not found");
|
|
await Users.requireOwnership(ctx, address.userId);
|
|
const patch: Record<string, unknown> = {};
|
|
if (updates.type !== undefined) patch.type = updates.type;
|
|
if (updates.fullName !== undefined) patch.fullName = updates.fullName;
|
|
if (updates.firstName !== undefined) patch.firstName = updates.firstName;
|
|
if (updates.lastName !== undefined) patch.lastName = updates.lastName;
|
|
if (updates.phone !== undefined) patch.phone = updates.phone;
|
|
if (updates.addressLine1 !== undefined)
|
|
patch.addressLine1 = updates.addressLine1;
|
|
if (updates.additionalInformation !== undefined)
|
|
patch.additionalInformation = updates.additionalInformation;
|
|
if (updates.city !== undefined) patch.city = updates.city;
|
|
if (updates.postalCode !== undefined) patch.postalCode = updates.postalCode;
|
|
if (updates.country !== undefined) patch.country = updates.country;
|
|
if (updates.isDefault !== undefined) patch.isDefault = updates.isDefault;
|
|
if (updates.isValidated !== undefined)
|
|
patch.isValidated = updates.isValidated;
|
|
if (Object.keys(patch).length === 0) return id;
|
|
await ctx.db.patch(id, patch as any);
|
|
return id;
|
|
},
|
|
});
|
|
|
|
export const remove = mutation({
|
|
args: { id: v.id("addresses") },
|
|
handler: async (ctx, { id }) => {
|
|
const address = await ctx.db.get(id);
|
|
if (!address) throw new Error("Address not found");
|
|
await Users.requireOwnership(ctx, address.userId);
|
|
if (address.isDefault) {
|
|
const rest = await ctx.db
|
|
.query("addresses")
|
|
.withIndex("by_user", (q) => q.eq("userId", address.userId))
|
|
.filter((q) => q.neq(q.field("_id"), id))
|
|
.collect();
|
|
const first = rest[0];
|
|
if (first) await ctx.db.patch(first._id, { isDefault: true });
|
|
}
|
|
await ctx.db.delete(id);
|
|
},
|
|
});
|
|
|
|
export const setDefault = mutation({
|
|
args: { id: v.id("addresses") },
|
|
handler: async (ctx, { id }) => {
|
|
const address = await ctx.db.get(id);
|
|
if (!address) throw new Error("Address not found");
|
|
await Users.requireOwnership(ctx, address.userId);
|
|
const all = await ctx.db
|
|
.query("addresses")
|
|
.withIndex("by_user", (q) => q.eq("userId", address.userId))
|
|
.collect();
|
|
for (const addr of all) {
|
|
await ctx.db.patch(addr._id, { isDefault: addr._id === id });
|
|
}
|
|
},
|
|
});
|
|
|
|
export const markValidated = mutation({
|
|
args: {
|
|
id: v.id("addresses"),
|
|
isValidated: v.boolean(),
|
|
},
|
|
handler: async (ctx, { id, isValidated }) => {
|
|
const address = await ctx.db.get(id);
|
|
if (!address) throw new Error("Address not found");
|
|
await Users.requireOwnership(ctx, address.userId);
|
|
await ctx.db.patch(id, { isValidated });
|
|
},
|
|
});
|