Files
order_site/apps/backend/src/services/order/cloudtentacles-override-rule-service.ts
T

186 lines
5.4 KiB
TypeScript

import path from 'node:path'
import type { JsonObject } from '../../types/json.js'
import { APP_CONFIG_KEYS } from '../../config/app-config-keys.js'
import { PROJECT_ROOT } from '../../config/runtime.js'
import { readAppConfigEntry, saveAppConfigEntry } from '../config/app-config-store.js'
import { normalizeCloudtentaclesMatchName } from './cloudtentacles-match-utils.js'
const CLOUDTENTACLES_OVERRIDE_RULE_FILE_PATH = path.join(
PROJECT_ROOT,
'data',
'cloudtentacles-override-rules.json',
)
export type CloudtentaclesOverrideDeliveryItem = {
cloudSkuId: number
cloudSkuName: string
quantity: number
}
export type CloudtentaclesOverrideRule = {
id: string
enabled: boolean
productName: string
normalizedProductName: string
sourceKey: string
deliveryItems: CloudtentaclesOverrideDeliveryItem[]
notes: string
}
export type CloudtentaclesOverrideRuleConfig = {
enabled: boolean
rules: CloudtentaclesOverrideRule[]
}
export function getCloudtentaclesOverrideRuleFilePath() {
return CLOUDTENTACLES_OVERRIDE_RULE_FILE_PATH
}
export function getCloudtentaclesOverrideRuleConfig(): CloudtentaclesOverrideRuleConfig {
return readAppConfigEntry({
configKey: APP_CONFIG_KEYS.cloudtentaclesOverrideRules,
legacyFilePath: CLOUDTENTACLES_OVERRIDE_RULE_FILE_PATH,
fallback: createDefaultCloudtentaclesOverrideRuleConfig,
normalize: normalizeCloudtentaclesOverrideRuleConfig,
})
}
export function saveCloudtentaclesOverrideRuleConfig(
rawValue: unknown,
): Promise<CloudtentaclesOverrideRuleConfig> {
return saveAppConfigEntry({
configKey: APP_CONFIG_KEYS.cloudtentaclesOverrideRules,
legacyFilePath: CLOUDTENTACLES_OVERRIDE_RULE_FILE_PATH,
value: rawValue,
normalize: normalizeCloudtentaclesOverrideRuleConfig,
})
}
export function resolveCloudtentaclesOverrideRule(productName: unknown) {
const config = getCloudtentaclesOverrideRuleConfig()
if (config.enabled === false) {
return null
}
const normalizedProductName = normalizeCloudtentaclesMatchName(productName)
if (!normalizedProductName) {
return null
}
return (
config.rules.find(
(rule) =>
rule.enabled !== false &&
rule.normalizedProductName === normalizedProductName &&
rule.deliveryItems.length > 0,
) || null
)
}
export function normalizeCloudtentaclesOverrideRuleConfig(
rawValue: unknown,
): CloudtentaclesOverrideRuleConfig {
const source = isPlainObject(rawValue) ? rawValue : {}
const rules = Array.isArray(source.rules) ? source.rules : []
return {
enabled: source.enabled !== false,
rules: rules
.map((item: unknown) => normalizeCloudtentaclesOverrideRule(item))
.filter((item): item is CloudtentaclesOverrideRule => Boolean(item)),
}
}
function normalizeCloudtentaclesOverrideRule(rawValue: unknown): CloudtentaclesOverrideRule | null {
if (!isPlainObject(rawValue)) {
return null
}
const productName = String(rawValue.productName || rawValue.name || '').trim()
const normalizedProductName = normalizeCloudtentaclesMatchName(productName)
const deliveryItems = normalizeDeliveryItems(rawValue.deliveryItems)
if (!productName || !normalizedProductName || deliveryItems.length === 0) {
return null
}
return {
id: String(rawValue.id || normalizedProductName).trim() || normalizedProductName,
enabled: rawValue.enabled !== false,
productName,
normalizedProductName,
sourceKey: String(rawValue.sourceKey || '').trim(),
deliveryItems,
notes: String(rawValue.notes || '').trim(),
}
}
function normalizeDeliveryItems(value: unknown) {
const rawItems = Array.isArray(value) ? value : []
const items = rawItems
.map((item) => normalizeDeliveryItem(item))
.filter((item): item is CloudtentaclesOverrideDeliveryItem => Boolean(item))
return mergeDeliveryItems(items)
}
function normalizeDeliveryItem(value: unknown): CloudtentaclesOverrideDeliveryItem | null {
const source = isPlainObject(value) ? value : {}
const cloudSkuId = normalizePositiveInteger(source.cloudSkuId || source.skuId, 0)
const cloudSkuName = String(source.cloudSkuName || source.skuName || '').trim()
const quantity = normalizePositiveInteger(source.quantity, 1)
if (!cloudSkuId && !cloudSkuName) {
return null
}
return {
cloudSkuId,
cloudSkuName,
quantity,
}
}
function mergeDeliveryItems(items: CloudtentaclesOverrideDeliveryItem[]) {
const merged = new Map<string, CloudtentaclesOverrideDeliveryItem>()
for (const item of items) {
const key =
item.cloudSkuId > 0
? `id:${item.cloudSkuId}`
: `name:${normalizeCloudtentaclesMatchName(item.cloudSkuName)}`
const existing = merged.get(key)
if (existing) {
existing.quantity += item.quantity
existing.cloudSkuName = existing.cloudSkuName || item.cloudSkuName
existing.cloudSkuId = existing.cloudSkuId || item.cloudSkuId
continue
}
merged.set(key, { ...item })
}
return Array.from(merged.values())
}
function normalizePositiveInteger(value: unknown, fallback: number) {
const parsed = Number(value)
if (!Number.isInteger(parsed) || parsed <= 0) {
return fallback
}
return parsed
}
function createDefaultCloudtentaclesOverrideRuleConfig(): CloudtentaclesOverrideRuleConfig {
return {
enabled: true,
rules: [],
}
}
function isPlainObject(value: unknown): value is JsonObject {
return Object.prototype.toString.call(value) === '[object Object]'
}