feat(plugins): catalog images and purchase receipt uploads

Catalog items can store up to three photos; remote tenants attach a receipt on pending requests.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-08 14:56:09 +03:30
co-authored by Cursor
parent 18ac137e25
commit 67a03d9593
7 changed files with 189 additions and 8 deletions
+49
View File
@@ -30,6 +30,7 @@ import {
formatIranDate,
iranDaysUntil,
} from '../common/iran-time.util';
import { rewriteLegacyUploadUrl } from '../uploads/uploads.util';
import {
daysToPlanMonths,
inferSettingKeyFromName,
@@ -252,6 +253,9 @@ export class PluginStoreService implements OnModuleInit, OnModuleDestroy {
...(dto.description !== undefined && {
description: dto.description?.trim() || null,
}),
...(dto.images !== undefined && {
images: this.normalizeImageList(dto.images) as Prisma.InputJsonValue,
}),
},
});
for (const price of dto.prices) {
@@ -528,6 +532,7 @@ export class PluginStoreService implements OnModuleInit, OnModuleDestroy {
settingKey: item.settingKey,
titleFa: item.titleFa,
description: item.description,
images: this.normalizeImageList(item.images),
prices: enabledPrices.map((p) => ({
durationDays: p.durationDays,
originalPriceRials: p.originalPriceRials,
@@ -655,6 +660,38 @@ export class PluginStoreService implements OnModuleInit, OnModuleDestroy {
return { ok: true };
}
async remoteAttachReceipt(
tenantId: string,
product: string,
requestId: string,
receiptUrl: string,
) {
await this.assertRemoteKhanegi(tenantId, product);
const url = rewriteLegacyUploadUrl(receiptUrl);
if (!url) throw new BadRequestException('عکس فیش لازم است');
const req = await this.prisma.pluginPurchaseRequest.findFirst({
where: { id: requestId, tenantId },
include: {
catalogItem: { select: { titleFa: true, settingKey: true } },
tenant: {
select: { id: true, nameFa: true, domain: true, slug: true, product: true },
},
},
});
if (!req) throw new NotFoundException('درخواست یافت نشد');
if (req.status !== 'pending') {
throw new BadRequestException('فقط برای درخواست در انتظار می‌توان فیش فرستاد');
}
const updated = await this.prisma.pluginPurchaseRequest.update({
where: { id: requestId },
data: { receiptUrl: url },
});
this.notify.notifyAdmins(
`فیش واریز افزونه: «${req.catalogItem.titleFa}» برای «${req.tenant.nameFa}» (${req.tenant.domain})`,
);
return this.toRequestRow({ ...req, receiptUrl: updated.receiptUrl });
}
async runKhanegiExpiry() {
try {
const plugins = await this.prisma.pluginSubscription.findMany({
@@ -759,6 +796,7 @@ export class PluginStoreService implements OnModuleInit, OnModuleDestroy {
settingKey: item.settingKey,
titleFa: item.titleFa,
description: item.description,
images: this.normalizeImageList(item.images),
inStore: item.inStore,
prices: PLUGIN_STORE_DURATIONS.map((days) => {
const row = priceMap.get(days);
@@ -795,11 +833,22 @@ export class PluginStoreService implements OnModuleInit, OnModuleDestroy {
finalPriceRials: row.finalPriceRials,
status: row.status,
autoCutOnExpiry: row.autoCutOnExpiry,
receiptUrl: rewriteLegacyUploadUrl(row.receiptUrl) || null,
createdAt: row.createdAt?.toISOString?.() ?? row.createdAt,
decidedAt: row.decidedAt?.toISOString?.() ?? row.decidedAt ?? null,
};
}
private normalizeImageList(raw: unknown): string[] {
if (!Array.isArray(raw)) return [];
const out: string[] = [];
for (const item of raw) {
const url = rewriteLegacyUploadUrl(item);
if (url && !out.includes(url) && out.length < 3) out.push(url);
}
return out;
}
private readMarks(raw: unknown, expiresAt: Date): Record<string, any> {
const iso = expiresAt.toISOString();
if (!raw || typeof raw !== 'object') return { _exp: iso };