Rename EMAILBRO → ASPEKTER, update Coin Scout, security fixes

- Rename: Docker containers, UI, nginx, User-Agent strings
- Coin Scout: sync from COIN_SCOUT project (latest version)
- Security: Pug injection protection (validatePugSafety)
- Security: concurrent render fix (unique temp files)
- Fix: disappearing IDs input when cleared
- Audit logging: all mutations, login/logout
- Users: createdBy/updatedBy on letters
- Local image storage option
This commit is contained in:
2026-04-13 12:39:25 +05:00
parent 718821fdd6
commit d34f04e922
5 changed files with 1115 additions and 236 deletions

View File

@@ -1,8 +1,11 @@
// ─── Coin Writer v5: literary + factual balance ───
// ─── Coin Writer v8: literary + concise ───
let gradeScore = () => -1
function setGradeScore(fn) { gradeScore = fn }
let getSilverPrice = () => 200
function setSilverPrice(fn) { getSilverPrice = fn }
function hash(str) {
let h = 0
for (let i = 0; i < str.length; i++) h = ((h << 5) - h + str.charCodeAt(i)) | 0
@@ -13,73 +16,77 @@ function q(name) { return `«${name}»` }
const PERIODS = [
{ re: /николай\s*ii|николая\s*ii/i, texts: [
'Последний российский император — его монеты неизменно возглавляют аукционные продажи.',
'Эпоха Николая II: Транссибирская магистраль, первые автомобили — и монеты, ставшие символом ушедшей империи.',
'Монеты Николая II коллекционируются по всему миру. Спрос не ослабевает десятилетиями.',
'Закат Российской империи. Его монеты — одни из самых собираемых в русской нумизматике.',
'Закат империи, эпоха золотого стандарта и Транссибирской магистрали. Монеты Николая II неизменно лидируют на аукционах русской нумизматики — от Москвы до Нью-Йорка.',
'Последний император. Его монеты — витрина русской нумизматики: безупречная чеканка, прозрачная аукционная история, спрос, не ослабевающий десятилетиями.',
'Николай II — самый коллекционируемый период Империи. Каталог Биткина знает каждый штемпель, аукционы отслеживают каждую продажу.',
]},
{ re: /пётр|петр\s*i|петра\s*i/i, from: 1682, to: 1725, texts: [
'Пётр Великий перекроил Россию — от алфавита до монетной системы.',
'Царь, построивший новую столицу на болотах. Его монеты — среди самых желанных.',
'Реформатор, открывший России путь в Европу. Коллекционный спрос стабильно высокий.',
'Пётр Великий перекроил Россию — от алфавита до монетной системы. Реформа 1700 года дала стране копейку, гривенник, полтинник и рубль. Его монеты — фундамент любой серьёзной коллекции.',
'Царь, построивший новую столицу на болотах и новую монетную систему с нуля. Среди самых желанных монет отечественной нумизматики.',
]},
{ re: /екатерин/i, from: 1762, to: 1796, texts: [
'При Екатерине II территория империи выросла на 500 тыс. кв. км. Монеты этого периода — классика.',
'Золотой век. 34 года правления и богатое нумизматическое наследие.',
'Золотой век Российской империи. При Екатерине II территория выросла на 500 тыс. кв. км, а монетное искусство достигло расцвета — изящные портретные типы, совершенная чеканка.',
]},
{ re: /александр\s*ii/i, from: 1855, to: 1881, texts: [
'Царь-Освободитель: отмена крепостного права, судебная реформа. Спрос на его монеты растёт.',
'Царь-Освободитель: отмена крепостного права, земская реформа, продажа Аляски. Монеты его эпохи переживают ренессанс коллекционного спроса.',
]},
{ re: /александр\s*iii/i, from: 1881, to: 1894, texts: [
'13 лет на троне. Единственный император, при котором Россия не вела ни одной войны. Монет — немного.',
'Тринадцать мирных лет на тронеединственный император, при котором Россия не вела ни одной войны. Короткое правление — ограниченная чеканка, каждый экземпляр на счету.',
]},
{ re: /павел/i, from: 1796, to: 1801, texts: [
'5 лет правления Павла I — и ограниченная чеканка. Каждый экземпляр нечаст.',
'Всего пять лет правления Павла I. Монеты с его характерным вензелем нечасты и неизменно востребованы.',
]},
{ re: /анна.*иоанн/i, from: 1730, to: 1740, texts: [
'Анна Иоанновна — эпоха дворцовых интриг. Монеты с особой притягательностью.',
'Анна Иоанновна — эпоха дворцовых переворотов и бироновщины. Монеты с её портретом притягивают ценителей XVIII века.',
]},
{ re: /елизавет/i, from: 1741, to: 1762, texts: [
'Дочь Петра Великого. При Елизавете расцвело монетное дело — изысканная чеканка, редкие тиражи.',
'Дочь Петра Великого. При Елизавете расцвело монетное искусство — утончённые портреты, безупречная чеканка, редкие тиражи.',
]},
{ re: /финлянд/i, texts: [
'Русская Финляндия — обособленная серия. Монеты чеканились для Великого княжества и обращались только на его территории.',
'Монеты Великого княжества Финляндского — компактная серия с преданным кругом ценителей.',
'Монеты Великого княжества Финляндского — обособленная серия, чеканившаяся для территории, где ходили свои номиналы: пенни и марки с портретами российских императоров. Компактная серия с преданным кругом ценителей.',
]},
{ re: /римск|roman|денарий|антониниан/i, texts: [
'Рим: легионы, акведуки, Колизей — и монеты, пережившие саму империю. Глобальный рынок растёт на 8-12% в год.',
'Монета цивилизации, построившей дороги, по которым ходят до сих пор.',
'Рим: легионы, акведуки, Колизей — и монеты, пережившие империю на два тысячелетия. Каждый денарий — исторический документ, побывавший в руках людей, о которых мы читаем в учебниках.',
'Монета цивилизации, построившей дороги, по которым ходят до сих пор. Мировой рынок античной нумизматики растёт на 812% ежегодно.',
]},
{ re: /греч|greek|драхм|обол|тетра/i, texts: [
'Древняя Греция: Сократ, Олимпийские игры и первые в истории монеты с портретами. Рынок растёт до 15% ежегодно.',
'Древняя Греция — родина монетного дела. Совы Афин, тетрадрахмы Александра Македонского: спрос на эти монеты не ослабевает тысячелетиями.',
]},
{ re: /боспор|пантикапей/i, texts: [
'Боспорское царство — античный Крым. Монеты Пантикапея — растущая ниша для ценителей причерноморской истории.',
'Боспорское царство — античный Крым, перекрёсток эллинского и скифского миров. Монеты Пантикапея с головой сатира узнаются с первого взгляда.',
]},
{ re: /визант|byzant/i, texts: [
'Византия просуществовала тысячу лет — дольше любой европейской империи. Доступная и перспективная античность.',
'Византия простояла тысячу лет — дольше любой европейской империи. Её солиды были мировой резервной валютой раннего Средневековья.',
]},
{ re: /осман|ottoman/i, texts: [
'Османская империя раскинулась от Дуная до Аравии. Пока недооценённый, но быстро растущий сегмент.',
]},
{ re: /сефевид|safavid/i, texts: [
'Сефевидская Персия — империя шахов, объединившая Иран. Монеты этой династии встречаются всё реже.',
'Монеты с тугрой — каллиграфическим вензелем султана — обладают особой, ни на что не похожей эстетикой. Пока недооценённый, но быстро растущий сегмент.',
]},
{ re: /смутн|владислав|лжедмитри/i, texts: [
'Смутное время — самые драматичные годы русской истории. Эти монеты — нумизматическая элита.',
'Между царствами, между эпохами. Монеты Смутного времени доступны единицам.',
]},
{ re: /медный бунт/i, texts: [
'Медный бунт 1662 года: народное восстание против обесценивания денег. Монеты-свидетели этих событий крайне редки.',
'Смутное время — самые драматичные годы русской истории. Шведская оккупация, польский королевич на русском троне. Монеты этого периода — нумизматическая элита.',
]},
{ re: /сибирск|сузун/i, texts: [
'Сибирские монеты чеканились из меди Колывано-Воскресенских заводов с примесью серебра и золота. Ходили только за Уралом.',
'Сибирская монета чеканилась из колыванской меди с природной примесью серебра и золота. Ходила только за Уралом. Обособленная серия с растущим коллекционным спросом.',
]},
{ re: /георгий победоносец/i, texts: [
'«Георгий Победоносец» — самая ликвидная монета из драгметаллов в России. Принимается в любом банке.',
'«Георгий Победоносец» — самая ликвидная монета из драгметаллов в России. Принимается в любом банке, узнаётся с первого взгляда.',
]},
{ re: /1921|1922|1923|1924|1925|1926|1927|1928|1929|1930|1931/i, materialRe: /серебро|silver/, texts: [
'Раннее СССР серебро: последний период серебра в обращении. Большинство изъято и переплавлено в 1930-хуцелевшие экземпляры особенно ценны.',
'Ранний СССР последний период, когда драгоценный металл ещё ходил в обращении. В 1930-х основной тираж изъяли и переплавили. Уцелевшие экземпляры встречаются всё реже.',
]},
{ re: /1947/i, texts: [
'1947 год — одна из загадок советской нумизматики. Монеты отчеканены, но в обращение почти не поступили и были уничтожены. Каждый уцелевший экземпляр — настоящая редкость.',
]},
{ re: /1958/i, texts: [
'1958 — монеты несостоявшейся денежной реформы. Почти весь тираж уничтожен. Находка для серьёзного коллекционера.',
]},
{ re: /полтин/i, materialRe: /серебро|silver/, texts: [
'Полтинник — крупный серебряный номинал, один из самых ликвидных в русской нумизматике. Коллекционеры собирают по годам, разновидностям штемпелей и минцмейстерам.',
]},
{ re: /рубль|рубл/i, materialRe: /серебро|silver/, from: 1700, to: 1917, texts: [
'Серебряный рубль Российской империи — «королевский» номинал русской нумизматики. Крупный модуль, детальные портреты, высокое содержание серебра. Стабильный аукционный спрос.',
]},
{ re: /талер|thaler|taler/i, texts: [
'Талер — прародитель доллара, крупная серебряная монета, ходившая по всей Европе. Ликвиден в любой точке мира.',
]},
]
@@ -87,225 +94,147 @@ function generateCoinEmail(coin, details, score) {
const name = coin.name || ''
const mat = (details.material || '').toLowerCase()
const year = details.year_from || 0
const age = year ? 2026 - year : 0
const age = year > 0 ? 2026 - year : 0
const price = coin.price || 0
const grade = details.grade || ''
const gs = gradeScore(grade)
const weightG = parseFloat(details.weight) || 0
const diameterMm = parseFloat(details.diameter) || 0
const isSilver = /серебро|silver/.test(mat)
const isGold = /золото|gold/.test(mat)
const isPrecious = isSilver || isGold
const h = hash(coin.id || name)
const totalAge = year < 0 ? Math.abs(year) + 2026 : age
const metalWord = isGold ? 'золото' : isSilver ? 'серебро' : ''
const metalAdj = isGold ? 'золотая' : isSilver ? 'серебряная' : ''
const p1 = []
const p2 = []
const p3 = []
const parts = []
// ══════════ P1: HOOK — литературно + факт ══════════
// ══════════ АБЗАЦ 1: КРЮЧОК — образ + исторический контекст ══════════
const hook = []
if (totalAge > 2000) {
p1.push(pick([
`${q(name)}. Этой монете более ${totalAge} лет — она старше большинства государств на карте мира. Когда её чеканили, ещё существовала Римская империя.`,
`Представьте: ${totalAge} лет назад кто-то расплатился этой монетой на рыночной площади древнего города. ${q(name)} — подлинный артефакт, переживший тысячелетия.`,
`${q(name)}. ${totalAge} лет истории в одном небольшом кружке металла. Монета, к которой прикасались люди из совершенно другого мира.`,
], h))
} else if (totalAge > 400) {
p1.push(pick([
`${q(name)}${totalAge} лет истории. Монета, которая пережила империи, войны и революции, и дошла до наших дней.`,
`${totalAge} лет назад мастер ударил штемпелем по заготовке — так появилась ${q(name)}. С тех пор мир изменился до неузнаваемости.`,
`${q(name)}. ${totalAge} лет — и монета по-прежнему существует. Каждая её потёртость — след чьей-то жизни, давно забытой историей.`,
if (age > 400) {
hook.push(pick([
`${q(name)}. Этой монете ${age} лет — она старше большинства государств на современной карте. Когда её чеканили, мастер бил штемпелем по раскалённой заготовке вручную, и каждый экземпляр получался неповторимым.`,
`${age} лет назад кто-то расплатился этой монетой. С тех пор рухнули империи, сменились языки и границы — а ${q(name)} дошла до наших дней.`,
`${q(name)} ${age} лет в одном кружке металла. Каждая потёртость на ней — след чьей-то жизни, давно забытой историей.`,
], h))
} else if (age > 200) {
p1.push(pick([
`${q(name)} — больше ${Math.floor(age / 100)} столетий истории. Монета из мира без электричества и фотографии, дошедшая до наших дней${gs >= gradeScore('VF') ? ' в достойном состоянии' : ''}.`,
`${q(name)}. ${age} лет назад она была частью чьей-то повседневности. Сегодня — коллекционная ценность.`,
`Больше ${Math.floor(age / 100)} веков назад эта монета зазвенела впервые. ${q(name)} — осколок эпохи, которую мы знаем лишь по книгам.`,
hook.push(pick([
`${q(name)}. ${year} год — мир без электричества и фотографии. Эта монета помнит то, чего уже не помнит никто из живущих.`,
`Больше двух столетий назад ${q(name)} впервые зазвенела на прилавке. Сегодня она — осколок эпохи, которую мы знаем лишь по книгам.`,
`${year} год. ${q(name)} — монета, пережившая больше двух столетий${gs >= gradeScore('XF') ? ' и дошедшая до нас в прекрасном состоянии' : ''}.`,
], h))
} else if (age > 100) {
p1.push(pick([
`${q(name)}. ${year} год — мир на пороге грандиозных перемен.${isPrecious ? ` ${isGold ? 'Золотая' : 'Серебряная'} монета, ценная и как металл, и как предмет коллекционирования.` : ''}`,
`${q(name)} — больше века истории. Монета, отчеканенная когда мир был совсем другим.${isPrecious ? ` ${isGold ? 'Золото' : 'Серебро'}.` : ''}`,
hook.push(pick([
`${q(name)}. ${year} год — мир на пороге перемен, которые изменят всё.`,
`${year} год. ${q(name)} — больше века истории. Монета из совершенно другого мира.`,
], h))
} else {
p1.push(`${q(name)}.${year ? ` ${year} год.` : ''}${isPrecious ? ` ${isGold ? 'Золотая' : 'Серебряная'} монета.` : ''}`)
hook.push(`${q(name)}.${year ? ` ${year} год.` : ''}`)
}
// Period context
// Исторический контекст
for (const period of PERIODS) {
if (!period.re.test(name)) continue
if (period.from && year && (year < period.from || year > period.to)) continue
if (period.materialRe && !period.materialRe.test(mat)) continue
p1.push(pick(period.texts, h + 7))
hook.push(pick(period.texts, h + 7))
break
}
// ══════════ CONTENT BLOCKS — independent, shuffleable ══════════
const blocks = []
parts.push(hook.join(' '))
// Block: Grade
// ══════════ АБЗАЦ 2: ТЕЛО — грейд + материал + одна деталь ══════════
const body = []
// Грейд — образно, но точно
if (gs >= gradeScore('Proof')) {
blocks.push(pick([
'Качество Proof — зеркальная поверхность, матовый рельеф. Безупречна в каждой детали.',
'Proof-чекан: полированные штемпели, идеальная заготовка — совершенство линий.',
'Proof — высшая категория качества. Зеркальное поле, матовый рельеф.',
body.push(pick([
'Качество Proof — зеркальное поле, матовый рельеф. Совершенство, созданное для ценителей, а не для кошелька.',
'Proof-чекан: полированные штемпели, безупречная заготовка. Каждая линия рисунка передана с фотографической точностью.',
], h + 10))
} else if (gs >= gradeScore('UNC')) {
blocks.push(pick([
`Сохранность UNC — не была в обращении. Полный штемпельный блеск.${age > 50 ? ` Для ${age}-летней монеты — редкость.` : ''}`,
`UNC — как в день чеканки.${age > 100 ? ` ${age} лет и ни следа износа.` : ' Ни единого следа времени.'}`,
`Не была в обращении (UNC). Оригинальный блеск, безупречный рельеф.${age > 50 ? ` Через ${age} лет — впечатляет.` : ''}`,
body.push(pick([
`UNC — монета не была в обращении. Полный штемпельный блеск, нетронутый рельеф.${age > 100 ? ` Через ${age} лет — это почти чудо: значит, кто-то берёг её с самого начала.` : ''}`,
`Сохранность UNC — как в день чеканки.${age > 50 ? ` ${age} лет, и ни единого следа износа. Такое не часто встретишь.` : ' Оригинальный штемпельный блеск.'}`,
], h + 10))
} else if (gs >= gradeScore('AU')) {
blocks.push(pick([
`AU — почти идеальное состояние. Едва заметные следы на выступающих точках.${age > 100 ? ` Для ${age}-летней монеты — отличный результат.` : ''}`,
`AU — на грани между обращением и совершенством.${age > 100 ? ` Через ${age} лет — это удача.` : ''}`,
`Almost Uncirculated. Минимальный износ.${age > 100 ? ` ${age} лет — и такое состояние.` : ' Блеск сохранён.'}`,
body.push(pick([
`AU — почти идеальное состояние. Едва заметные следы на самых выступающих точках рельефа, основной блеск сохранён.${age > 100 ? ` Для ${age}-летней монеты — отличный результат.` : ''}`,
`Almost Uncirculated — на грани между обращением и совершенством.${age > 100 ? ` ${age} лет — и такое состояние. Владельцы явно берегли эту монету.` : ' Минимальный износ, блеск на месте.'}`,
], h + 10))
} else if (gs >= gradeScore('XF')) {
blocks.push(pick([
`XF — чёткий рельеф, все детали выразительны.${age > 200 ? ` Для ${age}-летней монеты — впечатляюще. Большинство ровесников дошли в худшем виде.` : ''}`,
`Extremely Fine.${age > 200 ? ` ${age} лет а рисунок практически полный.` : ' Лёгкий износ, все элементы на месте.'}`,
`XF — высокая сохранность.${age > 200 ? ` Для монеты ${age}-летней давности — нечастый грейд.` : ''}`,
body.push(pick([
`XF — чёткий рельеф, все детали выразительны, все надписи читаются без труда.${age > 200 ? ` Для ${age}-летней монеты — впечатляюще. Большинство её ровесников дошли до нас в худшем виде.` : ''}`,
`Extremely Fine — высокая коллекционная сохранность.${age > 200 ? ` Монете ${age} лет, а рисунок практически полный. Нечастый результат.` : ' Лёгкий износ на выступающих частях, все элементы на месте.'}`,
], h + 10))
} else if (gs >= gradeScore('VF')) {
blocks.push(`Сохранность ${grade}.${age > 200 ? ` Для ${age}-летней монеты — достойно.` : ''}`)
body.push(`Сохранность ${grade} — все основные детали различимы, честный коллекционный экземпляр.${age > 200 ? ` Для ${age}-летней монеты — достойно.` : ''}`)
}
// Block: Material + melt
// Материал — конкретика, без лишних повторов слова «серебро»/«золото»
if (isSilver && weightG > 0 && price > 0) {
const melt = Math.round(weightG * 200)
const melt = Math.round(weightG * getSilverPrice())
const ratio = Math.round(melt / price * 100)
if (ratio > 100) {
blocks.push(pick([
`Серебро (${weightG}г) стоит ~${melt}₽ по курсу ЦБа монета продаётся дешевле. Такие аномалии встречаются нечасто.`,
`${weightG}г серебра${melt}₽. Стоимость металла превышает стоимость монеты.`,
`Серебра внутри на ${melt}₽ — дороже самой монеты. Арифметика на стороне покупателя.`,
body.push(pick([
`Внутри — ${weightG}г драгоценного металла на ${melt}₽ по курсу ЦБ. Монета стоит дешевле содержимого. Нумизматическую ценность вы получаете в подарок.`,
`${weightG}г при текущем курсе${melt}₽. Стоимость металла превышает цену монеты — нечастая ситуация.`,
], h + 12))
} else if (ratio > 60) {
blocks.push(pick([
`${weightG}г серебра (~${melt}₽) покрывают ${ratio}% стоимости. Надёжный фундамент.`,
`Серебро (${weightG}г, ~${melt}₽) — ${ratio}% от стоимости. Остальное — коллекционная премия.`,
], h + 12))
} else {
blocks.push(`Серебро, ${weightG}г. Драгоценный металл обеспечивает базовую поддержку стоимости.`)
body.push(`${weightG}г драгоценного металла (~${melt}₽) покрывают ${ratio}% стоимости — надёжный фундамент. Монета вряд ли подешевеет ниже стоимости содержимого.`)
} else if (weightG > 2) {
body.push(`${weightG}г${diameterMm ? `, Ø${diameterMm}мм` : ''}. Драгоценный металл обеспечивает ликвидность — такую монету всегда проще продать.`)
}
} else if (isSilver) {
blocks.push(pick([
'Серебряная монета — ценность, проверенная веками.',
'Серебро — классика нумизматики.',
'Серебро обеспечивает ликвидность — драгоценный металл всегда найдёт покупателя.',
], h + 12))
} else if (isGold && weightG > 0) {
body.push(`${weightG}г${diameterMm ? `, Ø${diameterMm}мм` : ''}. Двойная природа: нумизматический экземпляр и драгоценный металл. Одно усиливает другое.`)
} else if (isGold) {
blocks.push(pick([
'Золото — металл, который ценили во все времена и во всех цивилизациях.',
'Золотая монета — и артефакт, и ценность, проверенная тысячелетиями.',
'Золото не подвластно времени. Металл королей и императоров.',
], h + 12))
body.push('Драгоценный металл, не подвластный времени. Ценность, которая не нуждается в объяснениях.')
} else if (isSilver) {
body.push('Классика нумизматического собирательства. Благородная патина, приятная тактильность, стабильный спрос.')
}
// Block: Scarcity
// Одна деталь — тактильность, монетный двор или факт (не больше одной!)
const detailPool = []
if (age > 300 && !isGold && h % 3 === 0) detailPool.push(pick([
'Благородная патина подчёркивает рельеф — такую не подделать, она формируется десятилетиями.',
'Следы ручной чеканки делают каждый экземпляр уникальным — двух одинаковых не существует.',
], h + 18))
if (isSilver && weightG > 3 && h % 3 === 1) detailPool.push(pick([
`Приятная тяжесть ${weightG} граммов в ладони — ощущение, которое не передать фотографией.`,
`Характерный холод металла в руке${diameterMm ? `, кружок ${diameterMm}мм` : ''} — подлинность чувствуется на ощупь.`,
], h + 18))
if (isGold && h % 3 === 1) detailPool.push('Тёплый блеск, который не тускнеет веками. Особенная плотность ощущается сразу — не спутать ни с чем.')
if (/спб|петербург/i.test(name) && h % 3 === 2) detailPool.push('Чеканка Петербургского монетного двора — предприятия, основанного Петром I в 1724 году в Петропавловской крепости.')
if (/сузун/i.test(name) && h % 3 === 2) detailPool.push('Сузунский монетный двор (17631847) — единственное подобное предприятие за Уралом.')
if (isSilver && weightG > 0 && h % 3 === 2) detailPool.push(`Вес${weightG}г. В эпоху чеканки каждая монета проходила весовой контроль: отклонение от стандарта означало фальсификацию.`)
if (detailPool.length) body.push(detailPool[0])
if (body.length) parts.push(body.join(' '))
// ══════════ АБЗАЦ 3: ЗАКРЫТИЕ — почему сейчас ══════════
const closes = []
if (age > 200) {
blocks.push(pick([
`Предложение монет ${age > 300 ? 'этой эпохи' : 'этого периода'} сокращается — утраты и оседание в коллекциях делают каждый экземпляр ценнее.`,
`Число сохранившихся экземпляров может только уменьшаться. Через десять лет найти аналог будет сложнее.`,
`Каждый экземпляр, уходящий в коллекцию, сужает предложение. Процесс необратим.`,
closes.push(pick([
'Таких монет на рынке с каждым годом меньше — экземпляры уходят в постоянные коллекции и музейные фонды. Процесс необратим: их больше не чеканят.',
`Число доступных экземпляров ${age > 300 ? 'этой эпохи' : 'этого периода'} может только уменьшаться. То, что доступно сегодня, завтра может оказаться в чьей-то постоянной коллекции.`,
'Предложение сокращается с каждым годом — утраты, оседание в коллекциях, музейные фонды. Обратного хода нет.',
], h + 15))
} else if (age > 100) {
blocks.push(pick([
'Монет этого периода на рынке меньше с каждым десятилетием.',
'Доступных экземпляров всё меньше — их больше не чеканят.',
closes.push(pick([
'Столетних монет на рынке не прибавляется. С каждым десятилетием найти достойный экземпляр всё сложнее.',
'Их не чеканят уже больше века. Каждый экземпляр, ушедший в коллекцию, сужает предложение навсегда.',
], h + 15))
}
// Block: Collector appeal
if (isPrecious && gs >= gradeScore('VF')) {
blocks.push(pick([
'Драгоценный металл и хорошая сохранность — сочетание, которое ценилось во все времена.',
'Металл и состояние — два главных фактора в нумизматике. Оба на месте.',
'Сохранность и драгоценный металл — то, за чем охотятся коллекционеры.',
'Коллекционеры ценят прежде всего металл и состояние. Здесь — и то, и другое.',
], h + 16))
if (isPrecious && gs >= gradeScore('AU') && !closes.length) {
closes.push('Драгоценный металл в сочетании с высоким грейдом — формула, которая работает на протяжении всей истории нумизматики.')
}
// Block: Sensory
if (h % 3 === 0) {
if (age > 300 && !isGold) blocks.push(pick(['Благородная патина подтверждает подлинность и придаёт монете неповторимый характер.', 'Следы ручной чеканки — каждая такая монета уникальна, двух одинаковых не существует.'], h + 18))
else if (isSilver && weightG > 3) blocks.push(pick(['Приятная тяжесть настоящего серебра в ладони — ощущение, знакомое коллекционерам по всему миру.', 'Характерный холод серебра в руке. Подлинность чувствуется на ощупь.'], h + 18))
else if (isGold) blocks.push(pick(['Тёплый блеск золота, который не тускнеет веками.', 'Особая тяжесть золота в руке — ощущение, которое ни с чем не спутать.'], h + 18))
}
if (closes.length) parts.push(closes[0])
// Block: Collecting context — почему коллекционеры собирают именно такие
if (h % 3 === 1) {
if (age > 300) {
blocks.push(pick([
'Коллекционирование античных монет — одно из старейших хобби в мире. Им увлекались Петрарка, Медичи и российские императоры.',
'Античная нумизматика — область, где каждая монета является историческим документом. Надписи, портреты, символы рассказывают о правителях и событиях.',
'Каждая античная монета чеканилась вручную — поэтому двух абсолютно одинаковых не существует. Каждый экземпляр уникален.',
], h + 19))
} else if (year >= 1700 && year <= 1917) {
blocks.push(pick([
'Монеты Российской империи — одно из самых популярных направлений в отечественной нумизматике. Спрос стабилен и предсказуем.',
'Имперские монеты привлекают коллекционеров сочетанием доступности и исторической глубины. Многие серии можно собирать по годам, монетным дворам и разновидностям.',
'Русская нумизматика XVIII-XIX веков — направление с устоявшимся рынком, каталогами и аукционной историей. Риск приобрести подделку минимален при покупке у проверенных дилеров.',
], h + 19))
} else if (/серебро|silver/.test(mat)) {
blocks.push(pick([
'Серебряные монеты — один из самых ликвидных сегментов нумизматики. Их легко оценить, легко продать, легко хранить.',
'Коллекционирование серебряных монет совмещает эстетическое удовольствие и рациональный подход к сохранению ценности.',
], h + 19))
}
}
// Block: Technical facts — интересные факты о чеканке
if (h % 3 === 2) {
if (isSilver && weightG > 0) {
blocks.push(pick([
`Вес монеты — ${weightG}г. В эпоху чеканки вес строго контролировался: отклонение означало фальсификацию и каралось по закону.`,
`${weightG}г серебра. Монетная стопа — соотношение веса монеты к номиналу — тщательно регулировалась государством.`,
`Серебро ${weightG}г. Каждая монета проходила весовой контроль — это было гарантией доверия к денежной системе.`,
], h + 20))
} else if (isGold && weightG > 0) {
blocks.push(pick([
`Вес: ${weightG}г золота. Золотые монеты чеканились с особой точностью — даже минимальное отклонение веса было недопустимо.`,
`${weightG}г золота. Золотая монета — это не просто деньги, это государственная гарантия пробы и веса.`,
], h + 20))
} else if (age > 200) {
blocks.push(pick([
'В эпоху чеканки этой монеты каждый экземпляр проходил ручной контроль качества. Брак уничтожался и переплавлялся.',
'Монетное дело в ту эпоху было одной из важнейших государственных функций — от качества монеты зависело доверие к экономике.',
], h + 20))
}
}
// Block: Gift / collection starter
if (h % 5 === 0 && age > 50) {
blocks.push(pick([
'Такая монета может стать центральным экземпляром коллекции или запоминающимся подарком для ценителя истории.',
'Монета с историей — подарок, который не теряет значения с годами. Напротив — только набирает.',
'Отличный экземпляр для начала коллекции или пополнения существующей.',
], h + 21))
}
// Block: Provenance / geography
if (h % 5 === 1) {
if (/москв|moscow|ммд/i.test(name)) blocks.push('Отчеканена на Московском монетном дворе — одном из старейших в России.')
else if (/петербург|спб|спмд|ленинград/i.test(name)) blocks.push('Чеканка Санкт-Петербургского монетного двора — предприятия, основанного Петром I в 1724 году.')
else if (/рига|riga/i.test(name)) blocks.push('Рижский монетный двор чеканил монету на протяжении нескольких столетий для разных государств.')
else if (/сузун/i.test(name)) blocks.push('Сузунский монетный двор на Алтае работал с 1763 по 1847 год — единственное подобное предприятие за Уралом.')
}
// ══════════ SHUFFLE (grade stays first, rest randomized) ══════════
const first = blocks.shift() || ''
const rest = [...blocks]
for (let i = rest.length - 1; i > 0; i--) {
const j = (h + i * 13) % (i + 1)
;[rest[i], rest[j]] = [rest[j], rest[i]]
}
const body = [first, ...rest].filter(Boolean)
const mid = Math.ceil(body.length / 2)
return [p1.join(' '), body.slice(0, mid).join(' '), body.slice(mid).join(' ')].filter(p => p && p.trim()).join('\n\n')
return parts.filter(p => p && p.trim()).join('\n\n')
}
module.exports = { generateCoinEmail, setGradeScore }
module.exports = { generateCoinEmail, setGradeScore, setSilverPrice }