From 3314113e7aab93b473d41f653a3fe8328c745c4e Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Thu, 23 Jul 2026 18:13:25 +0330 Subject: [PATCH] invoice --- database/migrations/.snapshot-negareh.json | 45 ++++++++++--------- .../migrations/Migration20260626140000.ts | 9 ++-- negare.code-workspace | 14 ++++++ src/modules/admin/providers/admin.service.ts | 2 +- src/modules/invoice/dto/find-invoices.dto.ts | 16 +++++-- .../invoice/entities/invoice.entity.ts | 4 +- .../enum/invoice-confirm-status.enum.ts | 1 + .../invoice/enum/invoice-status.enum.ts | 5 --- src/modules/invoice/invoice.controller.ts | 8 ++++ src/modules/invoice/invoice.service.ts | 11 +++++ .../repositories/invoice.repository.ts | 16 +++++-- 11 files changed, 92 insertions(+), 39 deletions(-) create mode 100644 negare.code-workspace delete mode 100644 src/modules/invoice/enum/invoice-status.enum.ts diff --git a/database/migrations/.snapshot-negareh.json b/database/migrations/.snapshot-negareh.json index b7e7520..9a4d14b 100644 --- a/database/migrations/.snapshot-negareh.json +++ b/database/migrations/.snapshot-negareh.json @@ -703,6 +703,16 @@ "nullable": true, "length": 6, "mappedType": "datetime" + }, + "title": { + "name": "title", + "type": "varchar(255)", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 255, + "mappedType": "string" } }, "name": "order_prints", @@ -2063,24 +2073,6 @@ "nullable": false, "length": 26, "mappedType": "character" - }, - "description": { - "name": "description", - "type": "text", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": true, - "mappedType": "text" - }, - "attachments": { - "name": "attachments", - "type": "jsonb", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": true, - "mappedType": "json" } }, "name": "request_items", @@ -2509,8 +2501,8 @@ "nullable": false, "mappedType": "integer" }, - "confirm_status": { - "name": "confirm_status", + "status": { + "name": "status", "type": "text", "unsigned": false, "autoincrement": false, @@ -2520,7 +2512,8 @@ "enumItems": [ "pending", "partially_confirmed", - "confirmed" + "confirmed", + "archived" ], "mappedType": "enum" }, @@ -3213,6 +3206,16 @@ "name": "orders", "schema": "public", "indexes": [ + { + "columnNames": [ + "invoice_item_id" + ], + "composite": false, + "keyName": "orders_invoice_item_id_unique", + "constraint": true, + "primary": false, + "unique": true + }, { "columnNames": [ "print_id" diff --git a/database/migrations/Migration20260626140000.ts b/database/migrations/Migration20260626140000.ts index 47a38b1..02a3e59 100644 --- a/database/migrations/Migration20260626140000.ts +++ b/database/migrations/Migration20260626140000.ts @@ -15,7 +15,7 @@ export class Migration20260626140000 extends Migration { v_total int; v_paid int; v_enable_tax boolean; - v_confirm_status text; + v_status text; BEGIN v_invoice_id := COALESCE(NEW.invoice_id, OLD.invoice_id); @@ -68,7 +68,7 @@ export class Migration20260626140000 extends Migration { ) THEN 'confirmed' ELSE 'partially_confirmed' END - INTO v_confirm_status; + INTO v_status; UPDATE invoices SET @@ -78,8 +78,9 @@ export class Migration20260626140000 extends Migration { total = v_total, paid_amount = v_paid, balance = v_total - v_paid, - confirm_status = v_confirm_status - WHERE id = v_invoice_id; + status = v_status + WHERE id = v_invoice_id + AND status <> 'archived'; RETURN NULL; END; diff --git a/negare.code-workspace b/negare.code-workspace new file mode 100644 index 0000000..b8e7208 --- /dev/null +++ b/negare.code-workspace @@ -0,0 +1,14 @@ +{ + "folders": [ + { + "path": "." + }, + { + "path": "../negareh-console" + }, + { + "path": "../negareh-admin" + } + ], + "settings": {} +} \ No newline at end of file diff --git a/src/modules/admin/providers/admin.service.ts b/src/modules/admin/providers/admin.service.ts index 7d84a28..12c1177 100644 --- a/src/modules/admin/providers/admin.service.ts +++ b/src/modules/admin/providers/admin.service.ts @@ -233,7 +233,7 @@ export class AdminService { customersCount, ] = await Promise.all([ this.em.count(Request), - this.em.count(Invoice, { confirmStatus: InvoiceConfirmStatusEnum.PENDING }), + this.em.count(Invoice, { status: InvoiceConfirmStatusEnum.PENDING }), this.em.count(Order, { status: { $nin: [OrderStatusEnum.FINISHED, OrderStatusEnum.CANCELED] }, }), diff --git a/src/modules/invoice/dto/find-invoices.dto.ts b/src/modules/invoice/dto/find-invoices.dto.ts index 22614d5..19429d0 100644 --- a/src/modules/invoice/dto/find-invoices.dto.ts +++ b/src/modules/invoice/dto/find-invoices.dto.ts @@ -8,6 +8,7 @@ import { IsDateString, IsBoolean, IsEnum, + IsArray, } from 'class-validator'; import { InvoiceConfirmStatusEnum } from '../enum/invoice-confirm-status.enum'; import { Type, Transform } from 'class-transformer'; @@ -68,12 +69,21 @@ export class FindInvoicesDto { enableTax?: boolean; @ApiPropertyOptional({ + description: 'Filter by invoice statuses', enum: InvoiceConfirmStatusEnum, - description: 'Filter by invoice confirmation status', + isArray: true, }) @IsOptional() - @IsEnum(InvoiceConfirmStatusEnum) - confirmStatus?: InvoiceConfirmStatusEnum; + @Transform(({ value }: { value: unknown }) => + Array.isArray(value) + ? value + : typeof value === 'string' + ? value.split(',') + : value, + ) + @IsArray() + @IsEnum(InvoiceConfirmStatusEnum, { each: true }) + statuses?: InvoiceConfirmStatusEnum[]; @ApiPropertyOptional({ default: 'createdAt' }) @IsOptional() diff --git a/src/modules/invoice/entities/invoice.entity.ts b/src/modules/invoice/entities/invoice.entity.ts index c2d7668..3f68199 100644 --- a/src/modules/invoice/entities/invoice.entity.ts +++ b/src/modules/invoice/entities/invoice.entity.ts @@ -23,7 +23,7 @@ import { IAttachment } from 'src/modules/order/interface/order.interface'; @Entity({ tableName: 'invoices' }) @Index({ properties: ['user'] }) export class Invoice extends BaseEntity { - [OptionalProps]?: 'createdAt' | 'deletedAt' | 'invoiceNumber' | 'confirmStatus' + [OptionalProps]?: 'createdAt' | 'deletedAt' | 'invoiceNumber' | 'status' @PrimaryKey({ type: 'string', columnType: 'char(26)' }) id: string = ulid() @@ -80,7 +80,7 @@ export class Invoice extends BaseEntity { @Enum(() => InvoiceConfirmStatusEnum) - confirmStatus: InvoiceConfirmStatusEnum = InvoiceConfirmStatusEnum.PENDING; + status: InvoiceConfirmStatusEnum = InvoiceConfirmStatusEnum.PENDING; @Property({ type: 'boolean', default: false }) enableTax?: boolean = false diff --git a/src/modules/invoice/enum/invoice-confirm-status.enum.ts b/src/modules/invoice/enum/invoice-confirm-status.enum.ts index 6d350e2..bae6a0a 100644 --- a/src/modules/invoice/enum/invoice-confirm-status.enum.ts +++ b/src/modules/invoice/enum/invoice-confirm-status.enum.ts @@ -2,4 +2,5 @@ export enum InvoiceConfirmStatusEnum { PENDING = 'pending', PARTIALLY_CONFIRMED = 'partially_confirmed', CONFIRMED = 'confirmed', + ARCHIVED = 'archived', } diff --git a/src/modules/invoice/enum/invoice-status.enum.ts b/src/modules/invoice/enum/invoice-status.enum.ts deleted file mode 100644 index c0ae5c3..0000000 --- a/src/modules/invoice/enum/invoice-status.enum.ts +++ /dev/null @@ -1,5 +0,0 @@ -export enum InvoiceStatusEnum { - PENDING = 'pending', - PAID = 'paid', -} - diff --git a/src/modules/invoice/invoice.controller.ts b/src/modules/invoice/invoice.controller.ts index 2ddcee3..447f253 100644 --- a/src/modules/invoice/invoice.controller.ts +++ b/src/modules/invoice/invoice.controller.ts @@ -80,6 +80,14 @@ export class InvoiceController { return this.invoiceService.update(id, updateInvoiceDto); } + @Patch('admin/invoice/:id/archive') + @UseGuards(AdminAuthGuard) + @Permissions(PermissionEnum.UPDATE_INVOICE) + @ApiOperation({ summary: 'Archive an invoice (admin)' }) + archive(@Param('id') id: string) { + return this.invoiceService.archive(id); + } + @Delete('admin/invoice/:id') @UseGuards(AdminAuthGuard) @Permissions(PermissionEnum.DELETE_INVOICE) diff --git a/src/modules/invoice/invoice.service.ts b/src/modules/invoice/invoice.service.ts index 2568fdf..89026a8 100644 --- a/src/modules/invoice/invoice.service.ts +++ b/src/modules/invoice/invoice.service.ts @@ -12,6 +12,7 @@ import { ProductService } from '../product/providers/product.service'; import { RequestService } from '../request/request.service'; import { InvoiceRepository } from './repositories/invoice.repository'; import { InvoiceCreatedEvent } from './events/invoice.events'; +import { InvoiceConfirmStatusEnum } from './enum/invoice-confirm-status.enum'; const TAX_RATE = 0.1; type ResolvedItemDiscount = { @@ -336,6 +337,16 @@ export class InvoiceService { return invoice; } + async archive(id: string) { + const invoice = await this.findOneOrFail(id); + if (invoice.status === InvoiceConfirmStatusEnum.ARCHIVED) { + throw new BadRequestException('Invoice is already archived'); + } + invoice.status = InvoiceConfirmStatusEnum.ARCHIVED; + await this.em.flush(); + return invoice; + } + async remove(id: string) { const invoice = await this.findOneOrFail(id); this.em.remove(invoice); diff --git a/src/modules/invoice/repositories/invoice.repository.ts b/src/modules/invoice/repositories/invoice.repository.ts index dc0c553..735ff27 100644 --- a/src/modules/invoice/repositories/invoice.repository.ts +++ b/src/modules/invoice/repositories/invoice.repository.ts @@ -4,6 +4,7 @@ import { FilterQuery } from '@mikro-orm/core'; import { Invoice } from '../entities/invoice.entity'; import { PaginatedResult } from 'src/common/interfaces/pagination.interface'; import { FindInvoicesDto } from '../dto/find-invoices.dto'; +import { InvoiceConfirmStatusEnum } from '../enum/invoice-confirm-status.enum'; interface FindInvoicesOpts extends FindInvoicesDto { userId?: string; @@ -27,7 +28,7 @@ export class InvoiceRepository extends EntityRepository { to, enableTax, requestId, - confirmStatus, + statuses, } = opts; const offset = (page - 1) * limit; @@ -46,8 +47,17 @@ export class InvoiceRepository extends EntityRepository { where.enableTax = enableTax; } - if (confirmStatus) { - where.confirmStatus = confirmStatus; + if (statuses) { + const normalizedStatuses = Array.isArray(statuses) ? statuses : [statuses]; + const validStatuses = normalizedStatuses.filter( + (s): s is InvoiceConfirmStatusEnum => !!s, + ); + + if (validStatuses.length === 1) { + where.status = validStatuses[0]; + } else if (validStatuses.length > 1) { + where.status = { $in: validStatuses }; + } } if (from || to) {