Files
order_site/apps/backend/src/repositories/order-item-repo.js
T

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)
}