Prefer text fields in technical-form AI and add invoice payment methods.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
f8b865b0aa
commit
503b70a690
@@ -0,0 +1,51 @@
|
|||||||
|
-- Meshkee CMS — invoice / template payment methods (نحوه ی پرداخت)
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- template payment methods (duplicatable free-text)
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
CREATE TABLE invoice_template_payment_methods (
|
||||||
|
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||||
|
template_id BIGINT NOT NULL,
|
||||||
|
text TEXT NOT NULL,
|
||||||
|
sort_order INT NOT NULL DEFAULT 0,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
|
||||||
|
CONSTRAINT invoice_template_payment_methods_template_id_fkey
|
||||||
|
FOREIGN KEY (template_id) REFERENCES invoice_templates (id) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT invoice_template_payment_methods_text_nonempty
|
||||||
|
CHECK (char_length(trim(text)) > 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_invoice_template_payment_methods_template_id
|
||||||
|
ON invoice_template_payment_methods (template_id, sort_order);
|
||||||
|
|
||||||
|
CREATE TRIGGER invoice_template_payment_methods_set_updated_at
|
||||||
|
BEFORE UPDATE ON invoice_template_payment_methods
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION set_updated_at();
|
||||||
|
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
-- invoice payment methods (duplicatable free-text)
|
||||||
|
-- ---------------------------------------------------------------------------
|
||||||
|
CREATE TABLE invoice_payment_methods (
|
||||||
|
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||||
|
invoice_id BIGINT NOT NULL,
|
||||||
|
text TEXT NOT NULL,
|
||||||
|
sort_order INT NOT NULL DEFAULT 0,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
|
||||||
|
CONSTRAINT invoice_payment_methods_invoice_id_fkey
|
||||||
|
FOREIGN KEY (invoice_id) REFERENCES invoices (id) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT invoice_payment_methods_text_nonempty
|
||||||
|
CHECK (char_length(trim(text)) > 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_invoice_payment_methods_invoice_id
|
||||||
|
ON invoice_payment_methods (invoice_id, sort_order);
|
||||||
|
|
||||||
|
CREATE TRIGGER invoice_payment_methods_set_updated_at
|
||||||
|
BEFORE UPDATE ON invoice_payment_methods
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION set_updated_at();
|
||||||
@@ -1383,6 +1383,7 @@ model InvoiceTemplate {
|
|||||||
accounts InvoiceTemplateAccount[]
|
accounts InvoiceTemplateAccount[]
|
||||||
items InvoiceTemplateItem[]
|
items InvoiceTemplateItem[]
|
||||||
keyPoints InvoiceTemplateKeyPoint[]
|
keyPoints InvoiceTemplateKeyPoint[]
|
||||||
|
paymentMethods InvoiceTemplatePaymentMethod[]
|
||||||
business Business? @relation(fields: [businessId], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
business Business? @relation(fields: [businessId], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||||
invoices Invoice[]
|
invoices Invoice[]
|
||||||
|
|
||||||
@@ -1423,6 +1424,19 @@ model InvoiceTemplateKeyPoint {
|
|||||||
@@map("invoice_template_key_points")
|
@@map("invoice_template_key_points")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model InvoiceTemplatePaymentMethod {
|
||||||
|
id BigInt @id @default(autoincrement())
|
||||||
|
templateId BigInt @map("template_id")
|
||||||
|
text String
|
||||||
|
sortOrder Int @default(0) @map("sort_order")
|
||||||
|
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||||
|
updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(6)
|
||||||
|
template InvoiceTemplate @relation(fields: [templateId], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||||
|
|
||||||
|
@@index([templateId, sortOrder], map: "idx_invoice_template_payment_methods_template_id")
|
||||||
|
@@map("invoice_template_payment_methods")
|
||||||
|
}
|
||||||
|
|
||||||
model InvoiceTemplateAccount {
|
model InvoiceTemplateAccount {
|
||||||
id BigInt @id @default(autoincrement())
|
id BigInt @id @default(autoincrement())
|
||||||
templateId BigInt @map("template_id")
|
templateId BigInt @map("template_id")
|
||||||
@@ -1458,6 +1472,7 @@ model Invoice {
|
|||||||
accounts InvoiceAccount[]
|
accounts InvoiceAccount[]
|
||||||
items InvoiceItem[]
|
items InvoiceItem[]
|
||||||
keyPoints InvoiceKeyPoint[]
|
keyPoints InvoiceKeyPoint[]
|
||||||
|
paymentMethods InvoicePaymentMethod[]
|
||||||
business Business @relation("InvoiceBusiness", fields: [businessId], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
business Business @relation("InvoiceBusiness", fields: [businessId], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||||
invoiceTemplate InvoiceTemplate? @relation(fields: [invoiceTemplateId], references: [id], onUpdate: NoAction)
|
invoiceTemplate InvoiceTemplate? @relation(fields: [invoiceTemplateId], references: [id], onUpdate: NoAction)
|
||||||
issuer User? @relation("InvoiceIssuer", fields: [issuedBy], references: [id], onUpdate: NoAction)
|
issuer User? @relation("InvoiceIssuer", fields: [issuedBy], references: [id], onUpdate: NoAction)
|
||||||
@@ -1504,6 +1519,19 @@ model InvoiceKeyPoint {
|
|||||||
@@map("invoice_key_points")
|
@@map("invoice_key_points")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model InvoicePaymentMethod {
|
||||||
|
id BigInt @id @default(autoincrement())
|
||||||
|
invoiceId BigInt @map("invoice_id")
|
||||||
|
text String
|
||||||
|
sortOrder Int @default(0) @map("sort_order")
|
||||||
|
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||||
|
updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(6)
|
||||||
|
invoice Invoice @relation(fields: [invoiceId], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||||
|
|
||||||
|
@@index([invoiceId, sortOrder], map: "idx_invoice_payment_methods_invoice_id")
|
||||||
|
@@map("invoice_payment_methods")
|
||||||
|
}
|
||||||
|
|
||||||
model InvoiceAccount {
|
model InvoiceAccount {
|
||||||
id BigInt @id @default(autoincrement())
|
id BigInt @id @default(autoincrement())
|
||||||
invoiceId BigInt @map("invoice_id")
|
invoiceId BigInt @map("invoice_id")
|
||||||
|
|||||||
@@ -1379,7 +1379,7 @@ export class LegacyMigrateService {
|
|||||||
.filter((m) => Number(m.size) > 0)
|
.filter((m) => Number(m.size) > 0)
|
||||||
.slice(0, 12)
|
.slice(0, 12)
|
||||||
.map((m) => ({ id: m.id, fileName: m.file_name }));
|
.map((m) => ({ id: m.id, fileName: m.file_name }));
|
||||||
const storagePrefix = await this.legacySourceS3.resolveBusinessPrefix(
|
const storagePrefix = await this.tryResolveStoragePrefix(
|
||||||
oldBizId,
|
oldBizId,
|
||||||
oldSlug,
|
oldSlug,
|
||||||
probeMedia,
|
probeMedia,
|
||||||
@@ -1417,7 +1417,7 @@ export class LegacyMigrateService {
|
|||||||
|
|
||||||
for (const media of articleMedia) {
|
for (const media of articleMedia) {
|
||||||
const size = Number(media.size) || 0;
|
const size = Number(media.size) || 0;
|
||||||
if (size <= 0) {
|
if (size <= 0 || !storagePrefix) {
|
||||||
imagesFailed += 1;
|
imagesFailed += 1;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -1611,7 +1611,7 @@ export class LegacyMigrateService {
|
|||||||
.filter((m) => Number(m.size) > 0)
|
.filter((m) => Number(m.size) > 0)
|
||||||
.slice(0, 12)
|
.slice(0, 12)
|
||||||
.map((m) => ({ id: m.id, fileName: m.file_name }));
|
.map((m) => ({ id: m.id, fileName: m.file_name }));
|
||||||
const storagePrefix = await this.legacySourceS3.resolveBusinessPrefix(
|
const storagePrefix = await this.tryResolveStoragePrefix(
|
||||||
oldBizId,
|
oldBizId,
|
||||||
oldSlug,
|
oldSlug,
|
||||||
probeMedia,
|
probeMedia,
|
||||||
@@ -1648,7 +1648,7 @@ export class LegacyMigrateService {
|
|||||||
|
|
||||||
for (const media of itemMedia) {
|
for (const media of itemMedia) {
|
||||||
const size = Number(media.size) || 0;
|
const size = Number(media.size) || 0;
|
||||||
if (size <= 0) {
|
if (size <= 0 || !storagePrefix) {
|
||||||
imagesFailed += 1;
|
imagesFailed += 1;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -1978,6 +1978,27 @@ export class LegacyMigrateService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async tryResolveStoragePrefix(
|
||||||
|
oldBizId: number,
|
||||||
|
oldSlug: string | null,
|
||||||
|
probeMedia: Array<{ id: number; fileName: string }>,
|
||||||
|
): Promise<string | null> {
|
||||||
|
if (!probeMedia.length) return null;
|
||||||
|
try {
|
||||||
|
return await this.legacySourceS3.resolveBusinessPrefix(
|
||||||
|
oldBizId,
|
||||||
|
oldSlug,
|
||||||
|
probeMedia,
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
|
console.warn(
|
||||||
|
`Legacy migrate: skipping image copy for business ${oldBizId}: ${message}`,
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async copyLegacyMedia(input: {
|
private async copyLegacyMedia(input: {
|
||||||
businessId: bigint;
|
businessId: bigint;
|
||||||
oldMedia: OldMediaRow;
|
oldMedia: OldMediaRow;
|
||||||
|
|||||||
@@ -126,10 +126,12 @@ export class CategoryTechnicalFormAiService {
|
|||||||
|
|
||||||
قوانین:
|
قوانین:
|
||||||
- بین ۵ تا ۱۱ فیلد کاربردی تولید کن که خریداران برای محصولات این دسته نیاز دارند (سیستم همیشه «سایر ویژگیها» را اضافه میکند).
|
- بین ۵ تا ۱۱ فیلد کاربردی تولید کن که خریداران برای محصولات این دسته نیاز دارند (سیستم همیشه «سایر ویژگیها» را اضافه میکند).
|
||||||
- از ترکیب text، textarea، select، multi_select و multi_value استفاده کن.
|
- ترجیحاً از type=text استفاده کن تا فروشنده بتواند هر مقدار دلخواهی را آزادانه وارد کند.
|
||||||
- فیلدهای select و multi_select باید ۳ تا ۸ گزینه واقعی داشته باشند.
|
- از select و multi_select حتیالمقدور استفاده نکن. فقط وقتی مجموعه گزینهها واقعاً ثابت، کوتاه و استاندارد است (مثلاً بله/خیر یا حداکثر چند گزینه قطعی) مجازند؛ برای ظرفیت، توان، ابعاد، مدل، جنس، رنگ آزاد و مشابه آنها همیشه text بگذار.
|
||||||
|
- اگر بین text و select مردد هستی، حتماً text انتخاب کن.
|
||||||
|
- فیلدهای select و multi_select (در صورت استفاده) باید ۳ تا ۸ گزینه واقعی داشته باشند.
|
||||||
- multi_value یک لیست تکراری از مقادیر متنی آزاد است (بدون گزینه) برای مقادیر کوتاه چندتایی.
|
- multi_value یک لیست تکراری از مقادیر متنی آزاد است (بدون گزینه) برای مقادیر کوتاه چندتایی.
|
||||||
- text برای مقادیر کوتاه (ابعاد، وزن، سال مدل).
|
- text برای مقادیر کوتاه (ابعاد، وزن، سال مدل، ظرفیت، توان).
|
||||||
- textarea برای مشخصات طولانیتر (ویژگیها، دستورالعمل نگهداری).
|
- textarea برای مشخصات طولانیتر (ویژگیها، دستورالعمل نگهداری).
|
||||||
- اگر برای این دسته مناسب است، فیلدهای «ابعاد» و «وزن» را بهصورت text اضافه کن (واحد متریک رایج ایران را در برچسب بیاور، مثلاً سانتیمتر / کیلوگرم). اگر برای دسته بیمعناست، اضافه نکن.
|
- اگر برای این دسته مناسب است، فیلدهای «ابعاد» و «وزن» را بهصورت text اضافه کن (واحد متریک رایج ایران را در برچسب بیاور، مثلاً سانتیمتر / کیلوگرم). اگر برای دسته بیمعناست، اضافه نکن.
|
||||||
- مشخصات مهم خریدار را الزامی علامت بزن.
|
- مشخصات مهم خریدار را الزامی علامت بزن.
|
||||||
|
|||||||
@@ -57,6 +57,12 @@ export class InvoiceKeyPointInputDto {
|
|||||||
text!: string;
|
text!: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class InvoicePaymentMethodInputDto {
|
||||||
|
@IsString()
|
||||||
|
@MinLength(1)
|
||||||
|
text!: string;
|
||||||
|
}
|
||||||
|
|
||||||
export class InvoiceAccountInputDto {
|
export class InvoiceAccountInputDto {
|
||||||
@IsString()
|
@IsString()
|
||||||
@MinLength(1)
|
@MinLength(1)
|
||||||
@@ -114,6 +120,12 @@ export class CreateInvoiceDto {
|
|||||||
@Type(() => InvoiceKeyPointInputDto)
|
@Type(() => InvoiceKeyPointInputDto)
|
||||||
keyPoints?: InvoiceKeyPointInputDto[];
|
keyPoints?: InvoiceKeyPointInputDto[];
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray()
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Type(() => InvoicePaymentMethodInputDto)
|
||||||
|
paymentMethods?: InvoicePaymentMethodInputDto[];
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsArray()
|
@IsArray()
|
||||||
@ValidateNested({ each: true })
|
@ValidateNested({ each: true })
|
||||||
@@ -134,7 +146,7 @@ export class UpdateInvoiceStatusDto {
|
|||||||
notes?: string;
|
notes?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Full content replace for an existing invoice (items / key points / accounts). */
|
/** Full content replace for an existing invoice (items / key points / payment methods / accounts). */
|
||||||
export class UpdateInvoiceDto {
|
export class UpdateInvoiceDto {
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
@@ -169,6 +181,12 @@ export class UpdateInvoiceDto {
|
|||||||
@Type(() => InvoiceKeyPointInputDto)
|
@Type(() => InvoiceKeyPointInputDto)
|
||||||
keyPoints?: InvoiceKeyPointInputDto[];
|
keyPoints?: InvoiceKeyPointInputDto[];
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray()
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Type(() => InvoicePaymentMethodInputDto)
|
||||||
|
paymentMethods?: InvoicePaymentMethodInputDto[];
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsArray()
|
@IsArray()
|
||||||
@ValidateNested({ each: true })
|
@ValidateNested({ each: true })
|
||||||
@@ -337,6 +355,12 @@ export class CreateInvoiceTemplateDto {
|
|||||||
@Type(() => InvoiceKeyPointInputDto)
|
@Type(() => InvoiceKeyPointInputDto)
|
||||||
keyPoints?: InvoiceKeyPointInputDto[];
|
keyPoints?: InvoiceKeyPointInputDto[];
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray()
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Type(() => InvoicePaymentMethodInputDto)
|
||||||
|
paymentMethods?: InvoicePaymentMethodInputDto[];
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsArray()
|
@IsArray()
|
||||||
@ValidateNested({ each: true })
|
@ValidateNested({ each: true })
|
||||||
@@ -373,6 +397,12 @@ export class UpdateInvoiceTemplateDto {
|
|||||||
@Type(() => InvoiceKeyPointInputDto)
|
@Type(() => InvoiceKeyPointInputDto)
|
||||||
keyPoints?: InvoiceKeyPointInputDto[];
|
keyPoints?: InvoiceKeyPointInputDto[];
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray()
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Type(() => InvoicePaymentMethodInputDto)
|
||||||
|
paymentMethods?: InvoicePaymentMethodInputDto[];
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsArray()
|
@IsArray()
|
||||||
@ValidateNested({ each: true })
|
@ValidateNested({ each: true })
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
InvoiceAccountInputDto,
|
InvoiceAccountInputDto,
|
||||||
InvoiceItemInputDto,
|
InvoiceItemInputDto,
|
||||||
InvoiceKeyPointInputDto,
|
InvoiceKeyPointInputDto,
|
||||||
|
InvoicePaymentMethodInputDto,
|
||||||
InvoiceTemplateItemInputDto,
|
InvoiceTemplateItemInputDto,
|
||||||
ListInvoicesDto,
|
ListInvoicesDto,
|
||||||
UpdateInvoiceDto,
|
UpdateInvoiceDto,
|
||||||
@@ -250,12 +251,14 @@ export class InvoicesService {
|
|||||||
user: { select: { id: true, firstName: true, lastName: true, cellNumber: true } },
|
user: { select: { id: true, firstName: true, lastName: true, cellNumber: true } },
|
||||||
items: { orderBy: [{ sortOrder: 'asc' as const }, { id: 'asc' as const }] },
|
items: { orderBy: [{ sortOrder: 'asc' as const }, { id: 'asc' as const }] },
|
||||||
keyPoints: { orderBy: [{ sortOrder: 'asc' as const }, { id: 'asc' as const }] },
|
keyPoints: { orderBy: [{ sortOrder: 'asc' as const }, { id: 'asc' as const }] },
|
||||||
|
paymentMethods: { orderBy: [{ sortOrder: 'asc' as const }, { id: 'asc' as const }] },
|
||||||
accounts: { orderBy: [{ sortOrder: 'asc' as const }, { id: 'asc' as const }] },
|
accounts: { orderBy: [{ sortOrder: 'asc' as const }, { id: 'asc' as const }] },
|
||||||
};
|
};
|
||||||
|
|
||||||
private readonly invoiceTemplateInclude = {
|
private readonly invoiceTemplateInclude = {
|
||||||
items: { orderBy: [{ sortOrder: 'asc' as const }, { id: 'asc' as const }] },
|
items: { orderBy: [{ sortOrder: 'asc' as const }, { id: 'asc' as const }] },
|
||||||
keyPoints: { orderBy: [{ sortOrder: 'asc' as const }, { id: 'asc' as const }] },
|
keyPoints: { orderBy: [{ sortOrder: 'asc' as const }, { id: 'asc' as const }] },
|
||||||
|
paymentMethods: { orderBy: [{ sortOrder: 'asc' as const }, { id: 'asc' as const }] },
|
||||||
accounts: { orderBy: [{ sortOrder: 'asc' as const }, { id: 'asc' as const }] },
|
accounts: { orderBy: [{ sortOrder: 'asc' as const }, { id: 'asc' as const }] },
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -271,6 +274,18 @@ export class InvoicesService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private serializePaymentMethod(row: {
|
||||||
|
id: bigint;
|
||||||
|
text: string;
|
||||||
|
sortOrder: number;
|
||||||
|
}) {
|
||||||
|
return {
|
||||||
|
id: row.id.toString(),
|
||||||
|
text: row.text,
|
||||||
|
sortOrder: row.sortOrder,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private serializeAccount(row: {
|
private serializeAccount(row: {
|
||||||
id: bigint;
|
id: bigint;
|
||||||
bankName: string;
|
bankName: string;
|
||||||
@@ -343,6 +358,11 @@ export class InvoicesService {
|
|||||||
text: string;
|
text: string;
|
||||||
sortOrder: number;
|
sortOrder: number;
|
||||||
}>;
|
}>;
|
||||||
|
paymentMethods?: Array<{
|
||||||
|
id: bigint;
|
||||||
|
text: string;
|
||||||
|
sortOrder: number;
|
||||||
|
}>;
|
||||||
accounts?: Array<{
|
accounts?: Array<{
|
||||||
id: bigint;
|
id: bigint;
|
||||||
bankName: string;
|
bankName: string;
|
||||||
@@ -372,6 +392,10 @@ export class InvoicesService {
|
|||||||
includeNested && row.keyPoints
|
includeNested && row.keyPoints
|
||||||
? row.keyPoints.map((kp) => this.serializeKeyPoint(kp))
|
? row.keyPoints.map((kp) => this.serializeKeyPoint(kp))
|
||||||
: undefined,
|
: undefined,
|
||||||
|
paymentMethods:
|
||||||
|
includeNested && row.paymentMethods
|
||||||
|
? row.paymentMethods.map((pm) => this.serializePaymentMethod(pm))
|
||||||
|
: undefined,
|
||||||
accounts:
|
accounts:
|
||||||
includeNested && row.accounts
|
includeNested && row.accounts
|
||||||
? row.accounts.map((acc) => this.serializeAccount(acc))
|
? row.accounts.map((acc) => this.serializeAccount(acc))
|
||||||
@@ -488,6 +512,11 @@ export class InvoicesService {
|
|||||||
text: string;
|
text: string;
|
||||||
sortOrder: number;
|
sortOrder: number;
|
||||||
}>;
|
}>;
|
||||||
|
paymentMethods?: Array<{
|
||||||
|
id: bigint;
|
||||||
|
text: string;
|
||||||
|
sortOrder: number;
|
||||||
|
}>;
|
||||||
accounts?: Array<{
|
accounts?: Array<{
|
||||||
id: bigint;
|
id: bigint;
|
||||||
bankName: string;
|
bankName: string;
|
||||||
@@ -564,6 +593,10 @@ export class InvoicesService {
|
|||||||
includeNested && row.keyPoints
|
includeNested && row.keyPoints
|
||||||
? row.keyPoints.map((kp) => this.serializeKeyPoint(kp))
|
? row.keyPoints.map((kp) => this.serializeKeyPoint(kp))
|
||||||
: undefined,
|
: undefined,
|
||||||
|
paymentMethods:
|
||||||
|
includeNested && row.paymentMethods
|
||||||
|
? row.paymentMethods.map((pm) => this.serializePaymentMethod(pm))
|
||||||
|
: undefined,
|
||||||
accounts:
|
accounts:
|
||||||
includeNested && row.accounts
|
includeNested && row.accounts
|
||||||
? row.accounts.map((acc) => this.serializeAccount(acc))
|
? row.accounts.map((acc) => this.serializeAccount(acc))
|
||||||
@@ -581,6 +614,14 @@ export class InvoicesService {
|
|||||||
return { text };
|
return { text };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private normalizePaymentMethodInput(paymentMethod: InvoicePaymentMethodInputDto) {
|
||||||
|
const text = paymentMethod.text.trim();
|
||||||
|
if (!text) {
|
||||||
|
throw new BadRequestException('Each payment method requires text');
|
||||||
|
}
|
||||||
|
return { text };
|
||||||
|
}
|
||||||
|
|
||||||
private normalizeAccountInput(account: InvoiceAccountInputDto) {
|
private normalizeAccountInput(account: InvoiceAccountInputDto) {
|
||||||
const bankName = account.bankName.trim();
|
const bankName = account.bankName.trim();
|
||||||
if (!bankName) {
|
if (!bankName) {
|
||||||
@@ -793,6 +834,9 @@ export class InvoicesService {
|
|||||||
|
|
||||||
const items = dto.items.map((item) => this.normalizeTemplateItemInput(item));
|
const items = dto.items.map((item) => this.normalizeTemplateItemInput(item));
|
||||||
const keyPoints = (dto.keyPoints ?? []).map((kp) => this.normalizeKeyPointInput(kp));
|
const keyPoints = (dto.keyPoints ?? []).map((kp) => this.normalizeKeyPointInput(kp));
|
||||||
|
const paymentMethods = (dto.paymentMethods ?? []).map((pm) =>
|
||||||
|
this.normalizePaymentMethodInput(pm),
|
||||||
|
);
|
||||||
const accounts = (dto.accounts ?? []).map((acc) => this.normalizeAccountInput(acc));
|
const accounts = (dto.accounts ?? []).map((acc) => this.normalizeAccountInput(acc));
|
||||||
|
|
||||||
const row = await this.prisma.invoiceTemplate.create({
|
const row = await this.prisma.invoiceTemplate.create({
|
||||||
@@ -819,6 +863,12 @@ export class InvoicesService {
|
|||||||
sortOrder: index,
|
sortOrder: index,
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
|
paymentMethods: {
|
||||||
|
create: paymentMethods.map((pm, index) => ({
|
||||||
|
text: pm.text,
|
||||||
|
sortOrder: index,
|
||||||
|
})),
|
||||||
|
},
|
||||||
accounts: {
|
accounts: {
|
||||||
create: accounts.map((acc, index) => ({
|
create: accounts.map((acc, index) => ({
|
||||||
bankName: acc.bankName,
|
bankName: acc.bankName,
|
||||||
@@ -856,6 +906,10 @@ export class InvoicesService {
|
|||||||
dto.keyPoints !== undefined
|
dto.keyPoints !== undefined
|
||||||
? dto.keyPoints.map((kp) => this.normalizeKeyPointInput(kp))
|
? dto.keyPoints.map((kp) => this.normalizeKeyPointInput(kp))
|
||||||
: null;
|
: null;
|
||||||
|
const replacePaymentMethods =
|
||||||
|
dto.paymentMethods !== undefined
|
||||||
|
? dto.paymentMethods.map((pm) => this.normalizePaymentMethodInput(pm))
|
||||||
|
: null;
|
||||||
const replaceAccounts =
|
const replaceAccounts =
|
||||||
dto.accounts !== undefined
|
dto.accounts !== undefined
|
||||||
? dto.accounts.map((acc) => this.normalizeAccountInput(acc))
|
? dto.accounts.map((acc) => this.normalizeAccountInput(acc))
|
||||||
@@ -868,6 +922,9 @@ export class InvoicesService {
|
|||||||
if (replaceKeyPoints !== null) {
|
if (replaceKeyPoints !== null) {
|
||||||
await tx.invoiceTemplateKeyPoint.deleteMany({ where: { templateId } });
|
await tx.invoiceTemplateKeyPoint.deleteMany({ where: { templateId } });
|
||||||
}
|
}
|
||||||
|
if (replacePaymentMethods !== null) {
|
||||||
|
await tx.invoiceTemplatePaymentMethod.deleteMany({ where: { templateId } });
|
||||||
|
}
|
||||||
if (replaceAccounts !== null) {
|
if (replaceAccounts !== null) {
|
||||||
await tx.invoiceTemplateAccount.deleteMany({ where: { templateId } });
|
await tx.invoiceTemplateAccount.deleteMany({ where: { templateId } });
|
||||||
}
|
}
|
||||||
@@ -905,6 +962,16 @@ export class InvoicesService {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
|
...(replacePaymentMethods !== null
|
||||||
|
? {
|
||||||
|
paymentMethods: {
|
||||||
|
create: replacePaymentMethods.map((pm, index) => ({
|
||||||
|
text: pm.text,
|
||||||
|
sortOrder: index,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
...(replaceAccounts !== null
|
...(replaceAccounts !== null
|
||||||
? {
|
? {
|
||||||
accounts: {
|
accounts: {
|
||||||
@@ -1116,6 +1183,9 @@ export class InvoicesService {
|
|||||||
|
|
||||||
const items = dto.items.map((item) => this.normalizeTemplateItemInput(item));
|
const items = dto.items.map((item) => this.normalizeTemplateItemInput(item));
|
||||||
const keyPoints = (dto.keyPoints ?? []).map((kp) => this.normalizeKeyPointInput(kp));
|
const keyPoints = (dto.keyPoints ?? []).map((kp) => this.normalizeKeyPointInput(kp));
|
||||||
|
const paymentMethods = (dto.paymentMethods ?? []).map((pm) =>
|
||||||
|
this.normalizePaymentMethodInput(pm),
|
||||||
|
);
|
||||||
const accounts = (dto.accounts ?? []).map((acc) => this.normalizeAccountInput(acc));
|
const accounts = (dto.accounts ?? []).map((acc) => this.normalizeAccountInput(acc));
|
||||||
|
|
||||||
const row = await this.prisma.invoiceTemplate.create({
|
const row = await this.prisma.invoiceTemplate.create({
|
||||||
@@ -1143,6 +1213,12 @@ export class InvoicesService {
|
|||||||
sortOrder: index,
|
sortOrder: index,
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
|
paymentMethods: {
|
||||||
|
create: paymentMethods.map((pm, index) => ({
|
||||||
|
text: pm.text,
|
||||||
|
sortOrder: index,
|
||||||
|
})),
|
||||||
|
},
|
||||||
accounts: {
|
accounts: {
|
||||||
create: accounts.map((acc, index) => ({
|
create: accounts.map((acc, index) => ({
|
||||||
bankName: acc.bankName,
|
bankName: acc.bankName,
|
||||||
@@ -1183,6 +1259,10 @@ export class InvoicesService {
|
|||||||
dto.keyPoints !== undefined
|
dto.keyPoints !== undefined
|
||||||
? dto.keyPoints.map((kp) => this.normalizeKeyPointInput(kp))
|
? dto.keyPoints.map((kp) => this.normalizeKeyPointInput(kp))
|
||||||
: null;
|
: null;
|
||||||
|
const replacePaymentMethods =
|
||||||
|
dto.paymentMethods !== undefined
|
||||||
|
? dto.paymentMethods.map((pm) => this.normalizePaymentMethodInput(pm))
|
||||||
|
: null;
|
||||||
const replaceAccounts =
|
const replaceAccounts =
|
||||||
dto.accounts !== undefined
|
dto.accounts !== undefined
|
||||||
? dto.accounts.map((acc) => this.normalizeAccountInput(acc))
|
? dto.accounts.map((acc) => this.normalizeAccountInput(acc))
|
||||||
@@ -1195,6 +1275,9 @@ export class InvoicesService {
|
|||||||
if (replaceKeyPoints !== null) {
|
if (replaceKeyPoints !== null) {
|
||||||
await tx.invoiceTemplateKeyPoint.deleteMany({ where: { templateId } });
|
await tx.invoiceTemplateKeyPoint.deleteMany({ where: { templateId } });
|
||||||
}
|
}
|
||||||
|
if (replacePaymentMethods !== null) {
|
||||||
|
await tx.invoiceTemplatePaymentMethod.deleteMany({ where: { templateId } });
|
||||||
|
}
|
||||||
if (replaceAccounts !== null) {
|
if (replaceAccounts !== null) {
|
||||||
await tx.invoiceTemplateAccount.deleteMany({ where: { templateId } });
|
await tx.invoiceTemplateAccount.deleteMany({ where: { templateId } });
|
||||||
}
|
}
|
||||||
@@ -1232,6 +1315,16 @@ export class InvoicesService {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
|
...(replacePaymentMethods !== null
|
||||||
|
? {
|
||||||
|
paymentMethods: {
|
||||||
|
create: replacePaymentMethods.map((pm, index) => ({
|
||||||
|
text: pm.text,
|
||||||
|
sortOrder: index,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
...(replaceAccounts !== null
|
...(replaceAccounts !== null
|
||||||
? {
|
? {
|
||||||
accounts: {
|
accounts: {
|
||||||
@@ -1304,6 +1397,7 @@ export class InvoicesService {
|
|||||||
: null,
|
: null,
|
||||||
items: serialized.items,
|
items: serialized.items,
|
||||||
keyPoints: serialized.keyPoints,
|
keyPoints: serialized.keyPoints,
|
||||||
|
paymentMethods: serialized.paymentMethods,
|
||||||
accounts: serialized.accounts,
|
accounts: serialized.accounts,
|
||||||
subtotal: serialized.subtotal,
|
subtotal: serialized.subtotal,
|
||||||
total: serialized.total,
|
total: serialized.total,
|
||||||
@@ -1465,6 +1559,9 @@ export class InvoicesService {
|
|||||||
|
|
||||||
const items = dto.items.map((item) => this.normalizeItemInput(item));
|
const items = dto.items.map((item) => this.normalizeItemInput(item));
|
||||||
const keyPoints = (dto.keyPoints ?? []).map((kp) => this.normalizeKeyPointInput(kp));
|
const keyPoints = (dto.keyPoints ?? []).map((kp) => this.normalizeKeyPointInput(kp));
|
||||||
|
const paymentMethods = (dto.paymentMethods ?? []).map((pm) =>
|
||||||
|
this.normalizePaymentMethodInput(pm),
|
||||||
|
);
|
||||||
const accounts = (dto.accounts ?? []).map((acc) => this.normalizeAccountInput(acc));
|
const accounts = (dto.accounts ?? []).map((acc) => this.normalizeAccountInput(acc));
|
||||||
const invoiceTemplateId = dto.invoiceTemplateId?.trim()
|
const invoiceTemplateId = dto.invoiceTemplateId?.trim()
|
||||||
? BigInt(dto.invoiceTemplateId)
|
? BigInt(dto.invoiceTemplateId)
|
||||||
@@ -1514,6 +1611,12 @@ export class InvoicesService {
|
|||||||
sortOrder: index,
|
sortOrder: index,
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
|
paymentMethods: {
|
||||||
|
create: paymentMethods.map((pm, index) => ({
|
||||||
|
text: pm.text,
|
||||||
|
sortOrder: index,
|
||||||
|
})),
|
||||||
|
},
|
||||||
accounts: {
|
accounts: {
|
||||||
create: accounts.map((acc, index) => ({
|
create: accounts.map((acc, index) => ({
|
||||||
bankName: acc.bankName,
|
bankName: acc.bankName,
|
||||||
@@ -1600,6 +1703,9 @@ export class InvoicesService {
|
|||||||
|
|
||||||
const items = dto.items.map((item) => this.normalizeItemInput(item));
|
const items = dto.items.map((item) => this.normalizeItemInput(item));
|
||||||
const keyPoints = (dto.keyPoints ?? []).map((kp) => this.normalizeKeyPointInput(kp));
|
const keyPoints = (dto.keyPoints ?? []).map((kp) => this.normalizeKeyPointInput(kp));
|
||||||
|
const paymentMethods = (dto.paymentMethods ?? []).map((pm) =>
|
||||||
|
this.normalizePaymentMethodInput(pm),
|
||||||
|
);
|
||||||
const accounts = (dto.accounts ?? []).map((acc) => this.normalizeAccountInput(acc));
|
const accounts = (dto.accounts ?? []).map((acc) => this.normalizeAccountInput(acc));
|
||||||
|
|
||||||
let userId: bigint | undefined;
|
let userId: bigint | undefined;
|
||||||
@@ -1629,6 +1735,7 @@ export class InvoicesService {
|
|||||||
const row = await this.prisma.$transaction(async (tx) => {
|
const row = await this.prisma.$transaction(async (tx) => {
|
||||||
await tx.invoiceItem.deleteMany({ where: { invoiceId } });
|
await tx.invoiceItem.deleteMany({ where: { invoiceId } });
|
||||||
await tx.invoiceKeyPoint.deleteMany({ where: { invoiceId } });
|
await tx.invoiceKeyPoint.deleteMany({ where: { invoiceId } });
|
||||||
|
await tx.invoicePaymentMethod.deleteMany({ where: { invoiceId } });
|
||||||
await tx.invoiceAccount.deleteMany({ where: { invoiceId } });
|
await tx.invoiceAccount.deleteMany({ where: { invoiceId } });
|
||||||
|
|
||||||
return tx.invoice.update({
|
return tx.invoice.update({
|
||||||
@@ -1657,6 +1764,12 @@ export class InvoicesService {
|
|||||||
sortOrder: index,
|
sortOrder: index,
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
|
paymentMethods: {
|
||||||
|
create: paymentMethods.map((pm, index) => ({
|
||||||
|
text: pm.text,
|
||||||
|
sortOrder: index,
|
||||||
|
})),
|
||||||
|
},
|
||||||
accounts: {
|
accounts: {
|
||||||
create: accounts.map((acc, index) => ({
|
create: accounts.map((acc, index) => ({
|
||||||
bankName: acc.bankName,
|
bankName: acc.bankName,
|
||||||
|
|||||||
@@ -60,14 +60,24 @@ export class LegacySourceS3Service implements OnModuleDestroy {
|
|||||||
const slug = slugHint.trim();
|
const slug = slugHint.trim();
|
||||||
add(`${slug}_${oldBusinessId}`);
|
add(`${slug}_${oldBusinessId}`);
|
||||||
add(`${slug.replace(/-/g, '')}_${oldBusinessId}`);
|
add(`${slug.replace(/-/g, '')}_${oldBusinessId}`);
|
||||||
|
add(`${slug.replace(/_/g, '')}_${oldBusinessId}`);
|
||||||
|
add(`${slug.replace(/_/g, '-')}_${oldBusinessId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListBucket may be denied on the old bucket; slug candidates + HeadObject still work.
|
||||||
|
try {
|
||||||
const listed = await this.listTopLevelPrefixes();
|
const listed = await this.listTopLevelPrefixes();
|
||||||
for (const prefix of listed) {
|
for (const prefix of listed) {
|
||||||
if (prefix.endsWith(`_${oldBusinessId}`)) {
|
if (prefix.endsWith(`_${oldBusinessId}`)) {
|
||||||
add(prefix);
|
add(prefix);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
|
console.warn(
|
||||||
|
`Legacy S3 list prefixes failed for business ${oldBusinessId}; using slug candidates only: ${message}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (!candidates.length) {
|
if (!candidates.length) {
|
||||||
throw new ServiceUnavailableException(
|
throw new ServiceUnavailableException(
|
||||||
@@ -75,7 +85,8 @@ export class LegacySourceS3Service implements OnModuleDestroy {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sample = probes.slice(0, 12);
|
// Spread probes so early missing files don't hide a valid prefix.
|
||||||
|
const sample = this.pickProbeSample(probes, 12);
|
||||||
let bestPrefix = candidates[0];
|
let bestPrefix = candidates[0];
|
||||||
let bestHits = -1;
|
let bestHits = -1;
|
||||||
|
|
||||||
@@ -96,6 +107,16 @@ export class LegacySourceS3Service implements OnModuleDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (bestHits <= 0 && sample.length > 0) {
|
||||||
|
for (const prefix of candidates) {
|
||||||
|
if (await this.prefixHasAnyObject(prefix)) {
|
||||||
|
bestPrefix = prefix;
|
||||||
|
bestHits = 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (bestHits <= 0 && sample.length > 0) {
|
if (bestHits <= 0 && sample.length > 0) {
|
||||||
throw new ServiceUnavailableException(
|
throw new ServiceUnavailableException(
|
||||||
`Old S3 files not found for business ${oldBusinessId} (tried: ${candidates.join(', ')}).`,
|
`Old S3 files not found for business ${oldBusinessId} (tried: ${candidates.join(', ')}).`,
|
||||||
@@ -106,6 +127,35 @@ export class LegacySourceS3Service implements OnModuleDestroy {
|
|||||||
return bestPrefix;
|
return bestPrefix;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private pickProbeSample<T>(items: T[], n: number): T[] {
|
||||||
|
if (items.length <= n) return [...items];
|
||||||
|
const out: T[] = [];
|
||||||
|
const seen = new Set<number>();
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
const idx = Math.floor((i * (items.length - 1)) / Math.max(n - 1, 1));
|
||||||
|
if (seen.has(idx)) continue;
|
||||||
|
seen.add(idx);
|
||||||
|
out.push(items[idx]);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async prefixHasAnyObject(prefix: string): Promise<boolean> {
|
||||||
|
const { client, bucket } = this.getClient();
|
||||||
|
try {
|
||||||
|
const res = await client.send(
|
||||||
|
new ListObjectsV2Command({
|
||||||
|
Bucket: bucket,
|
||||||
|
Prefix: `${prefix}/`,
|
||||||
|
MaxKeys: 1,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return (res.Contents?.length ?? 0) > 0;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async getObjectBuffer(key: string): Promise<Buffer> {
|
async getObjectBuffer(key: string): Promise<Buffer> {
|
||||||
const { client, bucket } = this.getClient();
|
const { client, bucket } = this.getClient();
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user