Persist user carts in database

This commit is contained in:
Ruslan Bakiev
2026-04-04 09:08:42 +07:00
parent 6ea9d4e5b7
commit b01b2421b5
4 changed files with 338 additions and 0 deletions

View File

@@ -76,6 +76,15 @@ function normalizeOptionalText(value) {
return normalized ? normalized : null;
}
function normalizeQuantityValue(value) {
const normalized = Number(value);
if (!Number.isFinite(normalized) || normalized <= 0) {
return 0;
}
return Math.floor(normalized);
}
function isCounterpartyProfileComplete(profile) {
if (!profile) {
return false;
@@ -134,6 +143,38 @@ function withDeliveryAddressDefaultFlag(address, defaultDeliveryAddressId) {
};
}
function defaultCartParameters(product) {
return {
width: product.widthMm ?? 100,
thickness: product.thicknessMicron ?? 50,
color: 'прозрачный',
};
}
const cartInclude = {
items: {
orderBy: [{ updatedAt: 'desc' }, { createdAt: 'desc' }],
},
deliveryAddress: true,
};
async function getOrCreateCart(context, userId) {
const account = await context.prisma.user.findUnique({
where: { id: userId },
select: { defaultDeliveryAddressId: true },
});
return context.prisma.cart.upsert({
where: { userId },
update: {},
create: {
userId,
deliveryAddressId: account?.defaultDeliveryAddressId ?? null,
},
include: cartInclude,
});
}
async function enrichMessengerConnectionProfile(prisma, connection) {
if (
connection.type !== 'TELEGRAM' ||
@@ -306,6 +347,32 @@ export const resolvers = {
DeliveryAddress: {
isDefault: (address) => Boolean(address.isDefault),
},
Cart: {
deliveryAddress: async (cart, _, context) => {
if (!cart.deliveryAddressId) {
return null;
}
const [account, address] = await Promise.all([
context.prisma.user.findUnique({
where: { id: cart.userId },
select: { defaultDeliveryAddressId: true },
}),
context.prisma.deliveryAddress.findUnique({
where: { id: cart.deliveryAddressId },
}),
]);
if (!address) {
return null;
}
return withDeliveryAddressDefaultFlag(address, account?.defaultDeliveryAddressId ?? null);
},
},
CartItem: {
quantity: (item) => toFloat(item.quantity),
},
Query: {
healthcheck: () => 'ok',
@@ -319,6 +386,11 @@ export const resolvers = {
});
},
myCart: async (_, __, context) => {
const user = requireUser(context);
return getOrCreateCart(context, user.id);
},
myDeliveryAddresses: async (_, __, context) => {
const user = requireUser(context);
const [account, addresses] = await Promise.all([
@@ -678,6 +750,171 @@ export const resolvers = {
});
},
addProductToCart: async (_, { productId }, context) => {
const user = requireUser(context);
const normalizedProductId = normalizeText(productId);
if (!normalizedProductId) {
throw new Error('Product id is required.');
}
const product = await context.prisma.product.findFirst({
where: {
id: normalizedProductId,
isActive: true,
},
});
if (!product) {
throw new Error('Product is not available.');
}
const cart = await getOrCreateCart(context, user.id);
const existingItem = await context.prisma.cartItem.findFirst({
where: {
cartId: cart.id,
productId: normalizedProductId,
},
});
if (existingItem) {
await context.prisma.cartItem.update({
where: { id: existingItem.id },
data: {
quantity: Number(existingItem.quantity) + 1,
productName: product.name,
sku: product.sku,
isCustomizable: product.isCustomizable,
},
});
} else {
await context.prisma.cartItem.create({
data: {
cartId: cart.id,
productId: product.id,
productName: product.name,
sku: product.sku,
isCustomizable: product.isCustomizable,
quantity: 1,
parameters: defaultCartParameters(product),
},
});
}
return context.prisma.cart.findUnique({
where: { id: cart.id },
include: cartInclude,
});
},
updateCartItemQuantity: async (_, { input }, context) => {
const user = requireUser(context);
const normalizedProductId = normalizeText(input.productId);
if (!normalizedProductId) {
throw new Error('Product id is required.');
}
const quantity = normalizeQuantityValue(input.quantity);
const cart = await getOrCreateCart(context, user.id);
const existingItem = await context.prisma.cartItem.findFirst({
where: {
cartId: cart.id,
productId: normalizedProductId,
},
});
if (!existingItem) {
return cart;
}
if (quantity === 0) {
await context.prisma.cartItem.delete({
where: { id: existingItem.id },
});
} else {
await context.prisma.cartItem.update({
where: { id: existingItem.id },
data: { quantity },
});
}
return context.prisma.cart.findUnique({
where: { id: cart.id },
include: cartInclude,
});
},
removeCartItem: async (_, { productId }, context) => {
const user = requireUser(context);
const normalizedProductId = normalizeText(productId);
if (!normalizedProductId) {
throw new Error('Product id is required.');
}
const cart = await getOrCreateCart(context, user.id);
const existingItem = await context.prisma.cartItem.findFirst({
where: {
cartId: cart.id,
productId: normalizedProductId,
},
});
if (existingItem) {
await context.prisma.cartItem.delete({
where: { id: existingItem.id },
});
}
return context.prisma.cart.findUnique({
where: { id: cart.id },
include: cartInclude,
});
},
setCartDeliveryAddress: async (_, { addressId }, context) => {
const user = requireUser(context);
const cart = await getOrCreateCart(context, user.id);
const normalizedAddressId = normalizeOptionalText(addressId);
if (!normalizedAddressId) {
return context.prisma.cart.update({
where: { id: cart.id },
data: { deliveryAddressId: null },
include: cartInclude,
});
}
const address = await context.prisma.deliveryAddress.findFirst({
where: {
id: normalizedAddressId,
userId: user.id,
},
});
if (!address) {
throw new Error('Delivery address is not available for this user.');
}
return context.prisma.cart.update({
where: { id: cart.id },
data: { deliveryAddressId: address.id },
include: cartInclude,
});
},
clearCart: async (_, __, context) => {
const user = requireUser(context);
const cart = await getOrCreateCart(context, user.id);
await context.prisma.cartItem.deleteMany({
where: { cartId: cart.id },
});
return context.prisma.cart.findUnique({
where: { id: cart.id },
include: cartInclude,
});
},
createMyDeliveryAddress: async (_, { input }, context) => {
const user = requireUser(context);
const payload = toDeliveryAddressInputData(input);