Add per-item order pricing

This commit is contained in:
Ruslan Bakiev
2026-04-04 11:16:16 +07:00
parent 4281afd7e8
commit 3abebf3701
4 changed files with 86 additions and 10 deletions

View File

@@ -0,0 +1,2 @@
ALTER TABLE "OrderItem"
ADD COLUMN "unitPrice" DECIMAL(14, 2);

View File

@@ -273,6 +273,7 @@ model OrderItem {
product Product? @relation(fields: [productId], references: [id])
productName String
quantity Decimal @db.Decimal(14, 3)
unitPrice Decimal? @db.Decimal(14, 2)
createdAt DateTime @default(now())
}

View File

@@ -25,6 +25,10 @@ function toFloat(value) {
return value == null ? null : Number(value);
}
function roundMoney(value) {
return Math.round((Number(value) + Number.EPSILON) * 100) / 100;
}
function requireUser(context) {
if (!context.user) {
throw new Error('Authentication required.');
@@ -1365,18 +1369,74 @@ export const resolvers = {
const manager = requireManagerAccess(context);
const existingOrder = await context.prisma.order.findUnique({
where: { id: input.orderId },
include: {
items: true,
},
});
assertManagerCanAccessOrder(existingOrder);
const order = await context.prisma.order.update({
where: { id: input.orderId },
data: {
managerId: manager.id,
status: 'WAITING_DOUBLE_CONFIRM',
deliveryTerms: input.deliveryTerms,
deliveryFee: input.deliveryFee,
totalPrice: input.totalPrice,
},
if (!existingOrder.items.length) {
throw new Error('Order has no items to price.');
}
const deliveryFee = Number(input.deliveryFee);
if (!Number.isFinite(deliveryFee) || deliveryFee < 0) {
throw new Error('Delivery fee must be zero or greater.');
}
const orderItemIds = new Set(existingOrder.items.map((item) => item.id));
const itemPriceMap = new Map();
for (const itemPrice of input.itemPrices) {
if (itemPriceMap.has(itemPrice.itemId)) {
throw new Error('Duplicate item pricing entries are not allowed.');
}
if (!orderItemIds.has(itemPrice.itemId)) {
throw new Error('Pricing can only be set for items from this order.');
}
const unitPrice = Number(itemPrice.unitPrice);
if (!Number.isFinite(unitPrice) || unitPrice < 0) {
throw new Error('Unit price must be zero or greater.');
}
itemPriceMap.set(itemPrice.itemId, roundMoney(unitPrice));
}
if (itemPriceMap.size !== existingOrder.items.length) {
throw new Error('Pricing must be provided for every order item.');
}
const totalProductsPrice = existingOrder.items.reduce(
(sum, item) => sum + (Number(item.quantity) * itemPriceMap.get(item.id)),
0,
);
const totalPrice = roundMoney(totalProductsPrice + deliveryFee);
const order = await context.prisma.$transaction(async (tx) => {
for (const item of existingOrder.items) {
await tx.orderItem.update({
where: { id: item.id },
data: {
unitPrice: itemPriceMap.get(item.id),
},
});
}
return tx.order.update({
where: { id: input.orderId },
data: {
managerId: manager.id,
status: 'WAITING_DOUBLE_CONFIRM',
clientApproved: null,
managerApproved: null,
blockReason: null,
deliveryTerms: normalizeOptionalText(input.deliveryTerms),
deliveryFee: roundMoney(deliveryFee),
totalPrice,
},
});
});
await appendOrderEvent(context.prisma, order.id, 'WAITING_DOUBLE_CONFIRM', manager.id, 'Offer is published by manager');
@@ -1636,6 +1696,12 @@ export const resolvers = {
OrderItem: {
quantity: (item) => toFloat(item.quantity),
unitPrice: (item) => toFloat(item.unitPrice),
lineTotal: (item) => (
item.unitPrice == null
? null
: roundMoney(Number(item.quantity) * Number(item.unitPrice))
),
},
BonusTransaction: {

View File

@@ -232,6 +232,8 @@ type OrderItem {
productId: ID
productName: String!
quantity: Float!
unitPrice: Float
lineTotal: Float
}
type OrderStatusEvent {
@@ -428,9 +430,14 @@ input SubmitCalculationOrderInput {
input SetOrderOfferInput {
orderId: ID!
itemPrices: [OrderItemPriceInput!]!
deliveryTerms: String!
deliveryFee: Float!
totalPrice: Float!
}
input OrderItemPriceInput {
itemId: ID!
unitPrice: Float!
}
input BlockOrderInput {