Compare commits

...

2 Commits

Author SHA1 Message Date
morteza 9ccddbde11 count
deploy to danak / build_and_deploy (push) Has been cancelled
2026-07-23 18:47:56 +03:30
morteza 3314113e7a invoice 2026-07-23 18:13:25 +03:30
11 changed files with 165 additions and 46 deletions
+24 -21
View File
@@ -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"
@@ -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;
+14
View File
@@ -0,0 +1,14 @@
{
"folders": [
{
"path": "."
},
{
"path": "../negareh-console"
},
{
"path": "../negareh-admin"
}
],
"settings": {}
}
+1 -1
View File
@@ -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] },
}),
+13 -3
View File
@@ -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()
@@ -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
@@ -2,4 +2,5 @@ export enum InvoiceConfirmStatusEnum {
PENDING = 'pending',
PARTIALLY_CONFIRMED = 'partially_confirmed',
CONFIRMED = 'confirmed',
ARCHIVED = 'archived',
}
@@ -1,5 +0,0 @@
export enum InvoiceStatusEnum {
PENDING = 'pending',
PAID = 'paid',
}
+23
View File
@@ -23,6 +23,13 @@ export class InvoiceController {
return this.invoiceService.findAll(userId, dto);
}
@Get('public/invoice/tab-counts')
@UseGuards(AuthGuard)
@ApiOperation({ summary: 'Get invoice tab counts for the current user' })
getTabCounts(@Query() dto: FindInvoicesDto, @UserId() userId: string) {
return this.invoiceService.getTabCounts(dto, userId);
}
@Get('public/invoice/:id')
@UseGuards(AuthGuard)
@ApiOperation({ summary: 'Get one invoice by id' })
@@ -55,6 +62,14 @@ export class InvoiceController {
return this.invoiceService.findAllAsAdmin(dto);
}
@Get('admin/invoice/tab-counts')
@UseGuards(AdminAuthGuard)
@Permissions(PermissionEnum.VIEW_INVOICES)
@ApiOperation({ summary: 'Get invoice tab counts for admin invoice list' })
getTabCountsAsAdmin(@Query() dto: FindInvoicesDto) {
return this.invoiceService.getTabCounts(dto);
}
@Get('admin/invoice/item/:id')
@UseGuards(AdminAuthGuard)
@Permissions(PermissionEnum.VIEW_INVOICES)
@@ -80,6 +95,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)
+50
View File
@@ -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 = {
@@ -19,6 +20,12 @@ type ResolvedItemDiscount = {
discountPercent?: number;
};
export type InvoiceTabCounts = {
pending: number;
confirmed: number;
archived: number;
};
@Injectable()
export class InvoiceService {
constructor(
@@ -175,6 +182,39 @@ export class InvoiceService {
return this.invoiceRepository.findAllPaginated(dto);
}
getTabCounts(dto: FindInvoicesDto, userId?: string): Promise<InvoiceTabCounts> {
const baseFilter: FindInvoicesDto & { userId?: string } = {
...dto,
userId,
};
return this.countTabs(baseFilter);
}
private async countTabs(
baseFilter: FindInvoicesDto & { userId?: string },
): Promise<InvoiceTabCounts> {
const [pending, confirmed, archived] = await Promise.all([
this.invoiceRepository.countByFilters({
...baseFilter,
statuses: [InvoiceConfirmStatusEnum.PENDING],
}),
this.invoiceRepository.countByFilters({
...baseFilter,
statuses: [
InvoiceConfirmStatusEnum.PARTIALLY_CONFIRMED,
InvoiceConfirmStatusEnum.CONFIRMED,
],
}),
this.invoiceRepository.countByFilters({
...baseFilter,
statuses: [InvoiceConfirmStatusEnum.ARCHIVED],
}),
]);
return { pending, confirmed, archived };
}
async findOne(id: string, userId: string) {
const invoice = await this.em.findOne(
Invoice,
@@ -336,6 +376,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);
@@ -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;
@@ -15,23 +16,17 @@ export class InvoiceRepository extends EntityRepository<Invoice> {
super(em, Invoice);
}
async findAllPaginated(opts: FindInvoicesOpts): Promise<PaginatedResult<Invoice>> {
private buildWhere(opts: FindInvoicesOpts): FilterQuery<Invoice> {
const {
page = 1,
limit = 10,
search,
orderBy = 'createdAt',
order = 'desc',
userId,
from,
to,
enableTax,
requestId,
confirmStatus,
statuses,
} = opts;
const offset = (page - 1) * limit;
const where: FilterQuery<Invoice> = {};
if (userId) {
@@ -46,8 +41,17 @@ export class InvoiceRepository extends EntityRepository<Invoice> {
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) {
@@ -76,6 +80,20 @@ export class InvoiceRepository extends EntityRepository<Invoice> {
where.$or = searchConditions;
}
return where;
}
async findAllPaginated(opts: FindInvoicesOpts): Promise<PaginatedResult<Invoice>> {
const {
page = 1,
limit = 10,
orderBy = 'createdAt',
order = 'desc',
} = opts;
const offset = (page - 1) * limit;
const where = this.buildWhere(opts);
const [data, total] = await this.findAndCount(where, {
limit,
offset,
@@ -95,4 +113,8 @@ export class InvoiceRepository extends EntityRepository<Invoice> {
},
};
}
async countByFilters(opts: FindInvoicesOpts): Promise<number> {
return this.count(this.buildWhere(opts));
}
}