refactor: simplify order status model — remove DELIVERY_FEE_ADJUSTMENT and PAYMENT_VERIFICATION

- Add deliveryFeeLocked field to Order model
- Remove DELIVERY_FEE_ADJUSTMENT and PAYMENT_VERIFICATION statuses (11→8)
- 3 order paths: delivery+online (locked→unlocked→paid), pickup+online (unlocked→paid), pickup+on_pickup (direct to in_progress)
- Update checkout to use PENDING_PAYMENT + deliveryFeeLocked
- Update payment flow to stay in PENDING_PAYMENT until admin confirms
- Update admin UI to use deliveryFeeLocked instead of status check
- Update client payment UI with new deliveryFeeLocked logic
This commit is contained in:
Kirill
2026-05-15 21:55:14 +05:00
parent 2db6258b33
commit f855568687
18 changed files with 1170 additions and 135 deletions
@@ -17,6 +17,14 @@ describe('canTransitionAdminOrderStatus', () => {
expect(canTransitionAdminOrderStatus({ status: 'DRAFT', ...delivery }, 'PAID')).toBe(false)
})
it('PENDING_PAYMENT → PAID', () => {
expect(canTransitionAdminOrderStatus({ status: 'PENDING_PAYMENT', ...delivery }, 'PAID')).toBe(true)
})
it('PENDING_PAYMENT → CANCELLED', () => {
expect(canTransitionAdminOrderStatus({ status: 'PENDING_PAYMENT', ...delivery }, 'CANCELLED')).toBe(true)
})
it('PAID → IN_PROGRESS', () => {
expect(canTransitionAdminOrderStatus({ status: 'PAID', ...delivery }, 'IN_PROGRESS')).toBe(true)
})
-4
View File
@@ -12,11 +12,7 @@ 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':
return next === 'PAID' || next === 'CANCELLED'
case 'PAID':
return next === 'IN_PROGRESS' || next === 'CANCELLED'
+4 -4
View File
@@ -8,7 +8,7 @@ export async function registerAdminOrderRoutes(fastify) {
async () => {
const attentionCount = await prisma.order.count({
where: {
status: { in: ['DELIVERY_FEE_ADJUSTMENT', 'PENDING_PAYMENT', 'PAYMENT_VERIFICATION'] },
status: 'PENDING_PAYMENT',
},
})
return { attentionCount }
@@ -126,8 +126,8 @@ export async function registerAdminOrderRoutes(fastify) {
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' })
if (existing.status !== 'PENDING_PAYMENT' || existing.deliveryFeeLocked !== false) {
return reply.code(409).send({ error: 'Корректировка доставки доступна только пока стоимость не утверждена' })
}
const totalCents = existing.itemsSubtotalCents + parsed
@@ -136,7 +136,7 @@ export async function registerAdminOrderRoutes(fastify) {
data: {
deliveryFeeCents: parsed,
totalCents,
status: 'PENDING_PAYMENT',
deliveryFeeLocked: true,
},
})
return { item: updated }
+8 -2
View File
@@ -101,8 +101,13 @@ export async function registerUserOrderRoutes(fastify) {
})
let initialStatus = 'PENDING_PAYMENT'
if (paymentMethod === 'on_pickup') initialStatus = 'IN_PROGRESS'
else if (deliveryType === 'delivery') initialStatus = 'DELIVERY_FEE_ADJUSTMENT'
let deliveryFeeLocked = true
if (paymentMethod === 'on_pickup') {
initialStatus = 'IN_PROGRESS'
} else if (deliveryType === 'delivery') {
initialStatus = 'PENDING_PAYMENT'
deliveryFeeLocked = false
}
let created
try {
@@ -124,6 +129,7 @@ export async function registerUserOrderRoutes(fastify) {
data: {
userId,
status: initialStatus,
deliveryFeeLocked,
deliveryType,
deliveryCarrier,
paymentMethod,
+78 -99
View File
@@ -18,115 +18,94 @@ export async function registerUserPaymentRoutes(fastify) {
return reply.code(409).send({ error: 'Для этого заказа оплата при получении — кнопка оплаты не нужна.' })
}
if (order.status === 'DELIVERY_FEE_ADJUSTMENT') {
if (order.status !== 'PENDING_PAYMENT') {
return reply.code(409).send({ error: 'Сейчас нельзя выполнить оплату для этого заказа' })
}
if (!request.isMultipart()) {
return reply
.code(409)
.send({
error:
'Оплата станет доступна после корректировки стоимости доставки администратором.',
})
.code(400)
.send({ error: 'Отправьте multipart/form-data: поле detail и/или файл receipt' })
}
let nextStatus = order.status
if (order.status === 'DRAFT') {
await prisma.order.update({ where: { id }, data: { status: 'PENDING_PAYMENT' } })
nextStatus = 'PENDING_PAYMENT'
return { ok: true, status: nextStatus }
}
if (order.status === 'PAYMENT_VERIFICATION') {
return { ok: true, status: nextStatus }
}
if (order.status === 'PENDING_PAYMENT') {
if (!request.isMultipart()) {
return reply
.code(400)
.send({ error: 'Отправьте multipart/form-data: поле detail и/или файл receipt' })
let detail = ''
let receiptBuffer = null
let receiptFilename = ''
try {
const otherLimit = getOtherUploadMaxFileBytes()
const parts = request.parts({
limits: {
fileSize: otherLimit,
files: 2,
},
})
for await (const part of parts) {
if (part.file) {
if (part.fieldname === 'receipt') {
if (receiptBuffer !== null) {
return reply.code(400).send({ error: 'Допускается один файл receipt' })
}
receiptBuffer = await part.toBuffer()
receiptFilename = part.filename ?? 'receipt'
}
} else if (part.fieldname === 'detail') {
detail = String(part.value ?? '').trim()
}
}
} catch (err) {
const msg = err instanceof Error ? err.message : 'Не удалось разобрать форму'
return reply.code(400).send({ error: msg })
}
let detail = ''
let receiptBuffer = null
let receiptFilename = ''
const hasDetail = detail.length > 0
const hasReceipt = receiptBuffer !== null && receiptBuffer.length > 0
if (!hasDetail && !hasReceipt) {
return reply
.code(400)
.send({ error: 'Укажите текст о платеже и/или прикрепите изображение чека' })
}
const maxDetail = 2000
if (detail.length > maxDetail) {
return reply.code(400).send({ error: `Текст не длиннее ${maxDetail} символов` })
}
let attachmentUrl = null
if (hasReceipt) {
try {
const otherLimit = getOtherUploadMaxFileBytes()
const parts = request.parts({
limits: {
fileSize: otherLimit,
files: 2,
attachmentUrl = await saveImageBufferToUploads(receiptFilename, receiptBuffer)
} catch (err) {
const message = err instanceof Error ? err.message : 'Не удалось сохранить файл'
const statusCode =
err && typeof err === 'object' && 'statusCode' in err && Number.isInteger(err.statusCode)
? Number(err.statusCode)
: 400
return reply.code(statusCode).send({ error: message })
}
}
const bodyHtml = hasDetail
? `<p>${escapeHtml(detail).replace(/\r\n|\n|\r/g, '<br/>')}</p>`
: ''
const messageText = `<p><strong>Подтверждение оплаты (перевод ВТБ / Сбербанк)</strong></p>${bodyHtml}`
try {
await prisma.$transaction(async (tx) => {
await tx.orderMessage.create({
data: {
orderId: id,
authorType: 'user',
text: messageText,
attachmentUrl,
},
})
for await (const part of parts) {
if (part.file) {
if (part.fieldname === 'receipt') {
if (receiptBuffer !== null) {
return reply.code(400).send({ error: 'Допускается один файл receipt' })
}
receiptBuffer = await part.toBuffer()
receiptFilename = part.filename ?? 'receipt'
}
} else if (part.fieldname === 'detail') {
detail = String(part.value ?? '').trim()
}
}
} catch (err) {
const msg = err instanceof Error ? err.message : 'Не удалось разобрать форму'
return reply.code(400).send({ error: msg })
}
const hasDetail = detail.length > 0
const hasReceipt = receiptBuffer !== null && receiptBuffer.length > 0
if (!hasDetail && !hasReceipt) {
return reply
.code(400)
.send({ error: 'Укажите текст о платеже и/или прикрепите изображение чека' })
}
const maxDetail = 2000
if (detail.length > maxDetail) {
return reply.code(400).send({ error: `Текст не длиннее ${maxDetail} символов` })
}
let attachmentUrl = null
if (hasReceipt) {
try {
attachmentUrl = await saveImageBufferToUploads(receiptFilename, receiptBuffer)
} catch (err) {
const message = err instanceof Error ? err.message : 'Не удалось сохранить файл'
const statusCode =
err && typeof err === 'object' && 'statusCode' in err && Number.isInteger(err.statusCode)
? Number(err.statusCode)
: 400
return reply.code(statusCode).send({ error: message })
}
}
const bodyHtml = hasDetail
? `<p>${escapeHtml(detail).replace(/\r\n|\n|\r/g, '<br/>')}</p>`
: ''
const messageText = `<p><strong>Подтверждение оплаты (перевод ВТБ / Сбербанк)</strong></p>${bodyHtml}`
try {
await prisma.$transaction(async (tx) => {
await tx.order.update({ where: { id }, data: { status: 'PAYMENT_VERIFICATION' } })
await tx.orderMessage.create({
data: {
orderId: id,
authorType: 'user',
text: messageText,
attachmentUrl,
},
})
})
} catch (err) {
return reply.code(500).send({ error: 'Не удалось сохранить оплату' })
}
return { ok: true, status: 'PAYMENT_VERIFICATION' }
})
} catch (err) {
return reply.code(500).send({ error: 'Не удалось сохранить оплату' })
}
return reply.code(409).send({ error: 'Сейчас нельзя выполнить оплату для этого заказа' })
return { ok: true, status: 'PENDING_PAYMENT' }
},
)
}