This commit is contained in:
@kirill.komarov
2026-05-11 15:14:35 +05:00
parent 20096c1eec
commit 4eda6d0f81
20 changed files with 299 additions and 56 deletions
-20
View File
@@ -1,20 +0,0 @@
DATABASE_URL="file:./dev.db"
ADMIN_EMAIL=admin@example.com
# Default code for login
DEFAULT_CODE=123456
IS_DEFAULT_CODE_ENABLED=true
PORT=3333
# Токен для админ-запросов с фронта: Authorization: Bearer <значение>
ADMIN_API_TOKEN=dev-secret-change-me
# JWT для пользовательской авторизации (замени в проде)
JWT_SECRET=dev-jwt-secret-change-me
# Опционально: список origin для CORS через запятую (в dev можно не задавать)
# CORS_ORIGIN=http://localhost:5173
# SMTP для отправки кода (если не задано — код логируется в консоль как [DEV])
# SMTP_HOST=smtp.example.com
# SMTP_PORT=587
# SMTP_SECURE=false
# SMTP_USER=user@example.com
# SMTP_PASS=password
# MAIL_FROM="Craftshop <no-reply@example.com>"
+1 -2
View File
@@ -4,10 +4,9 @@
"private": true,
"type": "module",
"scripts": {
"dev": "node --env-file=.dev_env --watch src/index.js",
"dev": "node --env-file=.env --watch src/index.js",
"dev:classic": "node --watch src/index.js",
"start": "node src/index.js",
"start:dev_env": "node --env-file=.dev_env src/index.js",
"db:migrate": "prisma migrate dev",
"db:migrate:test": "node --env-file=.dev_env ./node_modules/prisma/build/index.js migrate deploy",
"db:seed": "prisma db seed",
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Order" ADD COLUMN "deliveryCarrier" TEXT;
+2
View File
@@ -116,6 +116,8 @@ model Order {
status String @default("DRAFT")
/// 'delivery' | 'pickup'
deliveryType String @default("delivery")
/// RUSSIAN_POST | OZON_PVZ | YANDEX_PVZ | FIVE_POST при deliveryType=delivery
deliveryCarrier String?
/// 'online' | 'on_pickup' — способ расчёта для заказа
paymentMethod String @default("online")
itemsSubtotalCents Int @default(0)
+9
View File
@@ -0,0 +1,9 @@
export const DELIVERY_CARRIERS = ['RUSSIAN_POST', 'OZON_PVZ', 'YANDEX_PVZ', 'FIVE_POST']
/**
* @param {unknown} value
* @returns {value is typeof DELIVERY_CARRIERS[number]}
*/
export function isDeliveryCarrier(value) {
return typeof value === 'string' && DELIVERY_CARRIERS.includes(value)
}
+3
View File
@@ -1,5 +1,6 @@
export const ORDER_STATUSES = [
'DRAFT',
'DELIVERY_FEE_ADJUSTMENT',
'PENDING_PAYMENT',
'PAYMENT_VERIFICATION',
'PAID',
@@ -22,6 +23,8 @@ export function canTransitionAdminOrderStatus(order, next) {
switch (from) {
case 'DRAFT':
return next === 'PENDING_PAYMENT' || next === 'CANCELLED'
case 'DELIVERY_FEE_ADJUSTMENT':
return next === 'CANCELLED'
case 'PENDING_PAYMENT':
return next === 'CANCELLED'
case 'PAYMENT_VERIFICATION':
+35 -1
View File
@@ -7,7 +7,9 @@ export async function registerAdminOrderRoutes(fastify) {
{ preHandler: [fastify.verifyAdmin] },
async () => {
const attentionCount = await prisma.order.count({
where: { status: { in: ['PENDING_PAYMENT', 'PAYMENT_VERIFICATION'] } },
where: {
status: { in: ['DELIVERY_FEE_ADJUSTMENT', 'PENDING_PAYMENT', 'PAYMENT_VERIFICATION'] },
},
})
return { attentionCount }
},
@@ -57,6 +59,7 @@ export async function registerAdminOrderRoutes(fastify) {
id: o.id,
status: o.status,
deliveryType: o.deliveryType,
deliveryCarrier: o.deliveryCarrier,
paymentMethod: o.paymentMethod,
totalCents: o.totalCents,
currency: o.currency,
@@ -109,6 +112,37 @@ export async function registerAdminOrderRoutes(fastify) {
},
)
fastify.patch(
'/api/admin/orders/:id/delivery-fee',
{ preHandler: [fastify.verifyAdmin] },
async (request, reply) => {
const { id } = request.params
const feeRaw = request.body?.deliveryFeeCents
const parsed =
typeof feeRaw === 'string' ? Number.parseInt(feeRaw, 10) : typeof feeRaw === 'number' ? feeRaw : NaN
if (!Number.isInteger(parsed) || parsed < 0) {
return reply.code(400).send({ error: 'deliveryFeeCents должно быть целым числом ≥ 0 (копейки)' })
}
const existing = await prisma.order.findUnique({ where: { id } })
if (!existing) return reply.code(404).send({ error: 'Заказ не найден' })
if (existing.status !== 'DELIVERY_FEE_ADJUSTMENT') {
return reply.code(409).send({ error: 'Корректировка доставки доступна только в статусе DELIVERY_FEE_ADJUSTMENT' })
}
const totalCents = existing.itemsSubtotalCents + parsed
const updated = await prisma.order.update({
where: { id },
data: {
deliveryFeeCents: parsed,
totalCents,
status: 'PENDING_PAYMENT',
},
})
return { item: updated }
},
)
fastify.post(
'/api/admin/orders/:id/messages',
{ preHandler: [fastify.verifyAdmin] },
+33 -4
View File
@@ -1,4 +1,5 @@
import { issueEmailCode, normalizeEmail, verifyEmailCode } from '../lib/auth.js'
import { isDeliveryCarrier } from '../lib/delivery-carrier.js'
import { escapeHtml } from '../lib/escape-html.js'
import { prisma } from '../lib/prisma.js'
import { getOtherUploadMaxFileBytes } from '../lib/upload-limits.js'
@@ -436,6 +437,24 @@ export async function registerAuthRoutes(fastify) {
return reply.code(400).send({ error: 'deliveryType должен быть delivery | pickup' })
}
const carrierRaw = request.body?.deliveryCarrier
let deliveryCarrier = null
if (deliveryType === 'delivery') {
const carrierStr =
carrierRaw === undefined || carrierRaw === null || carrierRaw === ''
? ''
: String(carrierRaw).trim()
if (!isDeliveryCarrier(carrierStr)) {
return reply
.code(400)
.send({
error:
'deliveryCarrier обязателен для доставки: RUSSIAN_POST | OZON_PVZ | YANDEX_PVZ | FIVE_POST',
})
}
deliveryCarrier = carrierStr
}
if (paymentMethod === 'on_pickup' && deliveryType !== 'pickup') {
return reply.code(400).send({ error: 'Оплата при получении доступна только для самовывоза' })
}
@@ -468,9 +487,7 @@ export async function registerAuthRoutes(fastify) {
}))
const itemsSubtotalCents = itemsPayload.reduce((sum, i) => sum + i.priceCentsSnapshot * i.qty, 0)
const totalQty = itemsPayload.reduce((sum, i) => sum + i.qty, 0)
const deliveryFeeCents =
deliveryType === 'delivery' ? 50000 * Math.max(1, Math.ceil(totalQty / 2)) : 0
const deliveryFeeCents = deliveryType === 'delivery' ? 50000 : 0
const totalCents = itemsSubtotalCents + deliveryFeeCents
const addressSnapshotJson =
@@ -488,7 +505,9 @@ export async function registerAuthRoutes(fastify) {
lng: address.lng,
})
const initialStatus = paymentMethod === 'on_pickup' ? 'IN_PROGRESS' : 'PENDING_PAYMENT'
let initialStatus = 'PENDING_PAYMENT'
if (paymentMethod === 'on_pickup') initialStatus = 'IN_PROGRESS'
else if (deliveryType === 'delivery') initialStatus = 'DELIVERY_FEE_ADJUSTMENT'
let created
try {
@@ -511,6 +530,7 @@ export async function registerAuthRoutes(fastify) {
userId,
status: initialStatus,
deliveryType,
deliveryCarrier,
paymentMethod,
itemsSubtotalCents,
deliveryFeeCents,
@@ -703,6 +723,15 @@ export async function registerAuthRoutes(fastify) {
return reply.code(409).send({ error: 'Для этого заказа оплата при получении — кнопка оплаты не нужна.' })
}
if (order.status === 'DELIVERY_FEE_ADJUSTMENT') {
return reply
.code(409)
.send({
error:
'Оплата станет доступна после корректировки стоимости доставки администратором.',
})
}
let nextStatus = order.status
if (order.status === 'DRAFT') {
await prisma.order.update({ where: { id }, data: { status: 'PENDING_PAYMENT' } })
Binary file not shown.

After

Width:  |  Height:  |  Size: 393 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 321 KiB