Fix e-payment settings payload, cart race, and media/content helpers.
Stop returning passwordSet (forbidNonWhitelisted), upsert carts safely, and harden related media/product/workshop flows. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
ad6f9dcf1d
commit
20cc0bbd78
@@ -10,7 +10,6 @@ import { BusinessSettings } from './business-settings.types';
|
||||
import {
|
||||
mergeBusinessSettings,
|
||||
mergeEPaymentSettings,
|
||||
mellatPasswordSet,
|
||||
normalizeBusinessSettings,
|
||||
normalizeEnabledBusinessModules,
|
||||
normalizeEPaymentSettings,
|
||||
@@ -241,23 +240,7 @@ export class BusinessSettingsService {
|
||||
}
|
||||
|
||||
private toClientSettings(settings: BusinessSettings) {
|
||||
const sanitized = sanitizeBusinessSettingsForClient(settings);
|
||||
return {
|
||||
...sanitized,
|
||||
store: {
|
||||
...sanitized.store,
|
||||
ePayment: {
|
||||
...sanitized.store.ePayment,
|
||||
gateways: {
|
||||
...sanitized.store.ePayment.gateways,
|
||||
mellat: {
|
||||
...sanitized.store.ePayment.gateways.mellat,
|
||||
passwordSet: mellatPasswordSet(settings),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
return sanitizeBusinessSettingsForClient(settings);
|
||||
}
|
||||
|
||||
private async assertPermission(
|
||||
|
||||
@@ -54,7 +54,7 @@ export type MellatGatewaySettings = {
|
||||
enabled: boolean;
|
||||
terminalId: string;
|
||||
username: string;
|
||||
/** Stored secret — redacted in API responses; use passwordSet instead. */
|
||||
/** Stored secret — never returned in API responses (always empty string). */
|
||||
password: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -249,8 +249,6 @@ export function sanitizeBusinessSettingsForClient(
|
||||
mellat: {
|
||||
...mellat,
|
||||
password: '',
|
||||
// Clients treat non-empty passwordSet via a parallel field — see
|
||||
// get/update response shaping in BusinessSettingsService.
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -258,10 +256,6 @@ export function sanitizeBusinessSettingsForClient(
|
||||
};
|
||||
}
|
||||
|
||||
export function mellatPasswordSet(settings: BusinessSettings): boolean {
|
||||
return settings.store.ePayment.gateways.mellat.password.length > 0;
|
||||
}
|
||||
|
||||
/** Whether a gateway is enabled and has enough credentials to initiate. */
|
||||
export function isGatewayReady(
|
||||
settings: EPaymentSettings,
|
||||
|
||||
@@ -279,21 +279,16 @@ export class CartService {
|
||||
}
|
||||
|
||||
private async getOrCreateCart(businessId: bigint, userId: bigint) {
|
||||
const existing = await this.prisma.cart.findUnique({
|
||||
// Concurrent GETs (e.g. React Strict Mode) can race on create; upsert is safe.
|
||||
const cart = await this.prisma.cart.upsert({
|
||||
where: {
|
||||
businessId_userId: { businessId, userId },
|
||||
},
|
||||
create: { businessId, userId },
|
||||
update: {},
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
return this.loadCart(existing.id);
|
||||
}
|
||||
|
||||
const created = await this.prisma.cart.create({
|
||||
data: { businessId, userId },
|
||||
});
|
||||
|
||||
return this.loadCart(created.id);
|
||||
return this.loadCart(cart.id);
|
||||
}
|
||||
|
||||
private async loadCart(cartId: bigint): Promise<CartWithItems> {
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { MediaType } from '@prisma/client';
|
||||
import { MediaEntityType, MediaType } from '@prisma/client';
|
||||
import { randomUUID } from 'crypto';
|
||||
import * as path from 'path';
|
||||
import sharp from 'sharp';
|
||||
@@ -209,6 +209,7 @@ export class MediaService {
|
||||
blogs,
|
||||
videos,
|
||||
instructions,
|
||||
workshops,
|
||||
brands,
|
||||
userProducts,
|
||||
sliders,
|
||||
@@ -235,6 +236,10 @@ export class MediaService {
|
||||
where: { business_id: businessId, featured_media_id: { in: uniqueIds } },
|
||||
select: { featured_media_id: true },
|
||||
}),
|
||||
this.prisma.workshops.findMany({
|
||||
where: { business_id: businessId, featured_media_id: { in: uniqueIds } },
|
||||
select: { featured_media_id: true },
|
||||
}),
|
||||
this.prisma.brand.findMany({
|
||||
where: { businessId, imageMediaId: { in: uniqueIds } },
|
||||
select: { imageMediaId: true },
|
||||
@@ -274,6 +279,7 @@ export class MediaService {
|
||||
for (const row of blogs) addId(row.featured_media_id);
|
||||
for (const row of videos) addId(row.featured_media_id);
|
||||
for (const row of instructions) addId(row.featured_media_id);
|
||||
for (const row of workshops) addId(row.featured_media_id);
|
||||
for (const row of brands) addId(row.imageMediaId);
|
||||
for (const row of userProducts) addId(row.featuredMediaId);
|
||||
for (const row of sliders) addId(row.image_media_id);
|
||||
@@ -304,6 +310,53 @@ export class MediaService {
|
||||
return mediaRows.length;
|
||||
}
|
||||
|
||||
async galleryMediaIds(
|
||||
businessId: bigint,
|
||||
entityType: MediaEntityType,
|
||||
entityId: bigint,
|
||||
): Promise<bigint[]> {
|
||||
const rows = await this.prisma.mediaAttachment.findMany({
|
||||
where: { businessId, entityType, entityId, isFeatured: false },
|
||||
select: { mediaId: true },
|
||||
});
|
||||
return rows.map((row) => row.mediaId);
|
||||
}
|
||||
|
||||
async allLinkedMediaIds(
|
||||
businessId: bigint,
|
||||
entityType: MediaEntityType,
|
||||
entityId: bigint,
|
||||
featuredMediaId?: bigint | null,
|
||||
): Promise<bigint[]> {
|
||||
const rows = await this.prisma.mediaAttachment.findMany({
|
||||
where: { businessId, entityType, entityId },
|
||||
select: { mediaId: true },
|
||||
});
|
||||
const ids = rows.map((row) => row.mediaId);
|
||||
if (featuredMediaId != null) ids.push(featuredMediaId);
|
||||
return ids;
|
||||
}
|
||||
|
||||
idsNotIn(
|
||||
previous: bigint[],
|
||||
next: Array<bigint | null | undefined>,
|
||||
): bigint[] {
|
||||
const keep = new Set(
|
||||
next
|
||||
.filter((id): id is bigint => id != null)
|
||||
.map((id) => id.toString()),
|
||||
);
|
||||
const seen = new Set<string>();
|
||||
const dropped: bigint[] = [];
|
||||
for (const id of previous) {
|
||||
const key = id.toString();
|
||||
if (keep.has(key) || seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
dropped.push(id);
|
||||
}
|
||||
return dropped;
|
||||
}
|
||||
|
||||
private async uploadOne(
|
||||
businessId: bigint,
|
||||
file: Express.Multer.File,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AuthModule } from '../auth/auth.module';
|
||||
import { BusinessSettingsModule } from '../business-settings/business-settings.module';
|
||||
import { MediaModule } from '../media/media.module';
|
||||
import { SitemapModule } from '../sitemap/sitemap.module';
|
||||
import { TenantModule } from '../tenant/tenant.module';
|
||||
import {
|
||||
@@ -10,7 +11,7 @@ import {
|
||||
import { PortfoliosService } from './portfolios.service';
|
||||
|
||||
@Module({
|
||||
imports: [AuthModule, BusinessSettingsModule, SitemapModule, TenantModule],
|
||||
imports: [AuthModule, BusinessSettingsModule, MediaModule, SitemapModule, TenantModule],
|
||||
controllers: [PortfoliosController, PublicPortfoliosController],
|
||||
providers: [PortfoliosService],
|
||||
})
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import { AuthUser } from '../auth/auth.types';
|
||||
import { PermissionsService } from '../auth/permissions.service';
|
||||
import { BusinessSettingsService } from '../business-settings/business-settings.service';
|
||||
import { MediaService } from '../media/media.service';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { SitemapService } from '../sitemap/sitemap.service';
|
||||
import { TenantService } from '../tenant/tenant.service';
|
||||
@@ -56,6 +57,7 @@ export class PortfoliosService {
|
||||
private readonly tenant: TenantService,
|
||||
private readonly businessSettings: BusinessSettingsService,
|
||||
private readonly sitemap: SitemapService,
|
||||
private readonly mediaService: MediaService,
|
||||
) {}
|
||||
|
||||
async list(businessIdRaw: string, query: ListPortfoliosDto, actor: AuthUser) {
|
||||
@@ -285,6 +287,13 @@ export class PortfoliosService {
|
||||
? await this.nextSortOrder(businessId)
|
||||
: undefined;
|
||||
|
||||
const previousGalleryIds = await this.mediaService.galleryMediaIds(
|
||||
businessId,
|
||||
MediaEntityType.portfolio,
|
||||
portfolioId,
|
||||
);
|
||||
let nextGalleryIds: bigint[] | undefined;
|
||||
|
||||
const updated = await this.prisma.$transaction(async (tx) => {
|
||||
const data: Prisma.portfoliosUncheckedUpdateInput = {
|
||||
slug,
|
||||
@@ -353,6 +362,7 @@ export class PortfoliosService {
|
||||
businessId,
|
||||
dto.galleryMediaIds,
|
||||
);
|
||||
nextGalleryIds = galleryMediaIds;
|
||||
await this.syncGalleryAttachments(
|
||||
tx,
|
||||
businessId,
|
||||
@@ -366,6 +376,21 @@ export class PortfoliosService {
|
||||
|
||||
await this.sitemap.invalidateForBusiness(businessId);
|
||||
|
||||
const dropped: bigint[] = [];
|
||||
if (nextGalleryIds) {
|
||||
dropped.push(
|
||||
...this.mediaService.idsNotIn(previousGalleryIds, nextGalleryIds),
|
||||
);
|
||||
}
|
||||
if (
|
||||
featuredMediaId !== undefined &&
|
||||
existing.featured_media_id != null &&
|
||||
existing.featured_media_id !== featuredMediaId
|
||||
) {
|
||||
dropped.push(existing.featured_media_id);
|
||||
}
|
||||
await this.mediaService.deleteUnused(businessId, dropped);
|
||||
|
||||
return {
|
||||
message: 'Portfolio updated successfully',
|
||||
portfolio: await this.serializePortfolio(updated),
|
||||
@@ -389,6 +414,13 @@ export class PortfoliosService {
|
||||
throw new NotFoundException('Portfolio not found');
|
||||
}
|
||||
|
||||
const mediaIds = await this.mediaService.allLinkedMediaIds(
|
||||
businessId,
|
||||
MediaEntityType.portfolio,
|
||||
portfolioId,
|
||||
existing.featured_media_id,
|
||||
);
|
||||
|
||||
await this.prisma.$transaction([
|
||||
this.prisma.comment.deleteMany({
|
||||
where: {
|
||||
@@ -415,6 +447,7 @@ export class PortfoliosService {
|
||||
]);
|
||||
|
||||
await this.sitemap.invalidateForBusiness(businessId);
|
||||
await this.mediaService.deleteUnused(businessId, mediaIds);
|
||||
|
||||
return { message: 'Portfolio deleted successfully' };
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
|
||||
import { AuthModule } from '../auth/auth.module';
|
||||
import { BrandsModule } from '../brands/brands.module';
|
||||
import { CategoriesModule } from '../categories/categories.module';
|
||||
import { MediaModule } from '../media/media.module';
|
||||
import { SitemapModule } from '../sitemap/sitemap.module';
|
||||
import { TenantModule } from '../tenant/tenant.module';
|
||||
import { ProductAiService } from './product-ai.service';
|
||||
@@ -11,7 +12,7 @@ import { ProductsController, PublicProductsController } from './products.control
|
||||
import { ProductsService } from './products.service';
|
||||
|
||||
@Module({
|
||||
imports: [AuthModule, BrandsModule, CategoriesModule, SitemapModule, TenantModule],
|
||||
imports: [AuthModule, BrandsModule, CategoriesModule, MediaModule, SitemapModule, TenantModule],
|
||||
controllers: [ProductsController, PublicProductsController],
|
||||
providers: [
|
||||
ProductsService,
|
||||
|
||||
@@ -8,6 +8,7 @@ import { ContentStatus, MediaEntityType, Prisma } from '@prisma/client';
|
||||
import { AuthUser } from '../auth/auth.types';
|
||||
import { PermissionsService } from '../auth/permissions.service';
|
||||
import { BrandsService } from '../brands/brands.service';
|
||||
import { MediaService } from '../media/media.service';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { SitemapService } from '../sitemap/sitemap.service';
|
||||
import { TenantService } from '../tenant/tenant.service';
|
||||
@@ -54,6 +55,7 @@ export class ProductsService {
|
||||
private readonly brands: BrandsService,
|
||||
private readonly tenant: TenantService,
|
||||
private readonly sitemap: SitemapService,
|
||||
private readonly mediaService: MediaService,
|
||||
) {}
|
||||
|
||||
async list(businessIdRaw: string, query: ListProductsDto, actor: AuthUser) {
|
||||
@@ -390,6 +392,13 @@ export class ProductsService {
|
||||
}
|
||||
}
|
||||
|
||||
const previousGalleryIds = await this.mediaService.galleryMediaIds(
|
||||
businessId,
|
||||
MediaEntityType.product,
|
||||
productId,
|
||||
);
|
||||
let nextGalleryIds: bigint[] | undefined;
|
||||
|
||||
const updated = await this.prisma.$transaction(async (tx) => {
|
||||
const product = await tx.product.update({
|
||||
where: { id: productId },
|
||||
@@ -440,6 +449,7 @@ export class ProductsService {
|
||||
businessId,
|
||||
dto.galleryMediaIds,
|
||||
);
|
||||
nextGalleryIds = galleryMediaIds;
|
||||
await this.syncGalleryAttachments(
|
||||
tx,
|
||||
businessId,
|
||||
@@ -453,6 +463,21 @@ export class ProductsService {
|
||||
|
||||
await this.sitemap.invalidateForBusiness(businessId);
|
||||
|
||||
const dropped: bigint[] = [];
|
||||
if (nextGalleryIds) {
|
||||
dropped.push(
|
||||
...this.mediaService.idsNotIn(previousGalleryIds, nextGalleryIds),
|
||||
);
|
||||
}
|
||||
if (
|
||||
featuredMediaId !== undefined &&
|
||||
existing.featuredMediaId != null &&
|
||||
existing.featuredMediaId !== featuredMediaId
|
||||
) {
|
||||
dropped.push(existing.featuredMediaId);
|
||||
}
|
||||
await this.mediaService.deleteUnused(businessId, dropped);
|
||||
|
||||
return {
|
||||
message: 'Product updated successfully',
|
||||
product: await this.serializeProduct(updated),
|
||||
@@ -472,6 +497,13 @@ export class ProductsService {
|
||||
throw new NotFoundException('Product not found');
|
||||
}
|
||||
|
||||
const mediaIds = await this.mediaService.allLinkedMediaIds(
|
||||
businessId,
|
||||
MediaEntityType.product,
|
||||
productId,
|
||||
existing.featuredMediaId,
|
||||
);
|
||||
|
||||
await this.prisma.$transaction([
|
||||
this.prisma.mediaAttachment.deleteMany({
|
||||
where: {
|
||||
@@ -491,6 +523,7 @@ export class ProductsService {
|
||||
]);
|
||||
|
||||
await this.sitemap.invalidateForBusiness(businessId);
|
||||
await this.mediaService.deleteUnused(businessId, mediaIds);
|
||||
|
||||
return { message: 'Product deleted successfully' };
|
||||
}
|
||||
|
||||
@@ -456,7 +456,13 @@ export class UserProductsService {
|
||||
|
||||
const existing = await this.prisma.userProduct.findFirst({
|
||||
where: { id: productId, businessId, userId: actor.id },
|
||||
select: { id: true, title: true, slug: true, metadata: true },
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
slug: true,
|
||||
metadata: true,
|
||||
featuredMediaId: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
@@ -475,6 +481,7 @@ export class UserProductsService {
|
||||
title: string;
|
||||
slug: string;
|
||||
metadata: unknown;
|
||||
featuredMediaId: bigint | null;
|
||||
},
|
||||
) {
|
||||
const titleFa = dto.titleFa.trim();
|
||||
@@ -525,6 +532,12 @@ export class UserProductsService {
|
||||
);
|
||||
}
|
||||
|
||||
const previousGalleryIds = await this.mediaService.galleryMediaIds(
|
||||
businessId,
|
||||
MediaEntityType.user_product,
|
||||
productId,
|
||||
);
|
||||
|
||||
const updated = await this.prisma.$transaction(async (tx) => {
|
||||
const product = await tx.userProduct.update({
|
||||
where: { id: productId },
|
||||
@@ -594,6 +607,20 @@ export class UserProductsService {
|
||||
select: { id: true, name: true, nameFa: true },
|
||||
});
|
||||
|
||||
const dropped: bigint[] = [];
|
||||
if (galleryMediaIds !== null) {
|
||||
dropped.push(
|
||||
...this.mediaService.idsNotIn(previousGalleryIds, galleryMediaIds),
|
||||
);
|
||||
}
|
||||
if (
|
||||
existing.featuredMediaId != null &&
|
||||
existing.featuredMediaId !== featuredMediaId
|
||||
) {
|
||||
dropped.push(existing.featuredMediaId);
|
||||
}
|
||||
await this.mediaService.deleteUnused(businessId, dropped);
|
||||
|
||||
return {
|
||||
message: 'User product updated successfully',
|
||||
product: this.serializeAdminListItem(updated, category ?? undefined),
|
||||
@@ -619,14 +646,28 @@ export class UserProductsService {
|
||||
|
||||
const product = await this.prisma.userProduct.findFirst({
|
||||
where: { id: productId, businessId, userId: actor.id },
|
||||
select: { id: true },
|
||||
select: { id: true, featuredMediaId: true },
|
||||
});
|
||||
|
||||
if (!product) {
|
||||
throw new NotFoundException('User product not found');
|
||||
}
|
||||
|
||||
const mediaIds = await this.mediaService.allLinkedMediaIds(
|
||||
businessId,
|
||||
MediaEntityType.user_product,
|
||||
productId,
|
||||
product.featuredMediaId,
|
||||
);
|
||||
|
||||
await this.prisma.$transaction(async (tx) => {
|
||||
await tx.mediaAttachment.deleteMany({
|
||||
where: {
|
||||
businessId,
|
||||
entityType: MediaEntityType.user_product,
|
||||
entityId: productId,
|
||||
},
|
||||
});
|
||||
await tx.categoryAssignment.deleteMany({
|
||||
where: {
|
||||
businessId,
|
||||
@@ -637,6 +678,8 @@ export class UserProductsService {
|
||||
await tx.userProduct.delete({ where: { id: productId } });
|
||||
});
|
||||
|
||||
await this.mediaService.deleteUnused(businessId, mediaIds);
|
||||
|
||||
return { message: 'User product deleted successfully' };
|
||||
}
|
||||
|
||||
@@ -784,7 +827,14 @@ export class UserProductsService {
|
||||
|
||||
const existing = await this.prisma.userProduct.findFirst({
|
||||
where: { id: productId, businessId },
|
||||
select: { id: true, title: true, slug: true, metadata: true, userId: true },
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
slug: true,
|
||||
metadata: true,
|
||||
userId: true,
|
||||
featuredMediaId: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AuthModule } from '../auth/auth.module';
|
||||
import { MediaModule } from '../media/media.module';
|
||||
import { TenantModule } from '../tenant/tenant.module';
|
||||
import {
|
||||
PublicWorkshopsController,
|
||||
@@ -8,7 +9,7 @@ import {
|
||||
import { WorkshopsService } from './workshops.service';
|
||||
|
||||
@Module({
|
||||
imports: [AuthModule, TenantModule],
|
||||
imports: [AuthModule, MediaModule, TenantModule],
|
||||
controllers: [WorkshopsController, PublicWorkshopsController],
|
||||
providers: [WorkshopsService],
|
||||
})
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
} from '@prisma/client';
|
||||
import { AuthUser } from '../auth/auth.types';
|
||||
import { PermissionsService } from '../auth/permissions.service';
|
||||
import { MediaService } from '../media/media.service';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { TenantService } from '../tenant/tenant.service';
|
||||
import {
|
||||
@@ -61,6 +62,7 @@ export class WorkshopsService {
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly permissions: PermissionsService,
|
||||
private readonly tenant: TenantService,
|
||||
private readonly mediaService: MediaService,
|
||||
) {}
|
||||
|
||||
async list(businessIdRaw: string, query: ListWorkshopsDto, actor: AuthUser) {
|
||||
@@ -278,6 +280,13 @@ export class WorkshopsService {
|
||||
}
|
||||
}
|
||||
|
||||
const previousGalleryIds = await this.mediaService.galleryMediaIds(
|
||||
businessId,
|
||||
MediaEntityType.workshop,
|
||||
workshopId,
|
||||
);
|
||||
let nextGalleryIds: bigint[] | undefined;
|
||||
|
||||
const updated = await this.prisma.$transaction(async (tx) => {
|
||||
const workshop = await tx.workshops.update({
|
||||
where: { id: workshopId },
|
||||
@@ -341,6 +350,7 @@ export class WorkshopsService {
|
||||
businessId,
|
||||
dto.galleryMediaIds,
|
||||
);
|
||||
nextGalleryIds = galleryMediaIds;
|
||||
await this.syncGalleryAttachments(
|
||||
tx,
|
||||
businessId,
|
||||
@@ -352,6 +362,21 @@ export class WorkshopsService {
|
||||
return workshop;
|
||||
});
|
||||
|
||||
const dropped: bigint[] = [];
|
||||
if (nextGalleryIds) {
|
||||
dropped.push(
|
||||
...this.mediaService.idsNotIn(previousGalleryIds, nextGalleryIds),
|
||||
);
|
||||
}
|
||||
if (
|
||||
featuredMediaId !== undefined &&
|
||||
existing.featured_media_id != null &&
|
||||
existing.featured_media_id !== featuredMediaId
|
||||
) {
|
||||
dropped.push(existing.featured_media_id);
|
||||
}
|
||||
await this.mediaService.deleteUnused(businessId, dropped);
|
||||
|
||||
return {
|
||||
message: 'Workshop updated successfully',
|
||||
workshop: await this.serializeWorkshop(updated),
|
||||
@@ -371,6 +396,13 @@ export class WorkshopsService {
|
||||
throw new NotFoundException('Workshop not found');
|
||||
}
|
||||
|
||||
const mediaIds = await this.mediaService.allLinkedMediaIds(
|
||||
businessId,
|
||||
MediaEntityType.workshop,
|
||||
workshopId,
|
||||
existing.featured_media_id,
|
||||
);
|
||||
|
||||
await this.prisma.$transaction([
|
||||
this.prisma.workshop_registrations.deleteMany({
|
||||
where: { workshop_id: workshopId, business_id: businessId },
|
||||
@@ -392,6 +424,8 @@ export class WorkshopsService {
|
||||
this.prisma.workshops.delete({ where: { id: workshopId } }),
|
||||
]);
|
||||
|
||||
await this.mediaService.deleteUnused(businessId, mediaIds);
|
||||
|
||||
return { message: 'Workshop deleted successfully' };
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user