Files
Moniki_uslugi/apps/cms/src/seed/import-promotions.ts
T

278 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
})
}