278 lines
10 KiB
TypeScript
278 lines
10 KiB
TypeScript
import 'dotenv/config'
|
||
import { getPayload } from 'payload'
|
||
import config from '../payload.config'
|
||
|
||
const offers = [
|
||
{
|
||
slug: 'checkup-health',
|
||
sortOrder: 10,
|
||
price: 12900,
|
||
oldPrice: 18900,
|
||
endsAt: '2026-10-12T20:59:00.000Z',
|
||
imageUrl: '/promotions/checkup.jpg',
|
||
bookingHref: '/booking',
|
||
ru: {
|
||
title: 'Чек-ап «Здоровье»',
|
||
badge: '−32%',
|
||
description:
|
||
'Комплексное обследование за один визит: приём терапевта, лабораторный минимум, ЭКГ и заключение с рекомендациями.',
|
||
conditions:
|
||
'Для новых пациентов КДЦ МОНИКИ. Пакет нельзя разбить на отдельные услуги. Запись только онлайн до окончания акции. Действует в КДЦ на ул. Щепкина, 61/2.',
|
||
ctaLabel: 'Записаться на чек-ап',
|
||
},
|
||
en: {
|
||
title: 'Health check-up',
|
||
badge: '−32%',
|
||
description:
|
||
'A one-visit screening: internist consult, basic lab tests, ECG, and a written care plan.',
|
||
conditions:
|
||
'For new CDC MONIKI patients. The package cannot be split. Online booking only until the offer ends. Valid at 61/2 Shchepkina St.',
|
||
ctaLabel: 'Book the check-up',
|
||
},
|
||
},
|
||
{
|
||
slug: 'mri-brain',
|
||
sortOrder: 20,
|
||
price: 7500,
|
||
oldPrice: 9900,
|
||
endsAt: '2026-10-20T20:59:00.000Z',
|
||
imageUrl: '/promotions/mri.jpg',
|
||
bookingHref: '/booking',
|
||
ru: {
|
||
title: 'МРТ головного мозга',
|
||
badge: 'Скидка',
|
||
description:
|
||
'Исследование на экспертном томографе со скидкой при записи через сайт. Результат и описание врача-рентгенолога.',
|
||
conditions:
|
||
'Без контрастного усиления. Будние дни, 08:00–16:00. Нужно направление или заполненная анкета противопоказаний. Не суммируется с другими скидками.',
|
||
ctaLabel: 'Записаться на МРТ',
|
||
},
|
||
en: {
|
||
title: 'Brain MRI',
|
||
badge: 'Sale',
|
||
description:
|
||
'Expert MRI at a reduced price when booked online, with a radiologist report.',
|
||
conditions:
|
||
'Without contrast. Weekdays 08:00–16:00. A referral or contraindications form is required. Cannot be combined with other discounts.',
|
||
ctaLabel: 'Book an MRI',
|
||
},
|
||
},
|
||
{
|
||
slug: 'dental-hygiene',
|
||
sortOrder: 30,
|
||
price: 3900,
|
||
oldPrice: 5500,
|
||
endsAt: '2026-11-01T20:59:00.000Z',
|
||
imageUrl: '/promotions/dental.jpg',
|
||
bookingHref: '/booking',
|
||
ru: {
|
||
title: 'Профессиональная гигиена полости рта',
|
||
badge: '−29%',
|
||
description:
|
||
'Снятие налёта, полировка и рекомендации стоматолога. Подходит как самостоятельная процедура и как подготовка к лечению.',
|
||
conditions:
|
||
'Один пациент — одна процедура по акции. Первичный приём в стоматологии КДЦ. Не распространяется на отбеливание и лечение кариеса.',
|
||
ctaLabel: 'Записаться к стоматологу',
|
||
},
|
||
en: {
|
||
title: 'Professional dental cleaning',
|
||
badge: '−29%',
|
||
description:
|
||
'Plaque removal, polishing, and dentist advice — as a standalone visit or before treatment.',
|
||
conditions:
|
||
'One promotional procedure per patient. First visit at the CDC dental unit. Whitening and caries treatment are not included.',
|
||
ctaLabel: 'Book a dentist',
|
||
},
|
||
},
|
||
{
|
||
slug: 'cardio-package',
|
||
sortOrder: 40,
|
||
price: 6400,
|
||
oldPrice: 8800,
|
||
endsAt: '2026-11-15T20:59:00.000Z',
|
||
imageUrl: '/promotions/cardio.jpg',
|
||
bookingHref: '/booking',
|
||
ru: {
|
||
title: 'Кардиолог + ЭКГ + холтер',
|
||
badge: 'Пакет',
|
||
description:
|
||
'Консультация кардиолога, электрокардиограмма и суточное мониторирование в одном пакете — со скидкой при записи онлайн.',
|
||
conditions:
|
||
'Пакет оказывается целиком, услуги по отдельности по акции не продаются. Холтер выдаётся в день приёма. Действует в КДЦ МОНИКИ (Москва).',
|
||
ctaLabel: 'Записаться к кардиологу',
|
||
},
|
||
en: {
|
||
title: 'Cardiologist + ECG + Holter',
|
||
badge: 'Bundle',
|
||
description:
|
||
'Cardiology consult, ECG, and 24-hour Holter monitoring as one discounted online package.',
|
||
conditions:
|
||
'The bundle cannot be split. The Holter device is issued on the visit day. Valid at MONIKI CDC in Moscow.',
|
||
ctaLabel: 'Book a cardiologist',
|
||
},
|
||
},
|
||
] as const
|
||
|
||
export async function ensurePromotionsSchema(payload: Awaited<ReturnType<typeof getPayload>>) {
|
||
const adapter = payload.db as {
|
||
drizzle: { execute: (query: unknown) => Promise<unknown> }
|
||
}
|
||
const { sql } = await import('@payloadcms/db-postgres')
|
||
|
||
await adapter.drizzle.execute(sql`
|
||
CREATE TABLE IF NOT EXISTS promotions (
|
||
id serial PRIMARY KEY,
|
||
slug character varying NOT NULL,
|
||
price numeric NOT NULL,
|
||
old_price numeric,
|
||
starts_at timestamp(3) with time zone,
|
||
ends_at timestamp(3) with time zone NOT NULL,
|
||
show_countdown boolean DEFAULT true,
|
||
hide_when_expired boolean DEFAULT true,
|
||
booking_enabled boolean DEFAULT true,
|
||
booking_href character varying DEFAULT '/booking',
|
||
service_id integer,
|
||
image_id integer,
|
||
image_url character varying,
|
||
sort_order numeric DEFAULT 0,
|
||
is_published boolean DEFAULT true,
|
||
updated_at timestamp(3) with time zone NOT NULL DEFAULT now(),
|
||
created_at timestamp(3) with time zone NOT NULL DEFAULT now()
|
||
)
|
||
`)
|
||
|
||
await adapter.drizzle.execute(sql`
|
||
CREATE TABLE IF NOT EXISTS promotions_locales (
|
||
id serial PRIMARY KEY,
|
||
title character varying NOT NULL,
|
||
badge character varying,
|
||
description character varying,
|
||
conditions character varying,
|
||
cta_label character varying,
|
||
_locale _locales NOT NULL,
|
||
_parent_id integer NOT NULL
|
||
)
|
||
`)
|
||
|
||
await adapter.drizzle.execute(sql`CREATE UNIQUE INDEX IF NOT EXISTS promotions_slug_idx ON promotions (slug)`)
|
||
await adapter.drizzle.execute(sql`CREATE INDEX IF NOT EXISTS promotions_created_at_idx ON promotions (created_at)`)
|
||
await adapter.drizzle.execute(sql`CREATE INDEX IF NOT EXISTS promotions_updated_at_idx ON promotions (updated_at)`)
|
||
await adapter.drizzle.execute(sql`CREATE INDEX IF NOT EXISTS promotions_image_idx ON promotions (image_id)`)
|
||
await adapter.drizzle.execute(sql`CREATE INDEX IF NOT EXISTS promotions_service_idx ON promotions (service_id)`)
|
||
await adapter.drizzle.execute(sql`
|
||
CREATE UNIQUE INDEX IF NOT EXISTS promotions_locales_locale_parent_id_unique
|
||
ON promotions_locales (_locale, _parent_id)
|
||
`)
|
||
|
||
await adapter.drizzle.execute(sql`
|
||
DO $$ BEGIN
|
||
ALTER TABLE promotions
|
||
ADD CONSTRAINT promotions_image_id_media_id_fk
|
||
FOREIGN KEY (image_id) REFERENCES media(id) ON DELETE SET NULL;
|
||
EXCEPTION WHEN duplicate_object THEN NULL;
|
||
END $$
|
||
`)
|
||
await adapter.drizzle.execute(sql`
|
||
DO $$ BEGIN
|
||
ALTER TABLE promotions
|
||
ADD CONSTRAINT promotions_service_id_services_id_fk
|
||
FOREIGN KEY (service_id) REFERENCES services(id) ON DELETE SET NULL;
|
||
EXCEPTION WHEN duplicate_object THEN NULL;
|
||
END $$
|
||
`)
|
||
await adapter.drizzle.execute(sql`
|
||
DO $$ BEGIN
|
||
ALTER TABLE promotions_locales
|
||
ADD CONSTRAINT promotions_locales_parent_id_fk
|
||
FOREIGN KEY (_parent_id) REFERENCES promotions(id) ON DELETE CASCADE;
|
||
EXCEPTION WHEN duplicate_object THEN NULL;
|
||
END $$
|
||
`)
|
||
|
||
await adapter.drizzle.execute(sql`
|
||
ALTER TABLE payload_locked_documents_rels
|
||
ADD COLUMN IF NOT EXISTS promotions_id integer
|
||
`)
|
||
await adapter.drizzle.execute(sql`
|
||
CREATE INDEX IF NOT EXISTS payload_locked_documents_rels_promotions_id_idx
|
||
ON payload_locked_documents_rels (promotions_id)
|
||
`)
|
||
await adapter.drizzle.execute(sql`
|
||
DO $$ BEGIN
|
||
ALTER TABLE payload_locked_documents_rels
|
||
ADD CONSTRAINT payload_locked_documents_rels_promotions_fk
|
||
FOREIGN KEY (promotions_id) REFERENCES promotions(id) ON DELETE CASCADE;
|
||
EXCEPTION WHEN duplicate_object THEN NULL;
|
||
END $$
|
||
`)
|
||
}
|
||
|
||
export async function seedPromotions(payload: Awaited<ReturnType<typeof getPayload>>) {
|
||
await ensurePromotionsSchema(payload)
|
||
|
||
for (const offer of offers) {
|
||
const existing = await payload.find({
|
||
collection: 'promotions',
|
||
where: { slug: { equals: offer.slug } },
|
||
limit: 1,
|
||
depth: 0,
|
||
})
|
||
|
||
const data = {
|
||
slug: offer.slug,
|
||
sortOrder: offer.sortOrder,
|
||
price: offer.price,
|
||
oldPrice: offer.oldPrice,
|
||
endsAt: offer.endsAt,
|
||
imageUrl: offer.imageUrl,
|
||
bookingHref: offer.bookingHref,
|
||
bookingEnabled: true,
|
||
showCountdown: true,
|
||
hideWhenExpired: true,
|
||
isPublished: true,
|
||
...offer.ru,
|
||
}
|
||
|
||
let id = existing.docs[0]?.id
|
||
if (id) {
|
||
await payload.update({
|
||
collection: 'promotions',
|
||
id,
|
||
locale: 'ru',
|
||
data,
|
||
})
|
||
console.log('updated', offer.slug)
|
||
} else {
|
||
const created = await payload.create({
|
||
collection: 'promotions',
|
||
locale: 'ru',
|
||
data,
|
||
})
|
||
id = created.id
|
||
console.log('created', offer.slug)
|
||
}
|
||
|
||
await payload.update({
|
||
collection: 'promotions',
|
||
id,
|
||
locale: 'en',
|
||
data: offer.en,
|
||
})
|
||
}
|
||
|
||
console.log('promotions imported', offers.length)
|
||
}
|
||
|
||
async function main() {
|
||
const payload = await getPayload({ config })
|
||
await seedPromotions(payload)
|
||
process.exit(0)
|
||
}
|
||
|
||
if (process.argv[1]?.includes('import-promotions')) {
|
||
main().catch((err) => {
|
||
console.error(err)
|
||
process.exit(1)
|
||
})
|
||
}
|