add homepage management
This commit is contained in:
@@ -6,6 +6,8 @@ import { authenticator } from 'otplib'
|
||||
import qrcode from 'qrcode'
|
||||
import { prisma } from '../lib/prisma'
|
||||
import { requireAdminAuth, requireAdminRole } from '../middleware/requireAdminAuth'
|
||||
import { getMarketplaceHomepageContent, saveMarketplaceHomepageContent } from '../services/platformContentService'
|
||||
import type { MarketplaceHomepageContent, MarketplaceHomepageMetric, MarketplaceHomepagePillar, MarketplaceHomepageSectionType, MarketplaceHomepageStep } from '@rentaldrivego/types'
|
||||
|
||||
const router = Router()
|
||||
|
||||
@@ -14,6 +16,244 @@ const permissionSchema = z.object({
|
||||
actions: z.array(z.enum(['READ', 'CREATE', 'UPDATE', 'DELETE', 'SUSPEND', 'IMPERSONATE'])).min(1),
|
||||
})
|
||||
|
||||
const nullableString = z.union([z.string(), z.null()]).optional()
|
||||
const nullableEmail = z.union([z.string().email(), z.null()]).optional()
|
||||
const nullableUrl = z.union([z.string().url(), z.null()]).optional()
|
||||
const nullableDate = z.union([
|
||||
z.string().datetime(),
|
||||
z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
|
||||
z.null(),
|
||||
]).optional()
|
||||
|
||||
const homePageConfigSchema = z.object({
|
||||
heroTitle: nullableString,
|
||||
heroDescription: nullableString,
|
||||
viewOffersLabel: nullableString,
|
||||
viewPricingLabel: nullableString,
|
||||
contactCompanyLabel: nullableString,
|
||||
activeOffersTitle: nullableString,
|
||||
seeAllOffersLabel: nullableString,
|
||||
noActiveOffersLabel: nullableString,
|
||||
publishedVehiclesTitle: nullableString,
|
||||
viewVehicleLabel: nullableString,
|
||||
pricingEyebrow: nullableString,
|
||||
pricingTitle: nullableString,
|
||||
pricingDescription: nullableString,
|
||||
showOffers: z.boolean().optional(),
|
||||
showVehicles: z.boolean().optional(),
|
||||
showPricing: z.boolean().optional(),
|
||||
layout: z.object({
|
||||
items: z.array(z.object({
|
||||
id: z.string().min(1),
|
||||
type: z.enum(['hero', 'offers', 'vehicles', 'pricing']),
|
||||
x: z.number().int().min(1),
|
||||
y: z.number().int().min(1),
|
||||
w: z.number().int().min(1),
|
||||
h: z.number().int().min(1),
|
||||
})),
|
||||
}).optional(),
|
||||
}).optional()
|
||||
|
||||
const menuConfigSchema = z.object({
|
||||
aboutLabel: nullableString,
|
||||
vehiclesLabel: nullableString,
|
||||
offersLabel: nullableString,
|
||||
pricingLabel: nullableString,
|
||||
blogLabel: nullableString,
|
||||
contactLabel: nullableString,
|
||||
bookCarLabel: nullableString,
|
||||
siteNavigationLabel: nullableString,
|
||||
showAbout: z.boolean().optional(),
|
||||
showVehicles: z.boolean().optional(),
|
||||
showOffers: z.boolean().optional(),
|
||||
showPricing: z.boolean().optional(),
|
||||
showBlog: z.boolean().optional(),
|
||||
showContact: z.boolean().optional(),
|
||||
pageSections: z.object({
|
||||
home: z.object({
|
||||
hero: z.boolean().optional(),
|
||||
offers: z.boolean().optional(),
|
||||
vehicles: z.boolean().optional(),
|
||||
pricing: z.boolean().optional(),
|
||||
}).optional(),
|
||||
about: z.object({
|
||||
hero: z.boolean().optional(),
|
||||
highlights: z.boolean().optional(),
|
||||
details: z.boolean().optional(),
|
||||
}).optional(),
|
||||
offers: z.object({
|
||||
header: z.boolean().optional(),
|
||||
grid: z.boolean().optional(),
|
||||
}).optional(),
|
||||
pricing: z.object({
|
||||
hero: z.boolean().optional(),
|
||||
plans: z.boolean().optional(),
|
||||
payments: z.boolean().optional(),
|
||||
faq: z.boolean().optional(),
|
||||
}).optional(),
|
||||
vehicles: z.object({
|
||||
header: z.boolean().optional(),
|
||||
filters: z.boolean().optional(),
|
||||
grid: z.boolean().optional(),
|
||||
}).optional(),
|
||||
vehicleDetail: z.object({
|
||||
gallery: z.boolean().optional(),
|
||||
summary: z.boolean().optional(),
|
||||
}).optional(),
|
||||
blog: z.object({
|
||||
hero: z.boolean().optional(),
|
||||
posts: z.boolean().optional(),
|
||||
}).optional(),
|
||||
contact: z.object({
|
||||
content: z.boolean().optional(),
|
||||
}).optional(),
|
||||
booking: z.object({
|
||||
content: z.boolean().optional(),
|
||||
}).optional(),
|
||||
bookingConfirmation: z.object({
|
||||
hero: z.boolean().optional(),
|
||||
summary: z.boolean().optional(),
|
||||
actions: z.boolean().optional(),
|
||||
}).optional(),
|
||||
}).optional(),
|
||||
}).optional()
|
||||
|
||||
const marketplaceMetricSchema: z.ZodType<MarketplaceHomepageMetric> = z.object({
|
||||
value: z.string().min(1),
|
||||
label: z.string().min(1),
|
||||
})
|
||||
|
||||
const marketplacePillarSchema: z.ZodType<MarketplaceHomepagePillar> = z.object({
|
||||
title: z.string().min(1),
|
||||
body: z.string().min(1),
|
||||
})
|
||||
|
||||
const marketplaceStepSchema: z.ZodType<MarketplaceHomepageStep> = z.object({
|
||||
step: z.string().min(1),
|
||||
title: z.string().min(1),
|
||||
body: z.string().min(1),
|
||||
})
|
||||
|
||||
const marketplaceSectionSchema: z.ZodType<MarketplaceHomepageSectionType> = z.enum([
|
||||
'hero',
|
||||
'surface',
|
||||
'pillars',
|
||||
'audiences',
|
||||
'features',
|
||||
'steps',
|
||||
'closing',
|
||||
])
|
||||
|
||||
const marketplaceHomepageContentSchema: z.ZodType<MarketplaceHomepageContent> = z.object({
|
||||
sections: z.array(marketplaceSectionSchema).min(1),
|
||||
heroKicker: z.string().min(1),
|
||||
heroTitle: z.string().min(1),
|
||||
heroBody: z.string().min(1),
|
||||
startTrial: z.string().min(1),
|
||||
exploreVehicles: z.string().min(1),
|
||||
surfaceLabel: z.string().min(1),
|
||||
surfaceTitle: z.string().min(1),
|
||||
surfaceBody: z.string().min(1),
|
||||
liveLabel: z.string().min(1),
|
||||
trustedFleets: z.string().min(1),
|
||||
brandedFlows: z.string().min(1),
|
||||
multiTenant: z.string().min(1),
|
||||
companyKicker: z.string().min(1),
|
||||
companyTitle: z.string().min(1),
|
||||
companyBody: z.string().min(1),
|
||||
renterKicker: z.string().min(1),
|
||||
renterTitle: z.string().min(1),
|
||||
renterBody: z.string().min(1),
|
||||
pillars: z.array(marketplacePillarSchema).length(3),
|
||||
metrics: z.array(marketplaceMetricSchema).length(3),
|
||||
featureLabel: z.string().min(1),
|
||||
features: z.array(z.string().min(1)).min(1),
|
||||
stepsTitle: z.string().min(1),
|
||||
steps: z.array(marketplaceStepSchema).length(3),
|
||||
stepLabel: z.string().min(1),
|
||||
readyKicker: z.string().min(1),
|
||||
readyTitle: z.string().min(1),
|
||||
readyBody: z.string().min(1),
|
||||
viewPricing: z.string().min(1),
|
||||
createWorkspace: z.string().min(1),
|
||||
})
|
||||
|
||||
const marketplaceHomepageConfigSchema = z.object({
|
||||
en: marketplaceHomepageContentSchema,
|
||||
fr: marketplaceHomepageContentSchema,
|
||||
ar: marketplaceHomepageContentSchema,
|
||||
})
|
||||
|
||||
const adminCompanyUpdateSchema = z.object({
|
||||
company: z.object({
|
||||
name: z.string().min(1).optional(),
|
||||
slug: z.string().min(1).optional(),
|
||||
email: z.string().email().optional(),
|
||||
phone: nullableString,
|
||||
status: z.enum(['PENDING', 'TRIALING', 'ACTIVE', 'PAST_DUE', 'SUSPENDED', 'CANCELLED']).optional(),
|
||||
subscriptionPaymentRef: nullableString,
|
||||
}).optional(),
|
||||
subscription: z.object({
|
||||
plan: z.enum(['STARTER', 'GROWTH', 'PRO']).optional(),
|
||||
billingPeriod: z.enum(['MONTHLY', 'ANNUAL']).optional(),
|
||||
status: z.enum(['TRIALING', 'ACTIVE', 'PAST_DUE', 'CANCELLED', 'UNPAID']).optional(),
|
||||
currency: z.enum(['MAD', 'USD', 'EUR']).optional(),
|
||||
trialStartAt: nullableDate,
|
||||
trialEndAt: nullableDate,
|
||||
currentPeriodStart: nullableDate,
|
||||
currentPeriodEnd: nullableDate,
|
||||
cancelledAt: nullableDate,
|
||||
cancelAtPeriodEnd: z.boolean().optional(),
|
||||
}).optional(),
|
||||
brand: z.object({
|
||||
displayName: z.string().min(1).optional(),
|
||||
tagline: nullableString,
|
||||
subdomain: z.string().min(1).optional(),
|
||||
customDomain: nullableString,
|
||||
publicEmail: nullableEmail,
|
||||
publicPhone: nullableString,
|
||||
publicAddress: nullableString,
|
||||
publicCity: nullableString,
|
||||
publicCountry: nullableString,
|
||||
websiteUrl: nullableUrl,
|
||||
whatsappNumber: nullableString,
|
||||
defaultLocale: z.string().min(2).optional(),
|
||||
defaultCurrency: z.enum(['MAD', 'USD', 'EUR']).optional(),
|
||||
isListedOnMarketplace: z.boolean().optional(),
|
||||
homePageConfig: homePageConfigSchema,
|
||||
menuConfig: menuConfigSchema,
|
||||
}).optional(),
|
||||
contractSettings: z.object({
|
||||
legalName: nullableString,
|
||||
registrationNumber: nullableString,
|
||||
taxId: nullableString,
|
||||
terms: z.string().optional(),
|
||||
fuelPolicyType: z.enum(['FULL_TO_FULL', 'FULL_TO_EMPTY', 'SAME_TO_SAME', 'PREPAID', 'FREE']).optional(),
|
||||
lateFeePerHour: z.number().int().nullable().optional(),
|
||||
taxRate: z.number().nullable().optional(),
|
||||
signatureRequired: z.boolean().optional(),
|
||||
showTax: z.boolean().optional(),
|
||||
}).optional(),
|
||||
accountingSettings: z.object({
|
||||
reportingPeriod: z.enum(['WEEKLY', 'MONTHLY', 'QUARTERLY', 'ANNUAL']).optional(),
|
||||
fiscalYearStart: z.number().int().min(1).max(12).optional(),
|
||||
currency: z.enum(['MAD', 'USD', 'EUR']).optional(),
|
||||
accountantEmail: nullableEmail,
|
||||
accountantName: nullableString,
|
||||
autoSendReport: z.boolean().optional(),
|
||||
reportFormat: z.enum(['PDF', 'CSV', 'BOTH']).optional(),
|
||||
}).optional(),
|
||||
})
|
||||
|
||||
function parseOptionalDate(value?: string | null) {
|
||||
if (!value) return null
|
||||
return new Date(value)
|
||||
}
|
||||
|
||||
function toAuditJson<T>(value: T) {
|
||||
return JSON.parse(JSON.stringify(value))
|
||||
}
|
||||
|
||||
function signAdminToken(adminId: string) {
|
||||
return jwt.sign({ sub: adminId, type: 'admin' }, process.env.JWT_SECRET!, { expiresIn: '8h' })
|
||||
}
|
||||
@@ -97,11 +337,160 @@ router.get('/companies', requireAdminAuth, async (req, res, next) => {
|
||||
|
||||
router.get('/companies/:id', requireAdminAuth, async (req, res, next) => {
|
||||
try {
|
||||
const company = await prisma.company.findUniqueOrThrow({ where: { id: req.params.id }, include: { brand: true, subscription: { include: { invoices: { orderBy: { createdAt: 'desc' }, take: 10 } } }, employees: true } })
|
||||
const company = await prisma.company.findUniqueOrThrow({
|
||||
where: { id: req.params.id },
|
||||
include: {
|
||||
brand: true,
|
||||
contractSettings: true,
|
||||
accountingSettings: true,
|
||||
subscription: { include: { invoices: { orderBy: { createdAt: 'desc' }, take: 10 } } },
|
||||
employees: true,
|
||||
_count: { select: { employees: true, vehicles: true, customers: true, reservations: true } },
|
||||
},
|
||||
})
|
||||
res.json({ data: company })
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
router.get('/site-config/marketplace-homepage', requireAdminAuth, async (req, res, next) => {
|
||||
try {
|
||||
res.json({ data: await getMarketplaceHomepageContent() })
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
router.patch('/site-config/marketplace-homepage', requireAdminAuth, requireAdminRole('SUPPORT'), async (req, res, next) => {
|
||||
try {
|
||||
const { homepage } = z.object({ homepage: marketplaceHomepageConfigSchema }).parse(req.body)
|
||||
const saved = await saveMarketplaceHomepageContent(homepage)
|
||||
await prisma.auditLog.create({
|
||||
data: {
|
||||
adminUserId: req.admin.id,
|
||||
action: 'UPDATE',
|
||||
resource: 'MarketplaceHomepage',
|
||||
after: toAuditJson(saved),
|
||||
ipAddress: req.ip,
|
||||
userAgent: req.headers['user-agent'],
|
||||
},
|
||||
})
|
||||
res.json({ data: saved })
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
router.patch('/companies/:id', requireAdminAuth, requireAdminRole('SUPPORT'), async (req, res, next) => {
|
||||
try {
|
||||
const body = adminCompanyUpdateSchema.parse(req.body)
|
||||
const before = await prisma.company.findUniqueOrThrow({
|
||||
where: { id: req.params.id },
|
||||
include: {
|
||||
brand: true,
|
||||
contractSettings: true,
|
||||
accountingSettings: true,
|
||||
subscription: true,
|
||||
},
|
||||
})
|
||||
|
||||
const updated = await prisma.$transaction(async (tx) => {
|
||||
if (body.company) {
|
||||
await tx.company.update({
|
||||
where: { id: req.params.id },
|
||||
data: body.company,
|
||||
})
|
||||
}
|
||||
|
||||
if (body.subscription) {
|
||||
await tx.subscription.upsert({
|
||||
where: { companyId: req.params.id },
|
||||
update: {
|
||||
...body.subscription,
|
||||
trialStartAt: parseOptionalDate(body.subscription.trialStartAt),
|
||||
trialEndAt: parseOptionalDate(body.subscription.trialEndAt),
|
||||
currentPeriodStart: parseOptionalDate(body.subscription.currentPeriodStart),
|
||||
currentPeriodEnd: parseOptionalDate(body.subscription.currentPeriodEnd),
|
||||
cancelledAt: parseOptionalDate(body.subscription.cancelledAt),
|
||||
},
|
||||
create: {
|
||||
companyId: req.params.id,
|
||||
plan: body.subscription.plan ?? 'STARTER',
|
||||
billingPeriod: body.subscription.billingPeriod ?? 'MONTHLY',
|
||||
status: body.subscription.status ?? 'TRIALING',
|
||||
currency: body.subscription.currency ?? 'MAD',
|
||||
trialStartAt: parseOptionalDate(body.subscription.trialStartAt),
|
||||
trialEndAt: parseOptionalDate(body.subscription.trialEndAt),
|
||||
currentPeriodStart: parseOptionalDate(body.subscription.currentPeriodStart),
|
||||
currentPeriodEnd: parseOptionalDate(body.subscription.currentPeriodEnd),
|
||||
cancelledAt: parseOptionalDate(body.subscription.cancelledAt),
|
||||
cancelAtPeriodEnd: body.subscription.cancelAtPeriodEnd ?? false,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if (body.brand) {
|
||||
const currentCompany = await tx.company.findUniqueOrThrow({ where: { id: req.params.id } })
|
||||
await tx.brandSettings.upsert({
|
||||
where: { companyId: req.params.id },
|
||||
update: body.brand,
|
||||
create: {
|
||||
companyId: req.params.id,
|
||||
displayName: body.brand.displayName ?? currentCompany.name,
|
||||
subdomain: body.brand.subdomain ?? currentCompany.slug,
|
||||
paymentMethodsEnabled: before.brand?.paymentMethodsEnabled ?? [],
|
||||
...body.brand,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if (body.contractSettings) {
|
||||
await tx.contractSettings.upsert({
|
||||
where: { companyId: req.params.id },
|
||||
update: body.contractSettings,
|
||||
create: {
|
||||
companyId: req.params.id,
|
||||
...body.contractSettings,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if (body.accountingSettings) {
|
||||
await tx.accountingSettings.upsert({
|
||||
where: { companyId: req.params.id },
|
||||
update: body.accountingSettings,
|
||||
create: {
|
||||
companyId: req.params.id,
|
||||
...body.accountingSettings,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return tx.company.findUniqueOrThrow({
|
||||
where: { id: req.params.id },
|
||||
include: {
|
||||
brand: true,
|
||||
contractSettings: true,
|
||||
accountingSettings: true,
|
||||
subscription: { include: { invoices: { orderBy: { createdAt: 'desc' }, take: 10 } } },
|
||||
employees: true,
|
||||
_count: { select: { employees: true, vehicles: true, customers: true, reservations: true } },
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
await prisma.auditLog.create({
|
||||
data: {
|
||||
adminUserId: req.admin.id,
|
||||
action: 'UPDATE_COMPANY',
|
||||
resource: 'Company',
|
||||
resourceId: req.params.id,
|
||||
companyId: req.params.id,
|
||||
before: toAuditJson(before),
|
||||
after: toAuditJson(body),
|
||||
ipAddress: req.ip,
|
||||
},
|
||||
})
|
||||
|
||||
res.json({ data: updated })
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
router.patch('/companies/:id/status', requireAdminAuth, requireAdminRole('SUPPORT'), async (req, res, next) => {
|
||||
try {
|
||||
const { status, reason } = z.object({ status: z.enum(['ACTIVE','SUSPENDED','CANCELLED']), reason: z.string().optional() }).parse(req.body)
|
||||
|
||||
@@ -37,6 +37,97 @@ const brandSchema = z.object({
|
||||
paypalEmail: z.string().email().optional(),
|
||||
paypalMerchantId: z.string().optional(),
|
||||
isListedOnMarketplace: z.boolean().optional(),
|
||||
homePageConfig: z.object({
|
||||
heroTitle: z.union([z.string(), z.null()]).optional(),
|
||||
heroDescription: z.union([z.string(), z.null()]).optional(),
|
||||
viewOffersLabel: z.union([z.string(), z.null()]).optional(),
|
||||
viewPricingLabel: z.union([z.string(), z.null()]).optional(),
|
||||
contactCompanyLabel: z.union([z.string(), z.null()]).optional(),
|
||||
activeOffersTitle: z.union([z.string(), z.null()]).optional(),
|
||||
seeAllOffersLabel: z.union([z.string(), z.null()]).optional(),
|
||||
noActiveOffersLabel: z.union([z.string(), z.null()]).optional(),
|
||||
publishedVehiclesTitle: z.union([z.string(), z.null()]).optional(),
|
||||
viewVehicleLabel: z.union([z.string(), z.null()]).optional(),
|
||||
pricingEyebrow: z.union([z.string(), z.null()]).optional(),
|
||||
pricingTitle: z.union([z.string(), z.null()]).optional(),
|
||||
pricingDescription: z.union([z.string(), z.null()]).optional(),
|
||||
showOffers: z.boolean().optional(),
|
||||
showVehicles: z.boolean().optional(),
|
||||
showPricing: z.boolean().optional(),
|
||||
layout: z.object({
|
||||
items: z.array(z.object({
|
||||
id: z.string().min(1),
|
||||
type: z.enum(['hero', 'offers', 'vehicles', 'pricing']),
|
||||
x: z.number().int().min(1),
|
||||
y: z.number().int().min(1),
|
||||
w: z.number().int().min(1),
|
||||
h: z.number().int().min(1),
|
||||
})),
|
||||
}).optional(),
|
||||
}).optional(),
|
||||
menuConfig: z.object({
|
||||
aboutLabel: z.union([z.string(), z.null()]).optional(),
|
||||
vehiclesLabel: z.union([z.string(), z.null()]).optional(),
|
||||
offersLabel: z.union([z.string(), z.null()]).optional(),
|
||||
pricingLabel: z.union([z.string(), z.null()]).optional(),
|
||||
blogLabel: z.union([z.string(), z.null()]).optional(),
|
||||
contactLabel: z.union([z.string(), z.null()]).optional(),
|
||||
bookCarLabel: z.union([z.string(), z.null()]).optional(),
|
||||
siteNavigationLabel: z.union([z.string(), z.null()]).optional(),
|
||||
showAbout: z.boolean().optional(),
|
||||
showVehicles: z.boolean().optional(),
|
||||
showOffers: z.boolean().optional(),
|
||||
showPricing: z.boolean().optional(),
|
||||
showBlog: z.boolean().optional(),
|
||||
showContact: z.boolean().optional(),
|
||||
pageSections: z.object({
|
||||
home: z.object({
|
||||
hero: z.boolean().optional(),
|
||||
offers: z.boolean().optional(),
|
||||
vehicles: z.boolean().optional(),
|
||||
pricing: z.boolean().optional(),
|
||||
}).optional(),
|
||||
about: z.object({
|
||||
hero: z.boolean().optional(),
|
||||
highlights: z.boolean().optional(),
|
||||
details: z.boolean().optional(),
|
||||
}).optional(),
|
||||
offers: z.object({
|
||||
header: z.boolean().optional(),
|
||||
grid: z.boolean().optional(),
|
||||
}).optional(),
|
||||
pricing: z.object({
|
||||
hero: z.boolean().optional(),
|
||||
plans: z.boolean().optional(),
|
||||
payments: z.boolean().optional(),
|
||||
faq: z.boolean().optional(),
|
||||
}).optional(),
|
||||
vehicles: z.object({
|
||||
header: z.boolean().optional(),
|
||||
filters: z.boolean().optional(),
|
||||
grid: z.boolean().optional(),
|
||||
}).optional(),
|
||||
vehicleDetail: z.object({
|
||||
gallery: z.boolean().optional(),
|
||||
summary: z.boolean().optional(),
|
||||
}).optional(),
|
||||
blog: z.object({
|
||||
hero: z.boolean().optional(),
|
||||
posts: z.boolean().optional(),
|
||||
}).optional(),
|
||||
contact: z.object({
|
||||
content: z.boolean().optional(),
|
||||
}).optional(),
|
||||
booking: z.object({
|
||||
content: z.boolean().optional(),
|
||||
}).optional(),
|
||||
bookingConfirmation: z.object({
|
||||
hero: z.boolean().optional(),
|
||||
summary: z.boolean().optional(),
|
||||
actions: z.boolean().optional(),
|
||||
}).optional(),
|
||||
}).optional(),
|
||||
}).optional(),
|
||||
})
|
||||
|
||||
const contractSettingsSchema = z.object({
|
||||
|
||||
@@ -7,9 +7,18 @@ import { applyInsurancesToReservation } from '../services/insuranceService'
|
||||
import { applyPricingRules } from '../services/pricingRuleService'
|
||||
import { validateAndFlagLicense, validateLicense } from '../services/licenseValidationService'
|
||||
import * as paypal from '../services/paypalService'
|
||||
import { getMarketplaceHomepageContent } from '../services/platformContentService'
|
||||
|
||||
const router = Router()
|
||||
|
||||
router.get('/platform/homepage', async (_req, res, next) => {
|
||||
try {
|
||||
res.json({ data: await getMarketplaceHomepageContent() })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
function isDatabaseUnavailableError(error: unknown): boolean {
|
||||
if (!error || typeof error !== 'object') return false
|
||||
|
||||
|
||||
Reference in New Issue
Block a user