Files
the-pet-loft/convex/addresses.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

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