Initial release of Moniki paid services site and CMS (v1.0.0).

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alexey Novikov
2026-09-16 13:10:25 +03:00
co-authored by Cursor
commit 8abd8d06dd
263 changed files with 24973 additions and 0 deletions
+277
View File
@@ -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)
})
}