init
This commit is contained in:
@@ -0,0 +1,248 @@
|
||||
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 } = 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,
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user