Restore Africa supplier and product catalog
Build and deploy Docker image / build (push) Successful in 4m0s

This commit is contained in:
Ruslan Bakiev
2026-08-20 17:23:31 +07:00
parent 60831b9f8c
commit 5ab16917a1
6 changed files with 1909 additions and 2 deletions
+187
View File
@@ -0,0 +1,187 @@
import { createHash } from 'node:crypto'
import { readFileSync } from 'node:fs'
import { Prisma } from '@prisma/client'
import { prisma } from '../dist/db.js'
type SupplierSeed = {
lei: string
name: string
countryCode: string
city: string
}
type HubSeed = {
code: string
name: string
countryCode: string
}
type ProductSeed = {
name: string
hsCodes: string[]
label: string
}
const countryNames = new Intl.DisplayNames(['en'], { type: 'region' })
const productImages = [
'https://images.unsplash.com/photo-1625246333195-78d9c38ad449?auto=format&fit=crop&w=900&q=80',
'https://images.unsplash.com/photo-1574323347407-f5e1ad6d020b?auto=format&fit=crop&w=900&q=80',
'https://images.unsplash.com/photo-1592924357228-91a4daadcfea?auto=format&fit=crop&w=900&q=80',
]
function parseCsvLine(line: string): string[] {
const fields: string[] = []
let field = ''
let quoted = false
for (let index = 0; index < line.length; index += 1) {
const character = line[index]
if (character === '"') {
if (quoted && line[index + 1] === '"') {
field += '"'
index += 1
} else {
quoted = !quoted
}
} else if (character === ',' && !quoted) {
fields.push(field)
field = ''
} else {
field += character
}
}
if (quoted) throw new Error(`Unclosed CSV quote: ${line}`)
fields.push(field)
return fields
}
function stableUuid(namespace: string, value: string): string {
const hex = createHash('sha256').update(`${namespace}:${value}`).digest('hex')
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-4${hex.slice(13, 16)}-8${hex.slice(17, 20)}-${hex.slice(20, 32)}`
}
function normalize(value: string): string {
return value.normalize('NFKD').replace(/[^a-zA-Z0-9]/g, '').toLowerCase()
}
function readSuppliers(): SupplierSeed[] {
const value = JSON.parse(
readFileSync(new URL('../data/africa_suppliers.json', import.meta.url), 'utf8'),
) as SupplierSeed[]
for (const supplier of value) {
if (!supplier.lei || !supplier.name || supplier.countryCode.length !== 2) {
throw new Error(`Invalid Africa supplier reference: ${JSON.stringify(supplier)}`)
}
}
return value
}
function readHubs(): HubSeed[] {
const csv = readFileSync(
new URL('../data/unlocode_africa_road_terminals.csv', import.meta.url),
'utf8',
)
return csv.trim().split(/\r?\n/).slice(1).map((line) => {
const [code, name, countryCode] = parseCsvLine(line)
if (!code || !name || countryCode.length !== 2) throw new Error(`Invalid hub reference: ${line}`)
return { code, name, countryCode }
})
}
function readProducts(): ProductSeed[] {
const csv = readFileSync(new URL('../data/africa_products.csv', import.meta.url), 'utf8')
const grouped = new Map<string, ProductSeed>()
for (const line of csv.trim().split(/\r?\n/).slice(1)) {
const [name, hsCode, label] = parseCsvLine(line)
if (!name || !/^\d{6}$/.test(hsCode) || !label) throw new Error(`Invalid product reference: ${line}`)
const current = grouped.get(name)
if (current === undefined) grouped.set(name, { name, hsCodes: [hsCode], label })
else current.hsCodes.push(hsCode)
}
return [...grouped.values()]
}
function supplierHub(supplier: SupplierSeed, hubs: HubSeed[]): HubSeed {
const countryHubs = hubs.filter((hub) => hub.countryCode === supplier.countryCode)
if (countryHubs.length === 0) throw new Error(`No Africa hub for ${supplier.countryCode}`)
const city = normalize(supplier.city)
const cityHub = city.length > 0
? countryHubs.find((hub) => normalize(hub.name).includes(city) || city.includes(normalize(hub.name)))
: undefined
return cityHub ?? countryHubs[0]
}
const suppliers = readSuppliers()
const hubs = readHubs()
const products = readProducts()
for (const [index, product] of products.entries()) {
const uuid = stableUuid('africa-product', product.name)
const data = {
sku: `HS-${product.hsCodes[0]}`,
name: product.name,
categoryName: 'African commodities',
unit: 'ton',
imageUrl: productImages[index % productImages.length],
description: `${product.label}. HS codes: ${product.hsCodes.join(', ')}.`,
isActive: true,
}
await prisma.product.upsert({ where: { uuid }, create: { uuid, ...data }, update: data })
}
for (const supplier of suppliers) {
const uuid = stableUuid('africa-supplier', supplier.lei)
const country = countryNames.of(supplier.countryCode)
if (country === undefined) throw new Error(`Unknown country code: ${supplier.countryCode}`)
const data = {
teamUuid: null,
name: supplier.name,
description: `African supplier reference from GLEIF (${supplier.lei}), ${supplier.city}.`,
country,
countryCode: supplier.countryCode,
logoUrl: `https://ui-avatars.com/api/?name=${encodeURIComponent(supplier.name)}&background=0B6BFF&color=fff&bold=true`,
isVerified: false,
isActive: true,
}
await prisma.supplier.upsert({ where: { uuid }, create: { uuid, ...data }, update: data })
}
const storedProducts = await prisma.product.findMany({
where: { uuid: { in: products.map((product) => stableUuid('africa-product', product.name)) } },
orderBy: { name: 'asc' },
})
const storedSuppliers = await prisma.supplier.findMany({
where: { uuid: { in: suppliers.map((supplier) => stableUuid('africa-supplier', supplier.lei)) } },
orderBy: { name: 'asc' },
})
let quoteCount = 0
for (const [supplierIndex, supplier] of suppliers.entries()) {
const storedSupplier = storedSuppliers.find((item) => item.uuid === stableUuid('africa-supplier', supplier.lei))
if (storedSupplier === undefined) throw new Error(`Seeded supplier is missing: ${supplier.lei}`)
const hub = supplierHub(supplier, hubs)
for (let productOffset = 0; productOffset < 2; productOffset += 1) {
const productSeed = products[(supplierIndex * 2 + productOffset) % products.length]
const storedProduct = storedProducts.find((item) => item.uuid === stableUuid('africa-product', productSeed.name))
if (storedProduct === undefined) throw new Error(`Seeded product is missing: ${productSeed.name}`)
const uuid = stableUuid('africa-quote', `${supplier.lei}:${productSeed.name}`)
const data = {
supplierId: storedSupplier.id,
productId: storedProduct.id,
status: 'active',
quantity: new Prisma.Decimal(250 + (supplierIndex % 12) * 50 + productOffset * 25),
unit: storedProduct.unit,
pricePerUnit: new Prisma.Decimal(700 + (supplierIndex % 10) * 65 + productOffset * 40),
currency: 'USD',
incotermsCode: productOffset === 0 ? 'FOB' : 'DAP',
originPointUuid: `africa-${hub.code.toLowerCase()}`,
originName: hub.name,
validUntil: new Date('2028-12-31'),
notes: 'Africa reference catalog',
}
await prisma.quote.upsert({ where: { uuid }, create: { uuid, ...data }, update: data })
quoteCount += 1
}
}
console.log(`Seeded Africa exchange reference data: suppliers ${suppliers.length}, products ${products.length}, quotes ${quoteCount}`)
await prisma.$disconnect()