From 7133b47c95b57826c6d352ecba539c3c81c6f3eb Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Sat, 16 May 2026 15:59:40 +0330 Subject: [PATCH] invoice item total and subtotal --- .dockerignore | 11 +++++++++ Dockerfile | 7 +++--- database/migrations/.snapshot-negareh.json | 20 ++++++++++++++++ .../migrations/Migration20260516121012.ts | 9 ++++++++ src/modules/invoice/dto/create-invoice.dto.ts | 3 ++- .../invoice/entities/invoice-item.entity.ts | 2 -- src/modules/invoice/invoice.service.ts | 23 +++++++++++-------- 7 files changed, 58 insertions(+), 17 deletions(-) create mode 100644 .dockerignore create mode 100644 database/migrations/Migration20260516121012.ts diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..3a3e6c0 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,11 @@ +node_modules +dist +.git +.gitignore +*.md +.env* +coverage +.nyc_output +logs +*.log +npm-debug.log* diff --git a/Dockerfile b/Dockerfile index 9ee9880..432495f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,8 @@ # Stage 1: Base FROM node:22-alpine AS base -# Install timezone support -RUN apk add --no-cache tzdata -RUN cp /usr/share/zoneinfo/Asia/Tehran /etc/localtime && echo "Asia/Tehran" > /etc/timezone +# Set timezone (Node.js respects TZ env variable natively) +ENV TZ=Asia/Tehran # Set workdir for clarity WORKDIR /app @@ -12,7 +11,7 @@ WORKDIR /app FROM base AS deps WORKDIR /temp-deps COPY package*.json ./ -RUN npm ci --ignore-scripts +RUN npm ci --ignore-scripts --no-fund --no-audit # Stage 3: Build FROM base AS builder diff --git a/database/migrations/.snapshot-negareh.json b/database/migrations/.snapshot-negareh.json index 9ac3c42..102c310 100644 --- a/database/migrations/.snapshot-negareh.json +++ b/database/migrations/.snapshot-negareh.json @@ -2831,6 +2831,16 @@ "nullable": false, "mappedType": "integer" }, + "sub_total": { + "name": "sub_total", + "type": "int", + "generated": "(COALESCE(unit_price, 0) * quantity) STORED", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "integer" + }, "discount": { "name": "discount", "type": "int", @@ -2840,6 +2850,16 @@ "nullable": true, "mappedType": "integer" }, + "total": { + "name": "total", + "type": "int", + "generated": "((COALESCE(unit_price, 0) * quantity) - COALESCE(discount, 0)) STORED", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "integer" + }, "description": { "name": "description", "type": "text", diff --git a/database/migrations/Migration20260516121012.ts b/database/migrations/Migration20260516121012.ts new file mode 100644 index 0000000..56dfa95 --- /dev/null +++ b/database/migrations/Migration20260516121012.ts @@ -0,0 +1,9 @@ +import { Migration } from '@mikro-orm/migrations'; + +export class Migration20260516121012 extends Migration { + + override async up(): Promise { + this.addSql(`alter table "invoice_items" add column "sub_total" int generated always as (COALESCE(unit_price, 0) * quantity) STORED not null, add column "total" int generated always as ((COALESCE(unit_price, 0) * quantity) - COALESCE(discount, 0)) STORED not null;`); + } + +} diff --git a/src/modules/invoice/dto/create-invoice.dto.ts b/src/modules/invoice/dto/create-invoice.dto.ts index f3bbf9c..0b2e502 100644 --- a/src/modules/invoice/dto/create-invoice.dto.ts +++ b/src/modules/invoice/dto/create-invoice.dto.ts @@ -88,7 +88,8 @@ export class CreateInvoiceDto { description?: string; @IsArray() + @IsOptional() @Type(() => IAttachmentDto) @ApiProperty({ type: [IAttachmentDto] }) - attachments: IAttachmentDto[]; + attachments?: IAttachmentDto[]; } diff --git a/src/modules/invoice/entities/invoice-item.entity.ts b/src/modules/invoice/entities/invoice-item.entity.ts index 4a7ba0d..d6623ec 100644 --- a/src/modules/invoice/entities/invoice-item.entity.ts +++ b/src/modules/invoice/entities/invoice-item.entity.ts @@ -29,7 +29,6 @@ export class InvoiceItem extends BaseEntity { @Property({ type: 'int', columnType: 'int GENERATED ALWAYS AS (COALESCE(unit_price, 0) * quantity) STORED', - persist: false, }) subTotal!: number & Opt; @@ -42,7 +41,6 @@ export class InvoiceItem extends BaseEntity { @Property({ type: 'int', columnType: 'int GENERATED ALWAYS AS ((COALESCE(unit_price, 0) * quantity) - COALESCE(discount, 0)) STORED', - persist: false }) total!: number & Opt; diff --git a/src/modules/invoice/invoice.service.ts b/src/modules/invoice/invoice.service.ts index c6ba069..7732b76 100644 --- a/src/modules/invoice/invoice.service.ts +++ b/src/modules/invoice/invoice.service.ts @@ -22,7 +22,7 @@ export class InvoiceService { private readonly productService: ProductService, private readonly invoiceRepository: InvoiceRepository, private readonly eventEmitter: EventEmitter2, - ) {} + ) { } async create(dto: CreateInvoiceDto) { @@ -59,13 +59,16 @@ export class InvoiceService { ...(request && { request }), enableTax, approvalDeadline, - // discount, + discount: 0, paidAmount: 0, - // balance: 0, - attachments: [], + balance: 0, + attachments: dto.attachments ?? [], paymentMethod: dto.paymentMethod, invoiceNumber: 20, // TODO : invoice number - description: dto.description + description: dto.description, + subTotal: 0, + taxAmount: 0, + total: 0, }); em.persist(invoice); @@ -95,7 +98,7 @@ export class InvoiceService { // const total = Math.max(0, subTotal - discount + taxAmount); // invoice.subTotal = subTotal; - invoice.attachments = dto.attachments; + // invoice.attachments = dto.attachments; // invoice.taxAmount = taxAmount; // invoice.total = total; // invoice.balance = total - invoice.paidAmount; @@ -114,7 +117,7 @@ export class InvoiceService { await em.flush(); return invoice; }); -// TODO : maybe it is needed to get a fresh instance. + // TODO : maybe it is needed to get a fresh instance. this.eventEmitter.emit( InvoiceCreatedEvent.name, new InvoiceCreatedEvent( @@ -184,9 +187,9 @@ export class InvoiceService { if (dto.approvalDeadline !== undefined) { invoice.approvalDeadline = new Date(dto.approvalDeadline); } - if(dto.paymentMethod !== undefined) invoice.paymentMethod = dto.paymentMethod; - if(dto.description !== undefined) invoice.description = dto.description; - if(dto.attachments !== undefined) invoice.attachments = dto.attachments; + if (dto.paymentMethod !== undefined) invoice.paymentMethod = dto.paymentMethod; + if (dto.description !== undefined) invoice.description = dto.description; + if (dto.attachments !== undefined) invoice.attachments = dto.attachments; if (dto.items !== undefined) { // Items in dto.items represent the full list: delete any existing items not in the list