Initialize the API and add managed app banners.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-09 17:08:47 +03:30
co-authored by Cursor
commit b15fefe438
158 changed files with 25624 additions and 0 deletions
@@ -0,0 +1,44 @@
import { Body, Controller, Get, Param, Patch, Post, Query } from '@nestjs/common';
import { IsString, MinLength } from 'class-validator';
import { StockNotifyService } from './stock-notify.service';
import { AuthType } from '../common/decorators/auth.decorators';
import { CurrentUser } from '../common/decorators/current-user.decorator';
import type { AuthPrincipal } from '../common/decorators/current-user.decorator';
class SubscribeDto {
@IsString()
@MinLength(1)
productId!: string;
}
@Controller()
export class StockNotifyController {
constructor(private readonly stockNotifyService: StockNotifyService) {}
@AuthType('admin')
@Get('admin/stock-alerts')
list(@Query('activeOnly') activeOnly?: string) {
return this.stockNotifyService.listAdmin(activeOnly !== 'false');
}
@AuthType('admin')
@Patch('admin/stock-alerts/:id/notify')
notify(@Param('id') id: string) {
return this.stockNotifyService.markNotified(id);
}
@AuthType('customer')
@Post('stock-alerts')
subscribe(@CurrentUser() user: AuthPrincipal, @Body() dto: SubscribeDto) {
return this.stockNotifyService.subscribe(user.id, dto.productId);
}
@AuthType('customer')
@Post('stock-alerts/:productId/cancel')
unsubscribe(
@CurrentUser() user: AuthPrincipal,
@Param('productId') productId: string,
) {
return this.stockNotifyService.unsubscribe(user.id, productId);
}
}
+10
View File
@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { StockNotifyController } from './stock-notify.controller';
import { StockNotifyService } from './stock-notify.service';
@Module({
controllers: [StockNotifyController],
providers: [StockNotifyService],
exports: [StockNotifyService],
})
export class StockNotifyModule {}
+96
View File
@@ -0,0 +1,96 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
@Injectable()
export class StockNotifyService {
constructor(private readonly prisma: PrismaService) {}
async listAdmin(activeOnly = true) {
const rows = await this.prisma.stockNotifyRequest.findMany({
where: activeOnly ? { active: true } : undefined,
orderBy: { createdAt: 'desc' },
include: {
customer: { select: { firstName: true, lastName: true, phone: true } },
product: { select: { name: true, code: true, color: true, stock: true } },
},
});
return rows.map((r) => this.map(r));
}
async subscribe(customerId: string, productId: string) {
const product = await this.prisma.product.findUnique({ where: { id: productId } });
if (!product) throw new NotFoundException('محصول یافت نشد');
const row = await this.prisma.stockNotifyRequest.upsert({
where: { customerId_productId: { customerId, productId } },
create: { customerId, productId, active: true },
update: { active: true, notifiedAt: null },
include: {
customer: { select: { firstName: true, lastName: true, phone: true } },
product: { select: { name: true, code: true, color: true, stock: true } },
},
});
return this.map(row);
}
async unsubscribe(customerId: string, productId: string) {
await this.prisma.stockNotifyRequest.updateMany({
where: { customerId, productId },
data: { active: false },
});
return { productId, active: false };
}
async markNotified(id: string) {
const existing = await this.prisma.stockNotifyRequest.findUnique({
where: { id },
});
if (!existing) throw new NotFoundException('درخواست یافت نشد');
const row = await this.prisma.stockNotifyRequest.update({
where: { id },
data: { notifiedAt: new Date(), active: false },
include: {
customer: { select: { firstName: true, lastName: true, phone: true } },
product: { select: { name: true, code: true, color: true, stock: true } },
},
});
await this.prisma.customerNotification.create({
data: {
customerId: row.customerId,
event: 'stock_available',
title: 'موجودی محصول',
body: `محصول ${row.product.name} دوباره موجود شد.`,
tone: 'success',
link: `/product?id=${row.productId}`,
},
});
return this.map(row);
}
private map(r: {
id: string;
customerId: string;
productId: string;
active: boolean;
createdAt: Date;
notifiedAt: Date | null;
customer: { firstName: string; lastName: string; phone: string };
product: { name: string; code: string; color: string; stock: number | null };
}) {
return {
id: r.id,
customerId: r.customerId,
customerName: `${r.customer.firstName} ${r.customer.lastName}`.trim(),
customerPhone: r.customer.phone,
productId: r.productId,
productName: r.product.name,
productCode: r.product.code,
color: r.product.color,
stock: r.product.stock,
active: r.active,
createdAt: r.createdAt.toISOString(),
notifiedAt: r.notifiedAt?.toISOString(),
};
}
}