Files
nullcart/backend/src/modules/shopSettings/services/ShopSettingsService.ts
T

252 lines
8.5 KiB
TypeScript

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<ShopSettingsView> | null = null;
constructor(
@InjectRepository(ShopSettings)
private readonly shopSettingsRepo: Repository<ShopSettings>,
private readonly configService: ConfigService,
private readonly simplexChatClient: SimplexChatClient
) {}
async getView(): Promise<ShopSettingsView> {
const settings = await this.findSettings();
return this.toView(settings);
}
async getStorefrontBranding(): Promise<StorefrontBranding> {
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<ShopSettings> {
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<ShopSettingsView> {
const settings = await this.findSettings();
await this.shopSettingsRepo.update(settings.id, {
simplexLink: simplexLink.trim()
});
return this.getView();
}
async updateShippingNote({ shippingNote }: UpdateShippingNoteDto): Promise<ShopSettingsView> {
const settings = await this.findSettings();
await this.shopSettingsRepo.update(settings.id, {
shippingNote: shippingNote.trim()
});
return this.getView();
}
async updateNotifications({
notificationsEnabled,
notifyOnNewOrder,
notifyOnOrderMessage
}: UpdateNotificationsDto): Promise<ShopSettingsView> {
const settings = await this.findSettings();
await this.shopSettingsRepo.update(settings.id, {
notificationsEnabled,
notifyOnNewOrder,
notifyOnOrderMessage
});
return this.getView();
}
async uploadLogo(filename: string): Promise<ShopSettingsView> {
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<ShopSettingsView> {
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<ShopSettingsView> {
if (this.connectingSimplexPromise) {
return this.connectingSimplexPromise;
}
this.connectingSimplexPromise = this.runConnectSimplexNotifications(simplexNotificationLink).finally(() => {
this.connectingSimplexPromise = null;
});
return this.connectingSimplexPromise;
}
private async runConnectSimplexNotifications(simplexNotificationLink: string): Promise<ShopSettingsView> {
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<void> {
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<void> {
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<ShopSettings, 'logoStorageKey' | 'faviconStorageKey' | 'simplexLink' | 'shippingNote'>): 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;
}
}