feat: add stock keeping unit management with create, update, and retrieval functionalities

- Implemented CreateStockKeepingUnitDto for creating stock keeping units.
- Added StockKeepingUnitsService for handling business logic related to stock keeping units.
- Created StockKeepingUnitsController to manage HTTP requests for stock keeping units.
- Developed UpdateStockKeepingUnitDto for updating existing stock keeping units.
- Introduced StockKeepingUnitsServiceFindAllResponseDto for response structure.
- Established stock keeping units module for encapsulation of related components.

feat: enhance sales invoice fiscal management with new endpoints

- Created SalesInvoicesFilterDto for filtering sales invoices.
- Implemented PosSalesInvoiceFiscalController for managing fiscal operations on sales invoices.
- Developed PosSalesInvoiceFiscalService to handle fiscal logic and interactions.
- Added methods for sending, retrying, and checking the status of fiscal invoices.
- Integrated error handling and response mapping for fiscal operations.
This commit is contained in:
2026-05-01 19:43:59 +03:30
parent a68a7f594d
commit ad470d2166
91 changed files with 8973 additions and 2469 deletions
@@ -1,12 +1,18 @@
import { IPosPayload } from '@/common/models/posPayload.model'
import { translateEnumValue } from '@/common/utils'
import { PrismaService } from '@/prisma/prisma.service'
import { BadRequestException, Injectable } from '@nestjs/common'
import { ResponseMapper } from 'common/response/response-mapper'
import type { CustomerIndividual, CustomerLegal } from 'generated/prisma/client'
import { Prisma } from 'generated/prisma/client'
import { CustomerType, PaymentMethodType } from 'generated/prisma/enums'
import {
CustomerType,
FiscalResponseStatus,
PaymentMethodType,
} from 'generated/prisma/enums'
import { SalesInvoiceFiscalService } from '../../consumer/saleInvoices/fiscal/sales-invoice-fiscal.service'
import { CreateSalesInvoiceDto } from './dto/create-sales-invoice.dto'
import { SalesInvoicesFilterDto } from './dto/sales-invoices-filter.dto'
// Define type guard for CustomerIndividual
function isCustomerIndividual(customer: any): customer is CustomerIndividual {
@@ -50,14 +56,213 @@ export class SalesInvoicesService {
private salesInvoiceTaxService: SalesInvoiceFiscalService,
) {}
findAll() {
// TODO: Implement fetching all sales invoices
return []
async findAll(posInfo: IPosPayload, query: SalesInvoicesFilterDto = {}) {
const page = query.page || 1
const perPage = Math.min(query.perPage || 10, 100)
const where = this.buildFindAllWhere(posInfo, query)
const [items, total] = await this.prisma.$transaction(async tx => [
await tx.salesInvoice.findMany({
where,
orderBy: {
created_at: 'desc',
},
skip: (page - 1) * perPage,
take: perPage,
select: {
id: true,
code: true,
invoice_number: true,
invoice_date: true,
created_at: true,
total_amount: true,
customer: {
select: {
type: true,
individual: {
select: {
first_name: true,
last_name: true,
mobile_number: true,
national_id: true,
},
},
legal: {
select: {
name: true,
economic_code: true,
registration_number: true,
},
},
},
},
fiscal: {
select: {
attempts: {
orderBy: {
created_at: 'desc',
},
take: 1,
select: {
status: true,
},
},
},
},
},
}),
await tx.salesInvoice.count({ where }),
])
const summaryItems = items.map(invoice => ({
...invoice,
status: translateEnumValue(
'FiscalResponseStatus',
invoice.fiscal?.attempts?.[0]?.status || FiscalResponseStatus.NOT_SEND,
),
}))
return ResponseMapper.paginate(summaryItems, { total, page, perPage })
}
findOne(id: number) {
// TODO: Implement fetching a single sales invoice
return {}
async findOne(posInfo: IPosPayload, id: string) {
const invoice = await this.prisma.salesInvoice.findFirstOrThrow({
where: {
id,
pos: {
complex: {
business_activity_id: posInfo.business_id,
},
},
},
select: {
id: true,
code: true,
invoice_number: true,
invoice_date: true,
created_at: true,
updated_at: true,
notes: true,
total_amount: true,
unknown_customer: true,
customer: {
select: {
id: true,
type: true,
individual: {
select: {
first_name: true,
last_name: true,
mobile_number: true,
national_id: true,
postal_code: true,
economic_code: true,
},
},
legal: {
select: {
name: true,
economic_code: true,
registration_number: true,
postal_code: true,
},
},
},
},
pos: {
select: {
id: true,
name: true,
complex: {
select: {
id: true,
name: true,
business_activity: {
select: {
id: true,
name: true,
},
},
},
},
},
},
consumer_account: {
select: {
id: true,
role: true,
account: {
select: {
username: true,
},
},
},
},
items: {
select: {
id: true,
good_id: true,
service_id: true,
quantity: true,
measure_unit_code: true,
measure_unit_text: true,
sku_code: true,
unit_price: true,
discount: true,
total_amount: true,
notes: true,
payload: true,
good_snapshot: true,
},
},
payments: {
select: {
id: true,
amount: true,
payment_method: true,
paid_at: true,
created_at: true,
terminal_info: {
select: {
terminal_id: true,
stan: true,
rrn: true,
transaction_date_time: true,
customer_card_no: true,
description: true,
},
},
},
},
fiscal: {
select: {
id: true,
retry_count: true,
last_attempt_at: true,
attempts: {
orderBy: {
created_at: 'desc',
},
select: {
id: true,
attempt_no: true,
status: true,
tax_id: true,
error_message: true,
sent_at: true,
received_at: true,
created_at: true,
},
},
},
},
},
})
return ResponseMapper.single({
...invoice,
status: invoice.fiscal?.attempts?.[0]?.status || FiscalResponseStatus.NOT_SEND,
})
}
// async send(invoiceId: string, posInfo: IPosPayload) {
@@ -83,7 +288,7 @@ export class SalesInvoicesService {
// }
async create(data: CreateSalesInvoiceDto, posInfo: IPosPayload) {
const { business_id, pos_id, consumer_account_id } = posInfo
const { business_id, pos_id, consumer_account_id, complex_id } = posInfo
const normalizedInvoiceDate = this.normalizeInvoiceDate(data.invoice_date)
const { payments, terminalInfo } = this.buildPaymentsData(
data.payments,
@@ -101,6 +306,8 @@ export class SalesInvoicesService {
normalizedInvoiceDate,
invoiceNumber,
consumer_account_id,
businessId: business_id,
complexId: complex_id,
pos_id,
goodsById,
customerId: newCustomerId,
@@ -156,6 +363,161 @@ export class SalesInvoicesService {
)
}
private buildFindAllWhere(posInfo: IPosPayload, query: SalesInvoicesFilterDto) {
const where: Prisma.SalesInvoiceWhereInput = {
pos: {
complex: {
business_activity_id: posInfo.business_id,
},
},
}
if (query.code?.trim()) {
where.code = {
contains: query.code.trim(),
}
}
if (query.invoice_date_from || query.invoice_date_to) {
where.invoice_date = {
...(query.invoice_date_from ? { gte: new Date(query.invoice_date_from) } : {}),
...(query.invoice_date_to ? { lte: new Date(query.invoice_date_to) } : {}),
}
}
if (query.created_at_from || query.created_at_to) {
where.created_at = {
...(query.created_at_from ? { gte: new Date(query.created_at_from) } : {}),
...(query.created_at_to ? { lte: new Date(query.created_at_to) } : {}),
}
}
if (
query.total_amount !== undefined ||
query.total_amount_from !== undefined ||
query.total_amount_to !== undefined
) {
where.total_amount = {
...(query.total_amount !== undefined ? { equals: query.total_amount } : {}),
...(query.total_amount_from !== undefined
? { gte: query.total_amount_from }
: {}),
...(query.total_amount_to !== undefined ? { lte: query.total_amount_to } : {}),
}
}
if (
query.customer_name?.trim() ||
query.customer_mobile?.trim() ||
query.customer_national_id?.trim() ||
query.customer_economic_code?.trim()
) {
where.customer = {
is: {
OR: [
...(query.customer_name?.trim()
? [
{
individual: {
is: {
OR: [
{
first_name: {
contains: query.customer_name.trim(),
},
},
{
last_name: {
contains: query.customer_name.trim(),
},
},
],
},
},
},
{
legal: {
is: {
name: {
contains: query.customer_name.trim(),
},
},
},
},
]
: []),
...(query.customer_mobile?.trim()
? [
{
individual: {
is: {
mobile_number: {
contains: query.customer_mobile.trim(),
},
},
},
},
]
: []),
...(query.customer_national_id?.trim()
? [
{
individual: {
is: {
national_id: {
contains: query.customer_national_id.trim(),
},
},
},
},
]
: []),
...(query.customer_economic_code?.trim()
? [
{
legal: {
is: {
OR: [
{
economic_code: {
contains: query.customer_economic_code.trim(),
},
},
{
registration_number: {
contains: query.customer_economic_code.trim(),
},
},
],
},
},
},
]
: []),
],
},
}
}
if (query.status) {
if (query.status === FiscalResponseStatus.NOT_SEND) {
where.fiscal = null
} else {
where.fiscal = {
is: {
attempts: {
some: {
status: query.status,
},
},
},
}
}
}
return where
}
private normalizeInvoiceDate(invoiceDate: Date | string) {
return new Date(invoiceDate).toISOString() as any
}
@@ -339,11 +701,21 @@ export class SalesInvoicesService {
select: {
id: true,
name: true,
sku: true,
sku: {
select: {
id: true,
name: true,
},
},
local_sku: true,
barcode: true,
pricing_model: true,
unit_type: true,
measure_unit: {
select: {
id: true,
name: true,
},
},
base_sale_price: true,
image_url: true,
category: {
@@ -364,6 +736,8 @@ export class SalesInvoicesService {
normalizedInvoiceDate: Date
invoiceNumber: number
consumer_account_id: string
businessId: string
complexId: string
pos_id: string
goodsById: Map<string, any>
customerId: string | null
@@ -376,6 +750,8 @@ export class SalesInvoicesService {
pos_id,
goodsById,
customerId,
businessId,
complexId,
} = params
const { customer_id, customer_type, customer, payments, ...invoiceData } = data
@@ -384,7 +760,7 @@ export class SalesInvoicesService {
invoice_date: normalizedInvoiceDate,
invoice_number: invoiceNumber,
total_amount: data.total_amount,
code: 'INV-' + Date.now(),
code: this.generateInvoiceCode(businessId, complexId, pos_id, invoiceNumber),
items: {
createMany: {
data: data.items.map(item => ({
@@ -392,6 +768,7 @@ export class SalesInvoicesService {
quantity: item.quantity,
unit_price: item.unit_price,
total_amount: item.total_amount,
measure_unit_text: item.unit_type,
payload: item.payload ? JSON.parse(JSON.stringify(item.payload)) : undefined,
good_snapshot: item.good_id
? JSON.parse(
@@ -483,4 +860,13 @@ export class SalesInvoicesService {
}
}
}
private generateInvoiceCode(
business_id: string,
complex_id: string,
pos_id: string,
invoice_number: number,
) {
return `${business_id.substring(0, 2)}-${complex_id.substring(0, 2)}-${pos_id.substring(0, 2)}-${invoice_number.toString()}`
}
}