import { prisma } from '../lib/prisma.js' function normalizePhoneLite(input) { const s = String(input || '').trim() if (!s) return '' return s.replace(/[\s()-]/g, '') } function validateAddressPayload(body, reply) { const labelRaw = body?.label const label = labelRaw === null || labelRaw === undefined ? null : String(labelRaw).trim() if (label !== null && label.length > 40) return reply.code(400).send({ error: 'Метка адреса максимум 40 символов' }) const recipientName = String(body?.recipientName || '').trim() if (!recipientName) return reply.code(400).send({ error: 'Укажите ФИО получателя' }) if (recipientName.length > 80) return reply.code(400).send({ error: 'ФИО получателя максимум 80 символов' }) const recipientPhone = normalizePhoneLite(body?.recipientPhone) if (!recipientPhone) return reply.code(400).send({ error: 'Укажите телефон получателя' }) if (!/^\+?\d{7,20}$/.test(recipientPhone)) return reply.code(400).send({ error: 'Некорректный телефон получателя' }) const addressLine = String(body?.addressLine || '').trim() if (!addressLine) return reply.code(400).send({ error: 'Укажите адрес' }) if (addressLine.length > 200) return reply.code(400).send({ error: 'Адрес максимум 200 символов' }) const commentRaw = body?.comment const comment = commentRaw === null || commentRaw === undefined ? null : String(commentRaw).trim() if (comment !== null && comment.length > 200) return reply.code(400).send({ error: 'Комментарий максимум 200 символов' }) const lat = Number(body?.lat) const lng = Number(body?.lng) if (!Number.isFinite(lat) || lat < -90 || lat > 90) return reply.code(400).send({ error: 'Некорректная широта' }) if (!Number.isFinite(lng) || lng < -180 || lng > 180) return reply.code(400).send({ error: 'Некорректная долгота' }) return { label, recipientName, recipientPhone, addressLine, comment, lat, lng, } } export async function registerUserAddressRoutes(fastify) { fastify.get('/api/me/addresses', { preHandler: [fastify.authenticate] }, async (request, reply) => { try { const userId = request.user.sub const items = await prisma.shippingAddress.findMany({ where: { userId }, orderBy: [{ isDefault: 'desc' }, { updatedAt: 'desc' }], }) return { items } } catch (err) { request.log.error(err) return reply.code(500).send({ error: 'Не удалось загрузить адреса' }) } }) fastify.post('/api/me/addresses', { preHandler: [fastify.authenticate] }, async (request, reply) => { try { const userId = request.user.sub const validated = validateAddressPayload(request.body, reply) if (!validated) return const isDefault = Boolean(request.body?.isDefault) const created = await prisma.$transaction(async (tx) => { if (isDefault) { await tx.shippingAddress.updateMany({ where: { userId, isDefault: true }, data: { isDefault: false } }) } return tx.shippingAddress.create({ data: { userId, ...validated, isDefault, }, }) }) return reply.code(201).send({ item: created }) } catch (err) { request.log.error(err) return reply.code(500).send({ error: 'Не удалось создать адрес' }) } }) fastify.patch('/api/me/addresses/:id', { preHandler: [fastify.authenticate] }, async (request, reply) => { try { const userId = request.user.sub const { id } = request.params const existing = await prisma.shippingAddress.findFirst({ where: { id, userId } }) if (!existing) return reply.code(404).send({ error: 'Адрес не найден' }) const body = request.body ?? {} const data = {} if (body.label !== undefined) { const labelRaw = body.label const label = labelRaw === null || labelRaw === undefined ? null : String(labelRaw).trim() if (label !== null && label.length > 40) return reply.code(400).send({ error: 'Метка адреса максимум 40 символов' }) data.label = label && label.length ? label : null } if (body.recipientName !== undefined) { const v = String(body.recipientName || '').trim() if (!v) return reply.code(400).send({ error: 'Укажите ФИО получателя' }) if (v.length > 80) return reply.code(400).send({ error: 'ФИО получателя максимум 80 символов' }) data.recipientName = v } if (body.recipientPhone !== undefined) { const v = normalizePhoneLite(body.recipientPhone) if (!v) return reply.code(400).send({ error: 'Укажите телефон получателя' }) if (!/^\+?\d{7,20}$/.test(v)) return reply.code(400).send({ error: 'Некорректный телефон получателя' }) data.recipientPhone = v } if (body.addressLine !== undefined) { const v = String(body.addressLine || '').trim() if (!v) return reply.code(400).send({ error: 'Укажите адрес' }) if (v.length > 200) return reply.code(400).send({ error: 'Адрес максимум 200 символов' }) data.addressLine = v } if (body.comment !== undefined) { const commentRaw = body.comment const comment = commentRaw === null || commentRaw === undefined ? null : String(commentRaw).trim() if (comment !== null && comment.length > 200) return reply.code(400).send({ error: 'Комментарий максимум 200 символов' }) data.comment = comment && comment.length ? comment : null } if (body.lat !== undefined) { const lat = Number(body.lat) if (!Number.isFinite(lat) || lat < -90 || lat > 90) return reply.code(400).send({ error: 'Некорректная широта' }) data.lat = lat } if (body.lng !== undefined) { const lng = Number(body.lng) if (!Number.isFinite(lng) || lng < -180 || lng > 180) return reply.code(400).send({ error: 'Некорректная долгота' }) data.lng = lng } const setDefault = body.isDefault === true const updated = await prisma.$transaction(async (tx) => { if (setDefault) { await tx.shippingAddress.updateMany({ where: { userId, isDefault: true }, data: { isDefault: false } }) } return tx.shippingAddress.update({ where: { id }, data: { ...data, ...(setDefault ? { isDefault: true } : {}), }, }) }) return { item: updated } } catch (err) { request.log.error(err) return reply.code(500).send({ error: 'Не удалось обновить адрес' }) } }) fastify.delete('/api/me/addresses/:id', { preHandler: [fastify.authenticate] }, async (request, reply) => { try { const userId = request.user.sub const { id } = request.params const existing = await prisma.shippingAddress.findFirst({ where: { id, userId } }) if (!existing) return reply.code(404).send({ error: 'Адрес не найден' }) await prisma.shippingAddress.delete({ where: { id } }) return reply.code(204).send() } catch (err) { request.log.error(err) return reply.code(500).send({ error: 'Не удалось удалить адрес' }) } }) fastify.post('/api/me/addresses/:id/default', { preHandler: [fastify.authenticate] }, async (request, reply) => { try { const userId = request.user.sub const { id } = request.params const existing = await prisma.shippingAddress.findFirst({ where: { id, userId } }) if (!existing) return reply.code(404).send({ error: 'Адрес не найден' }) const updated = await prisma.$transaction(async (tx) => { await tx.shippingAddress.updateMany({ where: { userId, isDefault: true }, data: { isDefault: false } }) return tx.shippingAddress.update({ where: { id }, data: { isDefault: true } }) }) return { item: updated } } catch (err) { request.log.error(err) return reply.code(500).send({ error: 'Не удалось установить адрес по умолчанию' }) } }) }