Initial release of Moniki paid services site and CMS (v1.0.0).
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,277 @@
|
||||
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)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user