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 {
|
import {
|
||||||
mergeBusinessSettings,
|
mergeBusinessSettings,
|
||||||
mergeEPaymentSettings,
|
mergeEPaymentSettings,
|
||||||
mellatPasswordSet,
|
|
||||||
normalizeBusinessSettings,
|
normalizeBusinessSettings,
|
||||||
normalizeEnabledBusinessModules,
|
normalizeEnabledBusinessModules,
|
||||||
normalizeEPaymentSettings,
|
normalizeEPaymentSettings,
|
||||||
@@ -241,23 +240,7 @@ export class BusinessSettingsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private toClientSettings(settings: BusinessSettings) {
|
private toClientSettings(settings: BusinessSettings) {
|
||||||
const sanitized = sanitizeBusinessSettingsForClient(settings);
|
return 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),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async assertPermission(
|
private async assertPermission(
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export type MellatGatewaySettings = {
|
|||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
terminalId: string;
|
terminalId: string;
|
||||||
username: string;
|
username: string;
|
||||||
/** Stored secret — redacted in API responses; use passwordSet instead. */
|
/** Stored secret — never returned in API responses (always empty string). */
|
||||||
password: string;
|
password: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -249,8 +249,6 @@ export function sanitizeBusinessSettingsForClient(
|
|||||||
mellat: {
|
mellat: {
|
||||||
...mellat,
|
...mellat,
|
||||||
password: '',
|
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. */
|
/** Whether a gateway is enabled and has enough credentials to initiate. */
|
||||||
export function isGatewayReady(
|
export function isGatewayReady(
|
||||||
settings: EPaymentSettings,
|
settings: EPaymentSettings,
|
||||||
|
|||||||
@@ -279,21 +279,16 @@ export class CartService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async getOrCreateCart(businessId: bigint, userId: bigint) {
|
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: {
|
where: {
|
||||||
businessId_userId: { businessId, userId },
|
businessId_userId: { businessId, userId },
|
||||||
},
|
},
|
||||||
|
create: { businessId, userId },
|
||||||
|
update: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (existing) {
|
return this.loadCart(cart.id);
|
||||||
return this.loadCart(existing.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
const created = await this.prisma.cart.create({
|
|
||||||
data: { businessId, userId },
|
|
||||||
});
|
|
||||||
|
|
||||||
return this.loadCart(created.id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async loadCart(cartId: bigint): Promise<CartWithItems> {
|
private async loadCart(cartId: bigint): Promise<CartWithItems> {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
NotFoundException,
|
NotFoundException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { MediaType } from '@prisma/client';
|
import { MediaEntityType, MediaType } from '@prisma/client';
|
||||||
import { randomUUID } from 'crypto';
|
import { randomUUID } from 'crypto';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import sharp from 'sharp';
|
import sharp from 'sharp';
|
||||||
@@ -209,6 +209,7 @@ export class MediaService {
|
|||||||
blogs,
|
blogs,
|
||||||
videos,
|
videos,
|
||||||
instructions,
|
instructions,
|
||||||
|
workshops,
|
||||||
brands,
|
brands,
|
||||||
userProducts,
|
userProducts,
|
||||||
sliders,
|
sliders,
|
||||||
@@ -235,6 +236,10 @@ export class MediaService {
|
|||||||
where: { business_id: businessId, featured_media_id: { in: uniqueIds } },
|
where: { business_id: businessId, featured_media_id: { in: uniqueIds } },
|
||||||
select: { featured_media_id: true },
|
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({
|
this.prisma.brand.findMany({
|
||||||
where: { businessId, imageMediaId: { in: uniqueIds } },
|
where: { businessId, imageMediaId: { in: uniqueIds } },
|
||||||
select: { imageMediaId: true },
|
select: { imageMediaId: true },
|
||||||
@@ -274,6 +279,7 @@ export class MediaService {
|
|||||||
for (const row of blogs) addId(row.featured_media_id);
|
for (const row of blogs) addId(row.featured_media_id);
|
||||||
for (const row of videos) 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 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 brands) addId(row.imageMediaId);
|
||||||
for (const row of userProducts) addId(row.featuredMediaId);
|
for (const row of userProducts) addId(row.featuredMediaId);
|
||||||
for (const row of sliders) addId(row.image_media_id);
|
for (const row of sliders) addId(row.image_media_id);
|
||||||
@@ -304,6 +310,53 @@ export class MediaService {
|
|||||||
return mediaRows.length;
|
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(
|
private async uploadOne(
|
||||||
businessId: bigint,
|
businessId: bigint,
|
||||||
file: Express.Multer.File,
|
file: Express.Multer.File,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { AuthModule } from '../auth/auth.module';
|
import { AuthModule } from '../auth/auth.module';
|
||||||
import { BusinessSettingsModule } from '../business-settings/business-settings.module';
|
import { BusinessSettingsModule } from '../business-settings/business-settings.module';
|
||||||
|
import { MediaModule } from '../media/media.module';
|
||||||
import { SitemapModule } from '../sitemap/sitemap.module';
|
import { SitemapModule } from '../sitemap/sitemap.module';
|
||||||
import { TenantModule } from '../tenant/tenant.module';
|
import { TenantModule } from '../tenant/tenant.module';
|
||||||
import {
|
import {
|
||||||
@@ -10,7 +11,7 @@ import {
|
|||||||
import { PortfoliosService } from './portfolios.service';
|
import { PortfoliosService } from './portfolios.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [AuthModule, BusinessSettingsModule, SitemapModule, TenantModule],
|
imports: [AuthModule, BusinessSettingsModule, MediaModule, SitemapModule, TenantModule],
|
||||||
controllers: [PortfoliosController, PublicPortfoliosController],
|
controllers: [PortfoliosController, PublicPortfoliosController],
|
||||||
providers: [PortfoliosService],
|
providers: [PortfoliosService],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
import { AuthUser } from '../auth/auth.types';
|
import { AuthUser } from '../auth/auth.types';
|
||||||
import { PermissionsService } from '../auth/permissions.service';
|
import { PermissionsService } from '../auth/permissions.service';
|
||||||
import { BusinessSettingsService } from '../business-settings/business-settings.service';
|
import { BusinessSettingsService } from '../business-settings/business-settings.service';
|
||||||
|
import { MediaService } from '../media/media.service';
|
||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
import { SitemapService } from '../sitemap/sitemap.service';
|
import { SitemapService } from '../sitemap/sitemap.service';
|
||||||
import { TenantService } from '../tenant/tenant.service';
|
import { TenantService } from '../tenant/tenant.service';
|
||||||
@@ -56,6 +57,7 @@ export class PortfoliosService {
|
|||||||
private readonly tenant: TenantService,
|
private readonly tenant: TenantService,
|
||||||
private readonly businessSettings: BusinessSettingsService,
|
private readonly businessSettings: BusinessSettingsService,
|
||||||
private readonly sitemap: SitemapService,
|
private readonly sitemap: SitemapService,
|
||||||
|
private readonly mediaService: MediaService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async list(businessIdRaw: string, query: ListPortfoliosDto, actor: AuthUser) {
|
async list(businessIdRaw: string, query: ListPortfoliosDto, actor: AuthUser) {
|
||||||
@@ -285,6 +287,13 @@ export class PortfoliosService {
|
|||||||
? await this.nextSortOrder(businessId)
|
? await this.nextSortOrder(businessId)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
const previousGalleryIds = await this.mediaService.galleryMediaIds(
|
||||||
|
businessId,
|
||||||
|
MediaEntityType.portfolio,
|
||||||
|
portfolioId,
|
||||||
|
);
|
||||||
|
let nextGalleryIds: bigint[] | undefined;
|
||||||
|
|
||||||
const updated = await this.prisma.$transaction(async (tx) => {
|
const updated = await this.prisma.$transaction(async (tx) => {
|
||||||
const data: Prisma.portfoliosUncheckedUpdateInput = {
|
const data: Prisma.portfoliosUncheckedUpdateInput = {
|
||||||
slug,
|
slug,
|
||||||
@@ -353,6 +362,7 @@ export class PortfoliosService {
|
|||||||
businessId,
|
businessId,
|
||||||
dto.galleryMediaIds,
|
dto.galleryMediaIds,
|
||||||
);
|
);
|
||||||
|
nextGalleryIds = galleryMediaIds;
|
||||||
await this.syncGalleryAttachments(
|
await this.syncGalleryAttachments(
|
||||||
tx,
|
tx,
|
||||||
businessId,
|
businessId,
|
||||||
@@ -366,6 +376,21 @@ export class PortfoliosService {
|
|||||||
|
|
||||||
await this.sitemap.invalidateForBusiness(businessId);
|
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 {
|
return {
|
||||||
message: 'Portfolio updated successfully',
|
message: 'Portfolio updated successfully',
|
||||||
portfolio: await this.serializePortfolio(updated),
|
portfolio: await this.serializePortfolio(updated),
|
||||||
@@ -389,6 +414,13 @@ export class PortfoliosService {
|
|||||||
throw new NotFoundException('Portfolio not found');
|
throw new NotFoundException('Portfolio not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const mediaIds = await this.mediaService.allLinkedMediaIds(
|
||||||
|
businessId,
|
||||||
|
MediaEntityType.portfolio,
|
||||||
|
portfolioId,
|
||||||
|
existing.featured_media_id,
|
||||||
|
);
|
||||||
|
|
||||||
await this.prisma.$transaction([
|
await this.prisma.$transaction([
|
||||||
this.prisma.comment.deleteMany({
|
this.prisma.comment.deleteMany({
|
||||||
where: {
|
where: {
|
||||||
@@ -415,6 +447,7 @@ export class PortfoliosService {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
await this.sitemap.invalidateForBusiness(businessId);
|
await this.sitemap.invalidateForBusiness(businessId);
|
||||||
|
await this.mediaService.deleteUnused(businessId, mediaIds);
|
||||||
|
|
||||||
return { message: 'Portfolio deleted successfully' };
|
return { message: 'Portfolio deleted successfully' };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
|
|||||||
import { AuthModule } from '../auth/auth.module';
|
import { AuthModule } from '../auth/auth.module';
|
||||||
import { BrandsModule } from '../brands/brands.module';
|
import { BrandsModule } from '../brands/brands.module';
|
||||||
import { CategoriesModule } from '../categories/categories.module';
|
import { CategoriesModule } from '../categories/categories.module';
|
||||||
|
import { MediaModule } from '../media/media.module';
|
||||||
import { SitemapModule } from '../sitemap/sitemap.module';
|
import { SitemapModule } from '../sitemap/sitemap.module';
|
||||||
import { TenantModule } from '../tenant/tenant.module';
|
import { TenantModule } from '../tenant/tenant.module';
|
||||||
import { ProductAiService } from './product-ai.service';
|
import { ProductAiService } from './product-ai.service';
|
||||||
@@ -11,7 +12,7 @@ import { ProductsController, PublicProductsController } from './products.control
|
|||||||
import { ProductsService } from './products.service';
|
import { ProductsService } from './products.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [AuthModule, BrandsModule, CategoriesModule, SitemapModule, TenantModule],
|
imports: [AuthModule, BrandsModule, CategoriesModule, MediaModule, SitemapModule, TenantModule],
|
||||||
controllers: [ProductsController, PublicProductsController],
|
controllers: [ProductsController, PublicProductsController],
|
||||||
providers: [
|
providers: [
|
||||||
ProductsService,
|
ProductsService,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { ContentStatus, MediaEntityType, Prisma } from '@prisma/client';
|
|||||||
import { AuthUser } from '../auth/auth.types';
|
import { AuthUser } from '../auth/auth.types';
|
||||||
import { PermissionsService } from '../auth/permissions.service';
|
import { PermissionsService } from '../auth/permissions.service';
|
||||||
import { BrandsService } from '../brands/brands.service';
|
import { BrandsService } from '../brands/brands.service';
|
||||||
|
import { MediaService } from '../media/media.service';
|
||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
import { SitemapService } from '../sitemap/sitemap.service';
|
import { SitemapService } from '../sitemap/sitemap.service';
|
||||||
import { TenantService } from '../tenant/tenant.service';
|
import { TenantService } from '../tenant/tenant.service';
|
||||||
@@ -54,6 +55,7 @@ export class ProductsService {
|
|||||||
private readonly brands: BrandsService,
|
private readonly brands: BrandsService,
|
||||||
private readonly tenant: TenantService,
|
private readonly tenant: TenantService,
|
||||||
private readonly sitemap: SitemapService,
|
private readonly sitemap: SitemapService,
|
||||||
|
private readonly mediaService: MediaService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async list(businessIdRaw: string, query: ListProductsDto, actor: AuthUser) {
|
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 updated = await this.prisma.$transaction(async (tx) => {
|
||||||
const product = await tx.product.update({
|
const product = await tx.product.update({
|
||||||
where: { id: productId },
|
where: { id: productId },
|
||||||
@@ -440,6 +449,7 @@ export class ProductsService {
|
|||||||
businessId,
|
businessId,
|
||||||
dto.galleryMediaIds,
|
dto.galleryMediaIds,
|
||||||
);
|
);
|
||||||
|
nextGalleryIds = galleryMediaIds;
|
||||||
await this.syncGalleryAttachments(
|
await this.syncGalleryAttachments(
|
||||||
tx,
|
tx,
|
||||||
businessId,
|
businessId,
|
||||||
@@ -453,6 +463,21 @@ export class ProductsService {
|
|||||||
|
|
||||||
await this.sitemap.invalidateForBusiness(businessId);
|
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 {
|
return {
|
||||||
message: 'Product updated successfully',
|
message: 'Product updated successfully',
|
||||||
product: await this.serializeProduct(updated),
|
product: await this.serializeProduct(updated),
|
||||||
@@ -472,6 +497,13 @@ export class ProductsService {
|
|||||||
throw new NotFoundException('Product not found');
|
throw new NotFoundException('Product not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const mediaIds = await this.mediaService.allLinkedMediaIds(
|
||||||
|
businessId,
|
||||||
|
MediaEntityType.product,
|
||||||
|
productId,
|
||||||
|
existing.featuredMediaId,
|
||||||
|
);
|
||||||
|
|
||||||
await this.prisma.$transaction([
|
await this.prisma.$transaction([
|
||||||
this.prisma.mediaAttachment.deleteMany({
|
this.prisma.mediaAttachment.deleteMany({
|
||||||
where: {
|
where: {
|
||||||
@@ -491,6 +523,7 @@ export class ProductsService {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
await this.sitemap.invalidateForBusiness(businessId);
|
await this.sitemap.invalidateForBusiness(businessId);
|
||||||
|
await this.mediaService.deleteUnused(businessId, mediaIds);
|
||||||
|
|
||||||
return { message: 'Product deleted successfully' };
|
return { message: 'Product deleted successfully' };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -456,7 +456,13 @@ export class UserProductsService {
|
|||||||
|
|
||||||
const existing = await this.prisma.userProduct.findFirst({
|
const existing = await this.prisma.userProduct.findFirst({
|
||||||
where: { id: productId, businessId, userId: actor.id },
|
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) {
|
if (!existing) {
|
||||||
@@ -475,6 +481,7 @@ export class UserProductsService {
|
|||||||
title: string;
|
title: string;
|
||||||
slug: string;
|
slug: string;
|
||||||
metadata: unknown;
|
metadata: unknown;
|
||||||
|
featuredMediaId: bigint | null;
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
const titleFa = dto.titleFa.trim();
|
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 updated = await this.prisma.$transaction(async (tx) => {
|
||||||
const product = await tx.userProduct.update({
|
const product = await tx.userProduct.update({
|
||||||
where: { id: productId },
|
where: { id: productId },
|
||||||
@@ -594,6 +607,20 @@ export class UserProductsService {
|
|||||||
select: { id: true, name: true, nameFa: true },
|
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 {
|
return {
|
||||||
message: 'User product updated successfully',
|
message: 'User product updated successfully',
|
||||||
product: this.serializeAdminListItem(updated, category ?? undefined),
|
product: this.serializeAdminListItem(updated, category ?? undefined),
|
||||||
@@ -619,14 +646,28 @@ export class UserProductsService {
|
|||||||
|
|
||||||
const product = await this.prisma.userProduct.findFirst({
|
const product = await this.prisma.userProduct.findFirst({
|
||||||
where: { id: productId, businessId, userId: actor.id },
|
where: { id: productId, businessId, userId: actor.id },
|
||||||
select: { id: true },
|
select: { id: true, featuredMediaId: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!product) {
|
if (!product) {
|
||||||
throw new NotFoundException('User product not found');
|
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 this.prisma.$transaction(async (tx) => {
|
||||||
|
await tx.mediaAttachment.deleteMany({
|
||||||
|
where: {
|
||||||
|
businessId,
|
||||||
|
entityType: MediaEntityType.user_product,
|
||||||
|
entityId: productId,
|
||||||
|
},
|
||||||
|
});
|
||||||
await tx.categoryAssignment.deleteMany({
|
await tx.categoryAssignment.deleteMany({
|
||||||
where: {
|
where: {
|
||||||
businessId,
|
businessId,
|
||||||
@@ -637,6 +678,8 @@ export class UserProductsService {
|
|||||||
await tx.userProduct.delete({ where: { id: productId } });
|
await tx.userProduct.delete({ where: { id: productId } });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await this.mediaService.deleteUnused(businessId, mediaIds);
|
||||||
|
|
||||||
return { message: 'User product deleted successfully' };
|
return { message: 'User product deleted successfully' };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -784,7 +827,14 @@ export class UserProductsService {
|
|||||||
|
|
||||||
const existing = await this.prisma.userProduct.findFirst({
|
const existing = await this.prisma.userProduct.findFirst({
|
||||||
where: { id: productId, businessId },
|
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) {
|
if (!existing) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { AuthModule } from '../auth/auth.module';
|
import { AuthModule } from '../auth/auth.module';
|
||||||
|
import { MediaModule } from '../media/media.module';
|
||||||
import { TenantModule } from '../tenant/tenant.module';
|
import { TenantModule } from '../tenant/tenant.module';
|
||||||
import {
|
import {
|
||||||
PublicWorkshopsController,
|
PublicWorkshopsController,
|
||||||
@@ -8,7 +9,7 @@ import {
|
|||||||
import { WorkshopsService } from './workshops.service';
|
import { WorkshopsService } from './workshops.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [AuthModule, TenantModule],
|
imports: [AuthModule, MediaModule, TenantModule],
|
||||||
controllers: [WorkshopsController, PublicWorkshopsController],
|
controllers: [WorkshopsController, PublicWorkshopsController],
|
||||||
providers: [WorkshopsService],
|
providers: [WorkshopsService],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
} from '@prisma/client';
|
} from '@prisma/client';
|
||||||
import { AuthUser } from '../auth/auth.types';
|
import { AuthUser } from '../auth/auth.types';
|
||||||
import { PermissionsService } from '../auth/permissions.service';
|
import { PermissionsService } from '../auth/permissions.service';
|
||||||
|
import { MediaService } from '../media/media.service';
|
||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
import { TenantService } from '../tenant/tenant.service';
|
import { TenantService } from '../tenant/tenant.service';
|
||||||
import {
|
import {
|
||||||
@@ -61,6 +62,7 @@ export class WorkshopsService {
|
|||||||
private readonly prisma: PrismaService,
|
private readonly prisma: PrismaService,
|
||||||
private readonly permissions: PermissionsService,
|
private readonly permissions: PermissionsService,
|
||||||
private readonly tenant: TenantService,
|
private readonly tenant: TenantService,
|
||||||
|
private readonly mediaService: MediaService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async list(businessIdRaw: string, query: ListWorkshopsDto, actor: AuthUser) {
|
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 updated = await this.prisma.$transaction(async (tx) => {
|
||||||
const workshop = await tx.workshops.update({
|
const workshop = await tx.workshops.update({
|
||||||
where: { id: workshopId },
|
where: { id: workshopId },
|
||||||
@@ -341,6 +350,7 @@ export class WorkshopsService {
|
|||||||
businessId,
|
businessId,
|
||||||
dto.galleryMediaIds,
|
dto.galleryMediaIds,
|
||||||
);
|
);
|
||||||
|
nextGalleryIds = galleryMediaIds;
|
||||||
await this.syncGalleryAttachments(
|
await this.syncGalleryAttachments(
|
||||||
tx,
|
tx,
|
||||||
businessId,
|
businessId,
|
||||||
@@ -352,6 +362,21 @@ export class WorkshopsService {
|
|||||||
return workshop;
|
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 {
|
return {
|
||||||
message: 'Workshop updated successfully',
|
message: 'Workshop updated successfully',
|
||||||
workshop: await this.serializeWorkshop(updated),
|
workshop: await this.serializeWorkshop(updated),
|
||||||
@@ -371,6 +396,13 @@ export class WorkshopsService {
|
|||||||
throw new NotFoundException('Workshop not found');
|
throw new NotFoundException('Workshop not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const mediaIds = await this.mediaService.allLinkedMediaIds(
|
||||||
|
businessId,
|
||||||
|
MediaEntityType.workshop,
|
||||||
|
workshopId,
|
||||||
|
existing.featured_media_id,
|
||||||
|
);
|
||||||
|
|
||||||
await this.prisma.$transaction([
|
await this.prisma.$transaction([
|
||||||
this.prisma.workshop_registrations.deleteMany({
|
this.prisma.workshop_registrations.deleteMany({
|
||||||
where: { workshop_id: workshopId, business_id: businessId },
|
where: { workshop_id: workshopId, business_id: businessId },
|
||||||
@@ -392,6 +424,8 @@ export class WorkshopsService {
|
|||||||
this.prisma.workshops.delete({ where: { id: workshopId } }),
|
this.prisma.workshops.delete({ where: { id: workshopId } }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
await this.mediaService.deleteUnused(businessId, mediaIds);
|
||||||
|
|
||||||
return { message: 'Workshop deleted successfully' };
|
return { message: 'Workshop deleted successfully' };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user