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])
|
product Product? @relation(fields: [productId], references: [id])
|
||||||
productName String
|
productName String
|
||||||
quantity Decimal @db.Decimal(14, 3)
|
quantity Decimal @db.Decimal(14, 3)
|
||||||
|
unitPrice Decimal? @db.Decimal(14, 2)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ function toFloat(value) {
|
|||||||
return value == null ? null : Number(value);
|
return value == null ? null : Number(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function roundMoney(value) {
|
||||||
|
return Math.round((Number(value) + Number.EPSILON) * 100) / 100;
|
||||||
|
}
|
||||||
|
|
||||||
function requireUser(context) {
|
function requireUser(context) {
|
||||||
if (!context.user) {
|
if (!context.user) {
|
||||||
throw new Error('Authentication required.');
|
throw new Error('Authentication required.');
|
||||||
@@ -1365,18 +1369,74 @@ export const resolvers = {
|
|||||||
const manager = requireManagerAccess(context);
|
const manager = requireManagerAccess(context);
|
||||||
const existingOrder = await context.prisma.order.findUnique({
|
const existingOrder = await context.prisma.order.findUnique({
|
||||||
where: { id: input.orderId },
|
where: { id: input.orderId },
|
||||||
|
include: {
|
||||||
|
items: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
assertManagerCanAccessOrder(existingOrder);
|
assertManagerCanAccessOrder(existingOrder);
|
||||||
|
|
||||||
const order = await context.prisma.order.update({
|
if (!existingOrder.items.length) {
|
||||||
where: { id: input.orderId },
|
throw new Error('Order has no items to price.');
|
||||||
data: {
|
}
|
||||||
managerId: manager.id,
|
|
||||||
status: 'WAITING_DOUBLE_CONFIRM',
|
const deliveryFee = Number(input.deliveryFee);
|
||||||
deliveryTerms: input.deliveryTerms,
|
if (!Number.isFinite(deliveryFee) || deliveryFee < 0) {
|
||||||
deliveryFee: input.deliveryFee,
|
throw new Error('Delivery fee must be zero or greater.');
|
||||||
totalPrice: input.totalPrice,
|
}
|
||||||
},
|
|
||||||
|
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');
|
await appendOrderEvent(context.prisma, order.id, 'WAITING_DOUBLE_CONFIRM', manager.id, 'Offer is published by manager');
|
||||||
@@ -1636,6 +1696,12 @@ export const resolvers = {
|
|||||||
|
|
||||||
OrderItem: {
|
OrderItem: {
|
||||||
quantity: (item) => toFloat(item.quantity),
|
quantity: (item) => toFloat(item.quantity),
|
||||||
|
unitPrice: (item) => toFloat(item.unitPrice),
|
||||||
|
lineTotal: (item) => (
|
||||||
|
item.unitPrice == null
|
||||||
|
? null
|
||||||
|
: roundMoney(Number(item.quantity) * Number(item.unitPrice))
|
||||||
|
),
|
||||||
},
|
},
|
||||||
|
|
||||||
BonusTransaction: {
|
BonusTransaction: {
|
||||||
|
|||||||
@@ -232,6 +232,8 @@ type OrderItem {
|
|||||||
productId: ID
|
productId: ID
|
||||||
productName: String!
|
productName: String!
|
||||||
quantity: Float!
|
quantity: Float!
|
||||||
|
unitPrice: Float
|
||||||
|
lineTotal: Float
|
||||||
}
|
}
|
||||||
|
|
||||||
type OrderStatusEvent {
|
type OrderStatusEvent {
|
||||||
@@ -428,9 +430,14 @@ input SubmitCalculationOrderInput {
|
|||||||
|
|
||||||
input SetOrderOfferInput {
|
input SetOrderOfferInput {
|
||||||
orderId: ID!
|
orderId: ID!
|
||||||
|
itemPrices: [OrderItemPriceInput!]!
|
||||||
deliveryTerms: String!
|
deliveryTerms: String!
|
||||||
deliveryFee: Float!
|
deliveryFee: Float!
|
||||||
totalPrice: Float!
|
}
|
||||||
|
|
||||||
|
input OrderItemPriceInput {
|
||||||
|
itemId: ID!
|
||||||
|
unitPrice: Float!
|
||||||
}
|
}
|
||||||
|
|
||||||
input BlockOrderInput {
|
input BlockOrderInput {
|
||||||
|
|||||||
Reference in New Issue
Block a user