Add dark-mode business logo, payment order steps, and Arvan DNS diagnostics.
Store logo_dark_media_id on businesses, expose logoDarkUrl on tenant/website APIs, prepend awaiting-payment steps to order workflows, and surface clearer Arvan API network errors. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
e942348b1d
commit
6ce6f959ac
@@ -0,0 +1,10 @@
|
||||
-- Optional dark-theme logo for business profile / storefront dashboards
|
||||
ALTER TABLE businesses
|
||||
ADD COLUMN IF NOT EXISTS logo_dark_media_id BIGINT;
|
||||
|
||||
ALTER TABLE businesses
|
||||
DROP CONSTRAINT IF EXISTS businesses_logo_dark_media_id_fkey;
|
||||
|
||||
ALTER TABLE businesses
|
||||
ADD CONSTRAINT businesses_logo_dark_media_id_fkey
|
||||
FOREIGN KEY (logo_dark_media_id) REFERENCES media (id) ON DELETE SET NULL;
|
||||
@@ -169,6 +169,11 @@
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"logoDarkUrl": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "Optional logo for dark backgrounds; falls back to logoUrl when omitted."
|
||||
},
|
||||
"faviconUrl": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
|
||||
@@ -69,6 +69,7 @@ model Business {
|
||||
phoneNumbers Json @default("[]") @map("phone_numbers")
|
||||
socialMedia Json @default("{}") @map("social_media")
|
||||
logoMediaId BigInt? @map("logo_media_id")
|
||||
logoDarkMediaId BigInt? @map("logo_dark_media_id")
|
||||
faviconMediaId BigInt? @map("favicon_media_id")
|
||||
oldBusinessId BigInt? @map("old_business_id")
|
||||
addresses Address[]
|
||||
@@ -79,6 +80,7 @@ model Business {
|
||||
businessUsers BusinessUser[]
|
||||
faviconMedia Media? @relation("BusinessFavicon", fields: [faviconMediaId], references: [id], onUpdate: NoAction)
|
||||
logoMedia Media? @relation("BusinessLogo", fields: [logoMediaId], references: [id], onUpdate: NoAction)
|
||||
logoDarkMedia Media? @relation("BusinessLogoDark", fields: [logoDarkMediaId], references: [id], onUpdate: NoAction)
|
||||
carts Cart[]
|
||||
categories Category[]
|
||||
contentCategoryAssignments CategoryAssignment[]
|
||||
@@ -288,6 +290,7 @@ model Media {
|
||||
brandImages Brand[]
|
||||
faviconBusinesses Business[] @relation("BusinessFavicon")
|
||||
logoBusinesses Business[] @relation("BusinessLogo")
|
||||
logoDarkBusinesses Business[] @relation("BusinessLogoDark")
|
||||
business Business @relation(fields: [businessId], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||
uploader User? @relation(fields: [uploadedBy], references: [id], onUpdate: NoAction)
|
||||
attachments MediaAttachment[]
|
||||
|
||||
@@ -226,9 +226,33 @@ export class ArvanDnsService {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: body === undefined ? undefined : JSON.stringify(body),
|
||||
signal: AbortSignal.timeout(20_000),
|
||||
});
|
||||
} catch {
|
||||
throw new ServiceUnavailableException('Could not reach ArvanCloud DNS API');
|
||||
} catch (err) {
|
||||
const cause =
|
||||
err && typeof err === 'object' && 'cause' in err
|
||||
? (err as { cause?: unknown }).cause
|
||||
: undefined;
|
||||
const detail =
|
||||
(cause &&
|
||||
typeof cause === 'object' &&
|
||||
'code' in cause &&
|
||||
typeof (cause as { code: unknown }).code === 'string' &&
|
||||
(cause as { code: string }).code) ||
|
||||
(cause &&
|
||||
typeof cause === 'object' &&
|
||||
'message' in cause &&
|
||||
typeof (cause as { message: unknown }).message === 'string' &&
|
||||
(cause as { message: string }).message) ||
|
||||
(err instanceof Error ? err.message : null);
|
||||
this.logger.warn(
|
||||
`Arvan DNS request failed ${method} ${path}${detail ? `: ${detail}` : ''}`,
|
||||
);
|
||||
throw new ServiceUnavailableException(
|
||||
detail
|
||||
? `Could not reach ArvanCloud DNS API (${detail})`
|
||||
: 'Could not reach ArvanCloud DNS API',
|
||||
);
|
||||
}
|
||||
|
||||
const text = await response.text().catch(() => '');
|
||||
|
||||
+73
-24
@@ -30,6 +30,8 @@ import { parseUserProfile } from './profile.util';
|
||||
import { SmsService } from './sms.service';
|
||||
|
||||
const OTP_TTL_SECONDS = 300;
|
||||
/** Pending cross-site password change (longer than OTP so resend still works). */
|
||||
const PENDING_PASSWORD_TTL_SECONDS = 600;
|
||||
const HANDOFF_TTL_SECONDS = 60;
|
||||
const HANDOFF_RATE_LIMIT = 10;
|
||||
const HANDOFF_RATE_WINDOW_SECONDS = 60;
|
||||
@@ -78,13 +80,14 @@ export class AuthService {
|
||||
);
|
||||
}
|
||||
|
||||
let passwordMatchesExisting = false;
|
||||
if (existingUser) {
|
||||
const passwordValid = await bcrypt.compare(
|
||||
passwordMatchesExisting = await bcrypt.compare(
|
||||
dto.password,
|
||||
existingUser.passwordHash,
|
||||
);
|
||||
|
||||
if (!passwordValid && !dto.acknowledgeExistingAccount) {
|
||||
if (!passwordMatchesExisting && !dto.acknowledgeExistingAccount) {
|
||||
const otherNames = existingUser.businessCustomers
|
||||
.map((item) => item.business.nameFa?.trim() || item.business.name)
|
||||
.filter(Boolean);
|
||||
@@ -98,18 +101,49 @@ export class AuthService {
|
||||
}
|
||||
|
||||
const linkedExistingAccount = Boolean(existingUser);
|
||||
// New password claimed on another site — apply only after OTP proves ownership.
|
||||
const wantsPasswordUpdate =
|
||||
linkedExistingAccount && !passwordMatchesExisting;
|
||||
|
||||
// Hold new password in Redis until OTP; do not touch DB password or membership yet.
|
||||
if (existingUser && wantsPasswordUpdate && smsEnabled) {
|
||||
await this.redis.setPendingPasswordUpdate(
|
||||
dto.cellNumber,
|
||||
{
|
||||
passwordHash,
|
||||
businessId: business.id.toString(),
|
||||
},
|
||||
PENDING_PASSWORD_TTL_SECONDS,
|
||||
);
|
||||
|
||||
const authUser = await this.getAuthUser(existingUser.id);
|
||||
return {
|
||||
message:
|
||||
'Please verify your cell number with OTP. Your Meshkee password will be updated after verification.',
|
||||
smsEnabled: true,
|
||||
requiresOtp: true,
|
||||
passwordPending: true,
|
||||
passwordUpdated: false,
|
||||
user: this.serializeUser(authUser),
|
||||
registeredBusiness: {
|
||||
id: business.id,
|
||||
name: business.name,
|
||||
slug: business.slug,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const userId = await this.prisma.$transaction(async (tx) => {
|
||||
let userId: bigint;
|
||||
|
||||
if (existingUser) {
|
||||
// Cross-site join: replace Meshkee password with the one entered on this signup.
|
||||
const updated = await tx.user.update({
|
||||
where: { id: existingUser.id },
|
||||
data: { passwordHash },
|
||||
select: { id: true },
|
||||
});
|
||||
userId = updated.id;
|
||||
if (wantsPasswordUpdate) {
|
||||
await tx.user.update({
|
||||
where: { id: existingUser.id },
|
||||
data: { passwordHash },
|
||||
});
|
||||
}
|
||||
userId = existingUser.id;
|
||||
} else {
|
||||
const created = await tx.user.create({
|
||||
data: {
|
||||
@@ -157,15 +191,15 @@ export class AuthService {
|
||||
const tokens = await this.issueTokens(authUser);
|
||||
|
||||
return {
|
||||
message: linkedExistingAccount
|
||||
? smsEnabled
|
||||
? 'Joined this website. Your Meshkee password was updated to the one you just entered. Please verify your cell number with OTP.'
|
||||
: 'Joined this website. Your Meshkee password was updated to the one you just entered.'
|
||||
: smsEnabled
|
||||
? 'Registration successful. Please verify your cell number with OTP.'
|
||||
: 'Registration successful. SMS verification is disabled — account auto-verified.',
|
||||
message: wantsPasswordUpdate
|
||||
? 'Joined this website. Your Meshkee password was updated to the one you just entered.'
|
||||
: linkedExistingAccount
|
||||
? 'Joined this website with your existing Meshkee account.'
|
||||
: smsEnabled
|
||||
? 'Registration successful. Please verify your cell number with OTP.'
|
||||
: 'Registration successful. SMS verification is disabled — account auto-verified.',
|
||||
smsEnabled,
|
||||
passwordUpdated: linkedExistingAccount,
|
||||
passwordUpdated: wantsPasswordUpdate,
|
||||
user: this.serializeUser(authUser),
|
||||
registeredBusiness: {
|
||||
id: business.id,
|
||||
@@ -439,10 +473,8 @@ export class AuthService {
|
||||
|
||||
async loginWithOtp(cellNumber: string, code: string, domain?: string) {
|
||||
const user = await this.consumeOtp(cellNumber, code);
|
||||
|
||||
if (domain?.trim()) {
|
||||
await this.ensureCustomerMembershipForDomain(user.id, domain.trim());
|
||||
}
|
||||
const pending = await this.redis.consumePendingPasswordUpdate(cellNumber);
|
||||
const passwordUpdated = Boolean(pending?.passwordHash);
|
||||
|
||||
await this.prisma.user.update({
|
||||
where: { id: user.id },
|
||||
@@ -451,14 +483,28 @@ export class AuthService {
|
||||
isActive: true,
|
||||
cellVerifiedAt: user.cellVerifiedAt ?? new Date(),
|
||||
lastLoginAt: new Date(),
|
||||
...(pending?.passwordHash
|
||||
? { passwordHash: pending.passwordHash }
|
||||
: {}),
|
||||
},
|
||||
});
|
||||
|
||||
if (pending?.businessId) {
|
||||
await this.ensureCustomerMembership(user.id, BigInt(pending.businessId));
|
||||
}
|
||||
|
||||
if (domain?.trim()) {
|
||||
await this.ensureCustomerMembershipForDomain(user.id, domain.trim());
|
||||
}
|
||||
|
||||
const authUser = await this.getAuthUser(user.id);
|
||||
const tokens = await this.issueTokens(authUser);
|
||||
|
||||
return {
|
||||
message: 'Login successful',
|
||||
message: passwordUpdated
|
||||
? 'Login successful. Your Meshkee password was updated.'
|
||||
: 'Login successful',
|
||||
passwordUpdated,
|
||||
user: this.serializeUser(authUser),
|
||||
...tokens,
|
||||
};
|
||||
@@ -510,17 +556,20 @@ export class AuthService {
|
||||
domain: string,
|
||||
) {
|
||||
const business = await this.tenant.resolveBusinessByDomain(domain);
|
||||
await this.ensureCustomerMembership(userId, business.id);
|
||||
}
|
||||
|
||||
private async ensureCustomerMembership(userId: bigint, businessId: bigint) {
|
||||
const existing = await this.prisma.businessCustomer.findUnique({
|
||||
where: {
|
||||
businessId_userId: { businessId: business.id, userId },
|
||||
businessId_userId: { businessId, userId },
|
||||
},
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
await this.prisma.businessCustomer.create({
|
||||
data: {
|
||||
businessId: business.id,
|
||||
businessId,
|
||||
userId,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -489,14 +489,18 @@ export class LegacyPurgeService {
|
||||
id: businessId,
|
||||
OR: [
|
||||
{ logoMediaId: { in: uniqueIds } },
|
||||
{ logoDarkMediaId: { in: uniqueIds } },
|
||||
{ faviconMediaId: { in: uniqueIds } },
|
||||
],
|
||||
},
|
||||
select: { logoMediaId: true, faviconMediaId: true },
|
||||
select: { logoMediaId: true, logoDarkMediaId: true, faviconMediaId: true },
|
||||
});
|
||||
if (businessMedia?.logoMediaId != null) {
|
||||
stillLinked.add(businessMedia.logoMediaId.toString());
|
||||
}
|
||||
if (businessMedia?.logoDarkMediaId != null) {
|
||||
stillLinked.add(businessMedia.logoDarkMediaId.toString());
|
||||
}
|
||||
if (businessMedia?.faviconMediaId != null) {
|
||||
stillLinked.add(businessMedia.faviconMediaId.toString());
|
||||
}
|
||||
|
||||
@@ -68,6 +68,7 @@ export class BusinessProfileService {
|
||||
categoryAssignments: true,
|
||||
addresses: { orderBy: { createdAt: 'asc' } },
|
||||
logoMedia: true,
|
||||
logoDarkMedia: true,
|
||||
faviconMedia: true,
|
||||
},
|
||||
});
|
||||
@@ -99,6 +100,7 @@ export class BusinessProfileService {
|
||||
categoryAssignments: true,
|
||||
addresses: { orderBy: { createdAt: 'asc' } },
|
||||
logoMedia: true,
|
||||
logoDarkMedia: true,
|
||||
faviconMedia: true,
|
||||
},
|
||||
});
|
||||
@@ -140,6 +142,7 @@ export class BusinessProfileService {
|
||||
categoryAssignments: true,
|
||||
addresses: true,
|
||||
logoMedia: true,
|
||||
logoDarkMedia: true,
|
||||
faviconMedia: true,
|
||||
},
|
||||
});
|
||||
@@ -156,6 +159,10 @@ export class BusinessProfileService {
|
||||
await this.assertLogoMedia(businessId, BigInt(dto.logoMediaId));
|
||||
}
|
||||
|
||||
if (dto.logoDarkMediaId !== undefined && dto.logoDarkMediaId !== null) {
|
||||
await this.assertLogoMedia(businessId, BigInt(dto.logoDarkMediaId));
|
||||
}
|
||||
|
||||
const previousFaviconMediaId = business.faviconMediaId;
|
||||
const logoChanged =
|
||||
dto.logoMediaId !== undefined &&
|
||||
@@ -196,6 +203,13 @@ export class BusinessProfileService {
|
||||
}
|
||||
}
|
||||
|
||||
if (dto.logoDarkMediaId !== undefined) {
|
||||
data.logoDarkMedia =
|
||||
dto.logoDarkMediaId === null
|
||||
? { disconnect: true }
|
||||
: { connect: { id: BigInt(dto.logoDarkMediaId) } };
|
||||
}
|
||||
|
||||
if (Object.keys(data).length > 0) {
|
||||
await tx.business.update({
|
||||
where: { id: businessId },
|
||||
@@ -376,6 +390,8 @@ export class BusinessProfileService {
|
||||
socialMedia: unknown;
|
||||
logoMediaId: bigint | null;
|
||||
logoMedia: { publicUrl: string } | null;
|
||||
logoDarkMediaId: bigint | null;
|
||||
logoDarkMedia: { publicUrl: string } | null;
|
||||
faviconMediaId: bigint | null;
|
||||
faviconMedia: { publicUrl: string } | null;
|
||||
categoryAssignments: { categoryId: bigint }[];
|
||||
@@ -390,6 +406,8 @@ export class BusinessProfileService {
|
||||
socialMedia: normalizeSocialMedia(business.socialMedia),
|
||||
logoMediaId: business.logoMediaId?.toString() ?? null,
|
||||
logoUrl: business.logoMedia?.publicUrl ?? null,
|
||||
logoDarkMediaId: business.logoDarkMediaId?.toString() ?? null,
|
||||
logoDarkUrl: business.logoDarkMedia?.publicUrl ?? null,
|
||||
faviconMediaId: business.faviconMediaId?.toString() ?? null,
|
||||
faviconUrl:
|
||||
business.faviconMedia?.publicUrl ??
|
||||
|
||||
@@ -33,6 +33,8 @@ export type BusinessProfile = {
|
||||
socialMedia: BusinessSocialMedia;
|
||||
logoMediaId: string | null;
|
||||
logoUrl: string | null;
|
||||
logoDarkMediaId: string | null;
|
||||
logoDarkUrl: string | null;
|
||||
faviconMediaId: string | null;
|
||||
faviconUrl: string | null;
|
||||
categoryIds: string[];
|
||||
|
||||
@@ -110,6 +110,11 @@ export class UpdateBusinessProfileDto {
|
||||
@IsInt()
|
||||
logoMediaId?: number | null;
|
||||
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
logoDarkMediaId?: number | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@Type(() => Number)
|
||||
|
||||
@@ -199,6 +199,18 @@ export type BusinessSettings = {
|
||||
};
|
||||
|
||||
export const DEFAULT_ORDER_PROCESS_STEPS: OrderProcessStep[] = [
|
||||
{
|
||||
id: 'awaiting-payment',
|
||||
label: 'Waiting for payment',
|
||||
labelFa: 'در انتظار پرداخت',
|
||||
color: '#64748B',
|
||||
},
|
||||
{
|
||||
id: 'payment-successful',
|
||||
label: 'Payment successful',
|
||||
labelFa: 'پرداخت موفق',
|
||||
color: '#10B981',
|
||||
},
|
||||
{
|
||||
id: 'processing',
|
||||
label: 'Under processing',
|
||||
|
||||
@@ -320,6 +320,19 @@ export function listPublicPaymentGateways(settings: EPaymentSettings) {
|
||||
};
|
||||
}
|
||||
|
||||
const PAYMENT_PROCESS_STEP_IDS = ['awaiting-payment', 'payment-successful'] as const;
|
||||
|
||||
/** Prepend default payment steps when an older store settings JSON omits them. */
|
||||
function ensurePaymentProcessSteps(steps: OrderProcessStep[]): OrderProcessStep[] {
|
||||
const missing = DEFAULT_ORDER_PROCESS_STEPS.filter(
|
||||
(defaults) =>
|
||||
PAYMENT_PROCESS_STEP_IDS.includes(
|
||||
defaults.id as (typeof PAYMENT_PROCESS_STEP_IDS)[number],
|
||||
) && !steps.some((step) => step.id === defaults.id),
|
||||
);
|
||||
return missing.length ? [...missing, ...steps] : steps;
|
||||
}
|
||||
|
||||
function readOrderProcessSteps(value: unknown): OrderProcessStep[] {
|
||||
if (!Array.isArray(value)) {
|
||||
return DEFAULT_ORDER_PROCESS_STEPS;
|
||||
@@ -351,7 +364,9 @@ function readOrderProcessSteps(value: unknown): OrderProcessStep[] {
|
||||
})
|
||||
.filter((step): step is OrderProcessStep => step !== null);
|
||||
|
||||
return steps.length ? steps : DEFAULT_ORDER_PROCESS_STEPS;
|
||||
return steps.length
|
||||
? ensurePaymentProcessSteps(steps)
|
||||
: DEFAULT_ORDER_PROCESS_STEPS;
|
||||
}
|
||||
|
||||
export function normalizeBusinessSettings(raw: unknown): BusinessSettings {
|
||||
|
||||
@@ -28,6 +28,8 @@ export const ORDER_STEP_COLOR_HEXES = [
|
||||
export const DEFAULT_ORDER_STEP_COLOR = '#3B82F6';
|
||||
|
||||
const DEFAULT_STEP_COLORS_BY_ID: Record<string, string> = {
|
||||
'awaiting-payment': '#64748B',
|
||||
'payment-successful': '#10B981',
|
||||
processing: '#3B82F6',
|
||||
'ready-for-shipping': '#F59E0B',
|
||||
shipped: '#8B5CF6',
|
||||
|
||||
@@ -267,10 +267,11 @@ export class MediaService {
|
||||
id: businessId,
|
||||
OR: [
|
||||
{ logoMediaId: { in: uniqueIds } },
|
||||
{ logoDarkMediaId: { in: uniqueIds } },
|
||||
{ faviconMediaId: { in: uniqueIds } },
|
||||
],
|
||||
},
|
||||
select: { logoMediaId: true, faviconMediaId: true },
|
||||
select: { logoMediaId: true, logoDarkMediaId: true, faviconMediaId: true },
|
||||
}),
|
||||
]);
|
||||
|
||||
@@ -285,6 +286,7 @@ export class MediaService {
|
||||
for (const row of sliders) addId(row.image_media_id);
|
||||
for (const row of staticItems) addId(row.image_media_id);
|
||||
addId(business?.logoMediaId);
|
||||
addId(business?.logoDarkMediaId);
|
||||
addId(business?.faviconMediaId);
|
||||
|
||||
const deletableIds = uniqueIds.filter((id) => !stillLinked.has(id.toString()));
|
||||
|
||||
@@ -22,6 +22,54 @@ export class RedisService {
|
||||
await this.redis.del(`otp:${cellNumber}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cross-site signup with a new password: hold the hash until OTP proves
|
||||
* phone ownership, then apply it in login-otp.
|
||||
*/
|
||||
async setPendingPasswordUpdate(
|
||||
cellNumber: string,
|
||||
payload: { passwordHash: string; businessId: string },
|
||||
ttlSeconds: number,
|
||||
): Promise<void> {
|
||||
await this.redis.set(
|
||||
`auth:pending-password:${cellNumber}`,
|
||||
JSON.stringify(payload),
|
||||
'EX',
|
||||
ttlSeconds,
|
||||
);
|
||||
}
|
||||
|
||||
async consumePendingPasswordUpdate(
|
||||
cellNumber: string,
|
||||
): Promise<{ passwordHash: string; businessId: string } | null> {
|
||||
const key = `auth:pending-password:${cellNumber}`;
|
||||
const result = await this.redis.multi().get(key).del(key).exec();
|
||||
const raw = result?.[0]?.[1];
|
||||
if (typeof raw !== 'string' || !raw) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as {
|
||||
passwordHash?: unknown;
|
||||
businessId?: unknown;
|
||||
};
|
||||
if (
|
||||
typeof parsed.passwordHash !== 'string' ||
|
||||
typeof parsed.businessId !== 'string' ||
|
||||
!parsed.passwordHash ||
|
||||
!parsed.businessId
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
passwordHash: parsed.passwordHash,
|
||||
businessId: parsed.businessId,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async setHandoffTicket(
|
||||
ticket: string,
|
||||
userId: string,
|
||||
|
||||
@@ -44,6 +44,7 @@ export class TenantService {
|
||||
where: { id: business.id },
|
||||
select: {
|
||||
logoMedia: { select: { publicUrl: true } },
|
||||
logoDarkMedia: { select: { publicUrl: true } },
|
||||
faviconMedia: { select: { publicUrl: true } },
|
||||
},
|
||||
});
|
||||
@@ -61,6 +62,7 @@ export class TenantService {
|
||||
homeCharts: settings.modules.charts,
|
||||
specialProductsSource: settings.website.specialProductsSource,
|
||||
logoUrl: media?.logoMedia?.publicUrl ?? null,
|
||||
logoDarkUrl: media?.logoDarkMedia?.publicUrl ?? null,
|
||||
faviconUrl:
|
||||
media?.faviconMedia?.publicUrl ?? media?.logoMedia?.publicUrl ?? null,
|
||||
ePayment: listPublicPaymentGateways(settings.store.ePayment),
|
||||
|
||||
@@ -169,6 +169,11 @@
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"logoDarkUrl": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "Optional logo for dark backgrounds; falls back to logoUrl when omitted."
|
||||
},
|
||||
"faviconUrl": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
|
||||
@@ -21,6 +21,7 @@ export class WebsiteBusinessInfoService {
|
||||
where: { id: business.id },
|
||||
include: {
|
||||
logoMedia: true,
|
||||
logoDarkMedia: true,
|
||||
faviconMedia: true,
|
||||
addresses: { orderBy: { createdAt: 'asc' } },
|
||||
},
|
||||
@@ -37,6 +38,7 @@ export class WebsiteBusinessInfoService {
|
||||
about: record.about ?? '',
|
||||
vision: record.vision ?? '',
|
||||
logoUrl: record.logoMedia?.publicUrl ?? null,
|
||||
logoDarkUrl: record.logoDarkMedia?.publicUrl ?? null,
|
||||
faviconUrl:
|
||||
record.faviconMedia?.publicUrl ?? record.logoMedia?.publicUrl ?? null,
|
||||
emails: normalizeEmails(record.emails),
|
||||
|
||||
Reference in New Issue
Block a user