import { BadGatewayException, Injectable, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { getShopBrandingPublicUrl, resolveShopBrandingPath } from '../../../config/uploadPaths'; import { Config } from '../../../types/Config'; import { removeFileFromDisk } from '../../../utils/removeFileFromDisk'; import { SimplexChatClient } from '../../simplex/services/SimplexChatClient'; import { UpdateNotificationsDto } from '../dto/UpdateNotificationsDto'; import { UpdateShippingNoteDto } from '../dto/UpdateShippingNoteDto'; import { UpdateSimplexLinkDto } from '../dto/UpdateSimplexLinkDto'; import { ShopSettings } from '../entities/ShopSettings'; import { SetupChecklist } from '../types/SetupChecklist'; import { ShopSettingsView } from '../types/ShopSettingsView'; import { StorefrontBranding } from '../types/StorefrontBranding'; @Injectable() export class ShopSettingsService { private readonly logger = new Logger(ShopSettingsService.name); private connectingSimplexPromise: Promise | null = null; constructor( @InjectRepository(ShopSettings) private readonly shopSettingsRepo: Repository, private readonly configService: ConfigService, private readonly simplexChatClient: SimplexChatClient ) {} async getView(): Promise { const settings = await this.findSettings(); return this.toView(settings); } async getStorefrontBranding(): Promise { const settings = await this.findSettings(); const logoUrl = settings.logoStorageKey ? getShopBrandingPublicUrl(settings.logoStorageKey) : null; const faviconUrl = settings.faviconStorageKey ? getShopBrandingPublicUrl(settings.faviconStorageKey) : null; return { logoUrl, faviconUrl, simplexLink: settings.simplexLink, shippingNote: settings.shippingNote }; } async findSettings(): Promise { const [settings] = await this.shopSettingsRepo.find({ take: 1 }); if (!settings) { throw new Error('Shop settings have not been initialized'); } return settings; } async updateSimplexLink({ simplexLink }: UpdateSimplexLinkDto): Promise { const settings = await this.findSettings(); await this.shopSettingsRepo.update(settings.id, { simplexLink: simplexLink.trim() }); return this.getView(); } async updateShippingNote({ shippingNote }: UpdateShippingNoteDto): Promise { const settings = await this.findSettings(); await this.shopSettingsRepo.update(settings.id, { shippingNote: shippingNote.trim() }); return this.getView(); } async updateNotifications({ notificationsEnabled, notifyOnNewOrder, notifyOnOrderMessage }: UpdateNotificationsDto): Promise { const settings = await this.findSettings(); await this.shopSettingsRepo.update(settings.id, { notificationsEnabled, notifyOnNewOrder, notifyOnOrderMessage }); return this.getView(); } async uploadLogo(filename: string): Promise { const settings = await this.findSettings(); const previousLogoStorageKey = settings.logoStorageKey; await this.shopSettingsRepo.update(settings.id, { logoStorageKey: filename }); if (previousLogoStorageKey) { const previousLogoPath = resolveShopBrandingPath(previousLogoStorageKey); await removeFileFromDisk(previousLogoPath, ShopSettingsService.name); } return this.getView(); } async uploadFavicon(filename: string): Promise { const settings = await this.findSettings(); const previousFaviconStorageKey = settings.faviconStorageKey; await this.shopSettingsRepo.update(settings.id, { faviconStorageKey: filename }); if (previousFaviconStorageKey) { const previousFaviconPath = resolveShopBrandingPath(previousFaviconStorageKey); await removeFileFromDisk(previousFaviconPath, ShopSettingsService.name); } return this.getView(); } async connectSimplexNotifications(simplexNotificationLink: string): Promise { if (this.connectingSimplexPromise) { return this.connectingSimplexPromise; } this.connectingSimplexPromise = this.runConnectSimplexNotifications(simplexNotificationLink).finally(() => { this.connectingSimplexPromise = null; }); return this.connectingSimplexPromise; } private async runConnectSimplexNotifications(simplexNotificationLink: string): Promise { const trimmedLink = simplexNotificationLink.trim(); try { await this.updateSimplexNotificationLink(trimmedLink); const contactId = await this.simplexChatClient.connect(trimmedLink); await this.setSimplexNotificationContactId(contactId); this.logger.log(`SimpleX notification contact ready (id=${contactId})`); return this.getView(); } catch { await this.setSimplexNotificationContactId(null); this.logger.warn('Failed to connect SimpleX notification contact'); throw new BadGatewayException('Failed to connect to SimpleX'); } } async updateSimplexNotificationLink(simplexNotificationLink: string): Promise { const settings = await this.findSettings(); const trimmed = simplexNotificationLink.trim(); const linkChanged = settings.simplexNotificationLink !== trimmed; await this.shopSettingsRepo.update(settings.id, { simplexNotificationLink: trimmed, ...(linkChanged ? { simplexNotificationContactId: null } : {}) }); } private async setSimplexNotificationContactId(contactId: number | null): Promise { const settings = await this.findSettings(); await this.shopSettingsRepo.update(settings.id, { simplexNotificationContactId: contactId }); } private toView({ id, logoStorageKey, faviconStorageKey, simplexLink, simplexNotificationLink, shippingNote, notificationsEnabled, notifyOnNewOrder, notifyOnOrderMessage, simplexNotificationContactId, createdAt, updatedAt }: ShopSettings): ShopSettingsView { const setupChecklist = this.buildSetupChecklist({ logoStorageKey, faviconStorageKey, simplexLink, shippingNote }); const { shopName, shopFiatCurrency, monero, bitcoin } = this.configService.get( 'shopSettings' ) as Config['shopSettings']; const logoUrl = logoStorageKey ? getShopBrandingPublicUrl(logoStorageKey) : null; const faviconUrl = faviconStorageKey ? getShopBrandingPublicUrl(faviconStorageKey) : null; const isSetupComplete = this.isSetupComplete(setupChecklist); return { id, shopName, shopFiatCurrency, monero, bitcoin, logoUrl, faviconUrl, simplexLink, simplexNotificationLink, shippingNote, notificationsEnabled, notifyOnNewOrder, notifyOnOrderMessage, simplexNotificationConnected: simplexNotificationContactId !== null, isSetupComplete, setupChecklist, createdAt, updatedAt }; } private buildSetupChecklist({ logoStorageKey, faviconStorageKey, simplexLink, shippingNote }: Pick): SetupChecklist { const { validation: { shippingNoteMinLength } } = this.configService.get('app') as Config['app']; return { logo: logoStorageKey !== null, favicon: faviconStorageKey !== null, simplexLink: (simplexLink?.length ?? 0) > 0, shippingNote: (shippingNote?.length ?? 0) >= shippingNoteMinLength }; } private isSetupComplete(checklist: SetupChecklist): boolean { return checklist.logo && checklist.favicon && checklist.simplexLink && checklist.shippingNote; } }