188 lines
5.1 KiB
JavaScript
188 lines
5.1 KiB
JavaScript
// @ts-check
|
|
|
|
import { query, withTransaction } from '../db/client.js'
|
|
|
|
/** @typedef {import('../types/repository-inputs.js').OrderItemReplaceInput} OrderItemReplaceInput */
|
|
/** @typedef {import('../types/repository-rows.js').OrderItemRow} OrderItemRow */
|
|
|
|
/** @returns {Promise<OrderItemRow[]>} */
|
|
export async function listOrderItemsByOrderId(orderId) {
|
|
return listOrderItemsByOrderIdWithExecutor(query, orderId)
|
|
}
|
|
|
|
/** @returns {Promise<OrderItemRow[]>} */
|
|
/** @param {OrderItemReplaceInput[]} items */
|
|
export async function replaceOrderItems(orderId, items) {
|
|
return withTransaction(async (client) => {
|
|
const executor = client.query.bind(client)
|
|
const existingItems = await listOrderItemsByOrderIdWithExecutor(executor, orderId)
|
|
const plan = resolveOrderItemSyncPlan(existingItems, items)
|
|
|
|
for (const update of plan.updates) {
|
|
await executor(
|
|
`
|
|
UPDATE order_items
|
|
SET
|
|
sku_code = $1,
|
|
sku_name = $2,
|
|
quantity = $3,
|
|
spec_json = $4::jsonb,
|
|
item_snapshot_json = $5::jsonb,
|
|
updated_at = $6
|
|
WHERE id = $7
|
|
`,
|
|
[
|
|
update.item.skuCode,
|
|
update.item.skuName,
|
|
update.item.quantity,
|
|
update.item.specJson || '{}',
|
|
update.item.itemSnapshotJson || update.item.specJson || '{}',
|
|
update.item.updatedAt,
|
|
update.orderItemId,
|
|
],
|
|
)
|
|
}
|
|
|
|
for (const create of plan.creates) {
|
|
await executor(
|
|
`
|
|
INSERT INTO order_items (
|
|
order_id,
|
|
sku_code,
|
|
sku_name,
|
|
quantity,
|
|
spec_json,
|
|
item_snapshot_json,
|
|
created_at,
|
|
updated_at
|
|
) VALUES ($1, $2, $3, $4, $5::jsonb, $6::jsonb, $7, $8)
|
|
`,
|
|
[
|
|
Number(orderId),
|
|
create.skuCode,
|
|
create.skuName,
|
|
create.quantity,
|
|
create.specJson || '{}',
|
|
create.itemSnapshotJson || create.specJson || '{}',
|
|
create.createdAt,
|
|
create.updatedAt,
|
|
],
|
|
)
|
|
}
|
|
|
|
if (plan.deletes.length > 0) {
|
|
const deletableIds = await listDeletableOrderItemIdsWithExecutor(executor, plan.deletes)
|
|
|
|
if (deletableIds.length > 0) {
|
|
await executor(
|
|
'DELETE FROM order_items WHERE id = ANY($1::bigint[])',
|
|
[deletableIds],
|
|
)
|
|
}
|
|
}
|
|
|
|
return listOrderItemsByOrderIdWithExecutor(executor, orderId)
|
|
})
|
|
}
|
|
|
|
/**
|
|
* @param {OrderItemRow[]} existingItems
|
|
* @param {OrderItemReplaceInput[]} nextItems
|
|
*/
|
|
export function resolveOrderItemSyncPlan(existingItems, nextItems) {
|
|
const normalizedExisting = Array.isArray(existingItems) ? existingItems : []
|
|
const normalizedNext = Array.isArray(nextItems) ? nextItems : []
|
|
const unmatchedExisting = [...normalizedExisting]
|
|
const updates = []
|
|
const creates = []
|
|
|
|
for (const item of normalizedNext) {
|
|
const matchedIndex = findMatchingExistingOrderItemIndex(unmatchedExisting, item)
|
|
|
|
if (matchedIndex >= 0) {
|
|
const matched = unmatchedExisting.splice(matchedIndex, 1)[0]
|
|
updates.push({
|
|
orderItemId: matched.id,
|
|
item,
|
|
})
|
|
continue
|
|
}
|
|
|
|
if (unmatchedExisting.length > 0) {
|
|
const matched = unmatchedExisting.shift()
|
|
updates.push({
|
|
orderItemId: matched.id,
|
|
item,
|
|
})
|
|
continue
|
|
}
|
|
|
|
creates.push(item)
|
|
}
|
|
|
|
return {
|
|
updates,
|
|
creates,
|
|
deletes: unmatchedExisting.map((item) => Number(item.id)).filter((value) => value > 0),
|
|
}
|
|
}
|
|
|
|
function findMatchingExistingOrderItemIndex(existingItems, nextItem) {
|
|
const nextIdentity = buildOrderItemIdentity(nextItem)
|
|
|
|
return existingItems.findIndex((item) => buildOrderItemIdentity(item) === nextIdentity)
|
|
}
|
|
|
|
function buildOrderItemIdentity(item) {
|
|
return [
|
|
String(item?.skuCode ?? item?.sku_code ?? '').trim(),
|
|
String(item?.skuName ?? item?.sku_name ?? '').trim(),
|
|
String(Math.max(1, Number(item?.quantity || 1))),
|
|
].join('::')
|
|
}
|
|
|
|
async function listDeletableOrderItemIdsWithExecutor(executor, orderItemIds) {
|
|
const normalizedIds = Array.isArray(orderItemIds)
|
|
? orderItemIds.map((value) => Number(value)).filter((value) => value > 0)
|
|
: []
|
|
|
|
if (normalizedIds.length === 0) {
|
|
return []
|
|
}
|
|
|
|
const result = await executor(
|
|
`
|
|
SELECT oi.id
|
|
FROM order_items oi
|
|
LEFT JOIN fulfillment_tasks ft ON ft.order_item_id = oi.id
|
|
WHERE oi.id = ANY($1::bigint[])
|
|
GROUP BY oi.id
|
|
HAVING COUNT(ft.id) = 0
|
|
ORDER BY oi.id ASC
|
|
`,
|
|
[normalizedIds],
|
|
)
|
|
|
|
return result.rows.map((row) => Number(row.id)).filter((value) => value > 0)
|
|
}
|
|
|
|
async function listOrderItemsByOrderIdWithExecutor(executor, orderId) {
|
|
const result = await executor(
|
|
`
|
|
SELECT *
|
|
FROM order_items
|
|
WHERE order_id = $1
|
|
ORDER BY id ASC
|
|
`,
|
|
[Number(orderId)],
|
|
)
|
|
|
|
return /** @type {OrderItemRow[]} */ (result.rows)
|
|
}
|
|
|
|
/** @returns {Promise<OrderItemRow | null>} */
|
|
export async function getOrderItemById(orderItemId) {
|
|
const result = await query('SELECT * FROM order_items WHERE id = $1 LIMIT 1', [Number(orderItemId)])
|
|
return /** @type {OrderItemRow | null} */ (result.rows[0] || null)
|
|
}
|