Add per-item order pricing
This commit is contained in:
2
prisma/migrations/0008_order_item_pricing/migration.sql
Normal file
2
prisma/migrations/0008_order_item_pricing/migration.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE "OrderItem"
|
||||
ADD COLUMN "unitPrice" DECIMAL(14, 2);
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
|
||||
@@ -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,19 +1369,75 @@ 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({
|
||||
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',
|
||||
deliveryTerms: input.deliveryTerms,
|
||||
deliveryFee: input.deliveryFee,
|
||||
totalPrice: input.totalPrice,
|
||||
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');
|
||||
await notifyOrderStakeholders(context, order, 'WAITING_DOUBLE_CONFIRM', '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: {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user