2026-03-29 18:06:41 +03:30
|
|
|
import { IPosPayload } from '@/common/models/posPayload.model'
|
2026-05-01 19:43:59 +03:30
|
|
|
import { translateEnumValue } from '@/common/utils'
|
2026-05-03 16:23:17 +03:30
|
|
|
import { SalesInvoiceCreateInput } from '@/generated/prisma/models'
|
2026-03-07 11:25:11 +03:30
|
|
|
import { PrismaService } from '@/prisma/prisma.service'
|
2026-05-03 16:23:17 +03:30
|
|
|
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'
|
2026-03-07 11:25:11 +03:30
|
|
|
import { ResponseMapper } from 'common/response/response-mapper'
|
|
|
|
|
import type { CustomerIndividual, CustomerLegal } from 'generated/prisma/client'
|
2026-04-30 16:27:46 +03:30
|
|
|
import { Prisma } from 'generated/prisma/client'
|
2026-05-01 19:43:59 +03:30
|
|
|
import {
|
2026-05-03 16:23:17 +03:30
|
|
|
ConsumerRole,
|
2026-05-01 19:43:59 +03:30
|
|
|
CustomerType,
|
|
|
|
|
PaymentMethodType,
|
2026-05-03 16:23:17 +03:30
|
|
|
TspProviderRequestType,
|
|
|
|
|
TspProviderResponseStatus,
|
2026-05-01 19:43:59 +03:30
|
|
|
} from 'generated/prisma/enums'
|
2026-05-03 16:23:17 +03:30
|
|
|
import { SalesInvoiceTspService } from '../../tspProviders/sales-invoice-tsp.service'
|
2026-02-24 12:42:10 +03:30
|
|
|
import { CreateSalesInvoiceDto } from './dto/create-sales-invoice.dto'
|
2026-05-01 19:43:59 +03:30
|
|
|
import { SalesInvoicesFilterDto } from './dto/sales-invoices-filter.dto'
|
2026-02-24 12:42:10 +03:30
|
|
|
|
|
|
|
|
// Define type guard for CustomerIndividual
|
|
|
|
|
function isCustomerIndividual(customer: any): customer is CustomerIndividual {
|
|
|
|
|
return (
|
|
|
|
|
customer &&
|
|
|
|
|
typeof customer.first_name === 'string' &&
|
|
|
|
|
typeof customer.last_name === 'string'
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Define type guard for CustomerLegal
|
|
|
|
|
function isCustomerLegal(customer: any): customer is CustomerLegal {
|
|
|
|
|
return (
|
|
|
|
|
customer &&
|
|
|
|
|
typeof customer.company_name === 'string' &&
|
|
|
|
|
typeof customer.registration_number === 'string'
|
|
|
|
|
)
|
|
|
|
|
}
|
2026-01-07 15:25:59 +03:30
|
|
|
|
2026-04-30 16:27:46 +03:30
|
|
|
interface TerminalPaymentInfo {
|
|
|
|
|
terminalId: string
|
|
|
|
|
stan: string
|
|
|
|
|
rrn: string
|
|
|
|
|
transactionDateTime: string | Date
|
|
|
|
|
customerCardNO?: string
|
|
|
|
|
description?: string
|
|
|
|
|
amount?: number
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface NormalizedPayment {
|
|
|
|
|
method: PaymentMethodType
|
|
|
|
|
amount: number
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-07 15:25:59 +03:30
|
|
|
@Injectable()
|
|
|
|
|
export class SalesInvoicesService {
|
2026-04-30 16:27:46 +03:30
|
|
|
private readonly createInvoiceRetries = 3
|
|
|
|
|
|
2026-04-27 22:11:05 +03:30
|
|
|
constructor(
|
|
|
|
|
private prisma: PrismaService,
|
2026-05-03 16:23:17 +03:30
|
|
|
private salesInvoiceTaxService: SalesInvoiceTspService,
|
2026-04-27 22:11:05 +03:30
|
|
|
) {}
|
2026-02-24 12:42:10 +03:30
|
|
|
|
2026-05-01 19:43:59 +03:30
|
|
|
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,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
2026-05-03 16:23:17 +03:30
|
|
|
tsp_attempts: {
|
|
|
|
|
orderBy: {
|
|
|
|
|
created_at: 'desc',
|
|
|
|
|
},
|
|
|
|
|
take: 1,
|
2026-05-01 19:43:59 +03:30
|
|
|
select: {
|
2026-05-03 16:23:17 +03:30
|
|
|
status: true,
|
2026-05-01 19:43:59 +03:30
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
await tx.salesInvoice.count({ where }),
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
const summaryItems = items.map(invoice => ({
|
|
|
|
|
...invoice,
|
|
|
|
|
status: translateEnumValue(
|
2026-05-03 16:23:17 +03:30
|
|
|
'TspProviderResponseStatus',
|
|
|
|
|
invoice.tsp_attempts?.[0]?.status || TspProviderResponseStatus.NOT_SEND,
|
2026-05-01 19:43:59 +03:30
|
|
|
),
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
return ResponseMapper.paginate(summaryItems, { total, page, perPage })
|
2026-01-07 15:25:59 +03:30
|
|
|
}
|
|
|
|
|
|
2026-05-01 19:43:59 +03:30
|
|
|
async findOne(posInfo: IPosPayload, id: string) {
|
2026-05-03 16:23:17 +03:30
|
|
|
const invoice = await this.prisma.salesInvoice.findUnique({
|
2026-05-01 19:43:59 +03:30
|
|
|
where: {
|
|
|
|
|
id,
|
|
|
|
|
pos: {
|
2026-05-03 16:23:17 +03:30
|
|
|
id: posInfo.pos_id,
|
2026-05-01 19:43:59 +03:30
|
|
|
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,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
2026-05-03 16:23:17 +03:30
|
|
|
tsp_attempts: {
|
|
|
|
|
orderBy: {
|
|
|
|
|
created_at: 'desc',
|
|
|
|
|
},
|
2026-05-01 19:43:59 +03:30
|
|
|
select: {
|
|
|
|
|
id: true,
|
2026-05-03 16:23:17 +03:30
|
|
|
attempt_no: true,
|
|
|
|
|
status: true,
|
|
|
|
|
tax_id: true,
|
|
|
|
|
error_message: true,
|
|
|
|
|
sent_at: true,
|
|
|
|
|
received_at: true,
|
|
|
|
|
created_at: true,
|
2026-05-01 19:43:59 +03:30
|
|
|
},
|
2026-05-03 16:23:17 +03:30
|
|
|
take: 1,
|
2026-05-01 19:43:59 +03:30
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
2026-05-03 16:23:17 +03:30
|
|
|
if (invoice) {
|
|
|
|
|
const { tsp_attempts, ...rest } = invoice || {}
|
|
|
|
|
|
|
|
|
|
return ResponseMapper.single({
|
|
|
|
|
...rest,
|
|
|
|
|
status: tsp_attempts?.[0]?.status || TspProviderResponseStatus.NOT_SEND,
|
|
|
|
|
})
|
|
|
|
|
} else throw new NotFoundException('فاکتور مورد نظر شما یافت نشد.')
|
2026-01-07 15:25:59 +03:30
|
|
|
}
|
|
|
|
|
|
2026-05-03 16:23:17 +03:30
|
|
|
async send(invoiceId: string, posInfo: IPosPayload) {
|
|
|
|
|
const consumer_id = await this.checkAccessToInvoice(
|
|
|
|
|
posInfo.consumer_account_id,
|
|
|
|
|
posInfo.pos_id,
|
|
|
|
|
)
|
|
|
|
|
const invoice = await this.salesInvoiceTaxService.send(consumer_id, invoiceId)
|
2026-04-27 22:11:05 +03:30
|
|
|
|
2026-05-03 16:23:17 +03:30
|
|
|
return ResponseMapper.single(invoice)
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-04 11:21:49 +03:30
|
|
|
async revoke(invoiceId: string, posInfo: IPosPayload) {
|
|
|
|
|
await this.checkAccessToInvoice(posInfo.consumer_account_id, posInfo.pos_id)
|
|
|
|
|
|
|
|
|
|
const invoice = await this.salesInvoiceTaxService.revoke(posInfo.pos_id, invoiceId)
|
|
|
|
|
|
|
|
|
|
return ResponseMapper.single(invoice)
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-03 16:23:17 +03:30
|
|
|
async inquiry(consumer_account_id: string, pos_id: string, invoiceId: string) {
|
|
|
|
|
const consumer_id = await this.checkAccessToInvoice(consumer_account_id, pos_id)
|
|
|
|
|
const invoice = await this.salesInvoiceTaxService.get(invoiceId, pos_id, consumer_id)
|
|
|
|
|
return ResponseMapper.single(invoice)
|
|
|
|
|
}
|
2026-04-27 22:11:05 +03:30
|
|
|
|
|
|
|
|
// async sendBulk(invoiceIds: string[], posInfo: IPosPayload) {
|
|
|
|
|
// if (!invoiceIds.length) {
|
|
|
|
|
// throw new BadRequestException('invoice_ids is required and cannot be empty.')
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
// await this.salesInvoiceTaxService.sendBulk(invoiceIds, posInfo.pos_id)
|
|
|
|
|
|
|
|
|
|
// return ResponseMapper.single({
|
|
|
|
|
// invoice_ids: invoiceIds,
|
|
|
|
|
// sent_to_tax: true,
|
|
|
|
|
// })
|
|
|
|
|
// }
|
|
|
|
|
|
2026-03-29 18:06:41 +03:30
|
|
|
async create(data: CreateSalesInvoiceDto, posInfo: IPosPayload) {
|
2026-05-01 19:43:59 +03:30
|
|
|
const { business_id, pos_id, consumer_account_id, complex_id } = posInfo
|
2026-04-30 16:27:46 +03:30
|
|
|
const normalizedInvoiceDate = this.normalizeInvoiceDate(data.invoice_date)
|
|
|
|
|
const { payments, terminalInfo } = this.buildPaymentsData(
|
|
|
|
|
data.payments,
|
|
|
|
|
data.total_amount,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
for (let attempt = 1; attempt <= this.createInvoiceRetries; attempt++) {
|
|
|
|
|
try {
|
|
|
|
|
const salesInvoice = await this.prisma.$transaction(async $tx => {
|
|
|
|
|
const invoiceNumber = await this.getNextInvoiceNumber($tx, business_id)
|
|
|
|
|
const newCustomerId = await this.resolveCustomerId($tx, data, business_id)
|
|
|
|
|
const goodsById = await this.getGoodsById($tx, data)
|
|
|
|
|
const salesInvoiceData = this.buildSalesInvoiceData({
|
|
|
|
|
data,
|
|
|
|
|
normalizedInvoiceDate,
|
|
|
|
|
invoiceNumber,
|
|
|
|
|
consumer_account_id,
|
2026-05-01 19:43:59 +03:30
|
|
|
businessId: business_id,
|
|
|
|
|
complexId: complex_id,
|
2026-04-30 16:27:46 +03:30
|
|
|
pos_id,
|
|
|
|
|
goodsById,
|
|
|
|
|
customerId: newCustomerId,
|
|
|
|
|
})
|
2026-03-29 18:06:41 +03:30
|
|
|
|
2026-04-30 16:27:46 +03:30
|
|
|
const salesInvoice = await $tx.salesInvoice.create({
|
|
|
|
|
data: salesInvoiceData,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
await this.createPayments(
|
|
|
|
|
$tx,
|
|
|
|
|
salesInvoice.id,
|
|
|
|
|
payments,
|
|
|
|
|
terminalInfo,
|
|
|
|
|
normalizedInvoiceDate,
|
|
|
|
|
)
|
|
|
|
|
|
2026-05-03 16:23:17 +03:30
|
|
|
if (data.send_to_tsp) {
|
|
|
|
|
await this.salesInvoiceTaxService.send(salesInvoice.id, pos_id)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-30 16:27:46 +03:30
|
|
|
return salesInvoice
|
2026-02-24 12:42:10 +03:30
|
|
|
})
|
|
|
|
|
|
2026-04-30 16:27:46 +03:30
|
|
|
return ResponseMapper.create(salesInvoice)
|
|
|
|
|
} catch (error) {
|
|
|
|
|
if (
|
|
|
|
|
this.isRetryableInvoiceConflict(error) &&
|
|
|
|
|
attempt < this.createInvoiceRetries
|
|
|
|
|
) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
throw error
|
2026-02-24 12:42:10 +03:30
|
|
|
}
|
2026-04-30 16:27:46 +03:30
|
|
|
}
|
2026-02-24 12:42:10 +03:30
|
|
|
|
2026-04-30 16:27:46 +03:30
|
|
|
throw new BadRequestException('ایجاد فاکتور با خطا مواجه شد.')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private isRetryableInvoiceConflict(error: unknown) {
|
|
|
|
|
if (!(error instanceof Prisma.PrismaClientKnownRequestError)) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (error.code !== 'P2002') {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const target = (error.meta?.target as string[]) || []
|
|
|
|
|
return (
|
|
|
|
|
target.includes('invoice_number') ||
|
|
|
|
|
target.includes('sales_invoices_invoice_number_pos_id_key')
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-01 19:43:59 +03:30
|
|
|
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) {
|
2026-05-03 16:23:17 +03:30
|
|
|
if (query.status === TspProviderResponseStatus.NOT_SEND) {
|
|
|
|
|
where.tsp_attempts = undefined
|
2026-05-01 19:43:59 +03:30
|
|
|
} else {
|
2026-05-03 16:23:17 +03:30
|
|
|
where.tsp_attempts = {
|
|
|
|
|
some: {
|
|
|
|
|
status: query.status,
|
2026-05-01 19:43:59 +03:30
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return where
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-30 16:27:46 +03:30
|
|
|
private normalizeInvoiceDate(invoiceDate: Date | string) {
|
|
|
|
|
return new Date(invoiceDate).toISOString() as any
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private buildPaymentsData(
|
|
|
|
|
paymentsData: CreateSalesInvoiceDto['payments'],
|
|
|
|
|
totalAmount: number,
|
|
|
|
|
) {
|
|
|
|
|
const paymentMethodMap: Record<string, PaymentMethodType> = {
|
|
|
|
|
cash: PaymentMethodType.CASH,
|
|
|
|
|
set_off: PaymentMethodType.SET_OFF,
|
|
|
|
|
card: PaymentMethodType.CARD,
|
2026-05-03 16:23:17 +03:30
|
|
|
payment_gateway: PaymentMethodType.PAYMENT_GATEWAY,
|
2026-04-30 16:27:46 +03:30
|
|
|
bank: PaymentMethodType.BANK,
|
2026-05-03 16:23:17 +03:30
|
|
|
check: PaymentMethodType.CHEQUE,
|
2026-04-30 16:27:46 +03:30
|
|
|
other: PaymentMethodType.OTHER,
|
|
|
|
|
terminal: PaymentMethodType.TERMINAL,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const rawPayments = (paymentsData || {}) as Record<string, unknown>
|
|
|
|
|
const terminalInfo = rawPayments.terminals as TerminalPaymentInfo | undefined
|
|
|
|
|
|
|
|
|
|
const payments: NormalizedPayment[] = Object.entries(rawPayments)
|
|
|
|
|
.filter(([key]) => key !== 'terminals')
|
|
|
|
|
.map(([key, value]) => ({
|
|
|
|
|
method: paymentMethodMap[key.toLowerCase()],
|
|
|
|
|
amount: typeof value === 'number' ? value : Number(value || 0),
|
|
|
|
|
}))
|
|
|
|
|
.filter(payment => payment.method && payment.amount > 0) as NormalizedPayment[]
|
|
|
|
|
|
|
|
|
|
const hasTerminalPayment = payments.some(
|
|
|
|
|
payment => payment.method === PaymentMethodType.TERMINAL,
|
|
|
|
|
)
|
|
|
|
|
const nonTerminalTotal = payments
|
|
|
|
|
.filter(payment => payment.method !== PaymentMethodType.TERMINAL)
|
|
|
|
|
.reduce((sum, payment) => sum + payment.amount, 0)
|
|
|
|
|
|
|
|
|
|
if (!hasTerminalPayment && terminalInfo) {
|
|
|
|
|
const terminalAmount =
|
|
|
|
|
typeof terminalInfo.amount === 'number'
|
|
|
|
|
? terminalInfo.amount
|
|
|
|
|
: Math.max(0, Number(totalAmount) - nonTerminalTotal)
|
|
|
|
|
|
|
|
|
|
if (terminalAmount > 0) {
|
|
|
|
|
payments.push({
|
|
|
|
|
method: PaymentMethodType.TERMINAL,
|
|
|
|
|
amount: terminalAmount,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.validatePayments(payments, totalAmount, terminalInfo)
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
payments,
|
|
|
|
|
terminalInfo,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private validatePayments(
|
|
|
|
|
payments: NormalizedPayment[],
|
|
|
|
|
totalAmount: number,
|
|
|
|
|
terminalInfo?: TerminalPaymentInfo,
|
|
|
|
|
) {
|
|
|
|
|
const totalPayments = payments.reduce((sum, payment) => sum + payment.amount, 0)
|
|
|
|
|
const roundedTotalPayments = Number(totalPayments.toFixed(2))
|
|
|
|
|
const roundedTotalAmount = Number(Number(totalAmount).toFixed(2))
|
|
|
|
|
|
|
|
|
|
if (roundedTotalPayments !== roundedTotalAmount) {
|
|
|
|
|
throw new BadRequestException('مبلغ پرداختی باید برابر با مبلغ کل فاکتور باشد.')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const terminalPayments = payments.filter(
|
|
|
|
|
payment => payment.method === PaymentMethodType.TERMINAL,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if (terminalPayments.length > 0 && !terminalInfo) {
|
|
|
|
|
throw new BadRequestException('برای پرداخت ترمینال اطلاعات ترمینال الزامی است.')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async resolveCustomerId(
|
|
|
|
|
tx: Prisma.TransactionClient,
|
|
|
|
|
data: CreateSalesInvoiceDto,
|
|
|
|
|
businessId: string,
|
|
|
|
|
) {
|
|
|
|
|
if (data.customer_id) {
|
|
|
|
|
return data.customer_id
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
data.customer_type === CustomerType.INDIVIDUAL &&
|
|
|
|
|
data.customer?.customer_individual
|
|
|
|
|
) {
|
|
|
|
|
const { national_id, mobile_number, ...rest } = data.customer.customer_individual
|
|
|
|
|
|
|
|
|
|
const customerIndividual = await tx.customerIndividual.upsert({
|
|
|
|
|
where: {
|
|
|
|
|
business_activity_id_national_id: {
|
|
|
|
|
business_activity_id: businessId,
|
|
|
|
|
national_id: data.customer.customer_individual.national_id,
|
2026-02-24 12:42:10 +03:30
|
|
|
},
|
2026-04-30 16:27:46 +03:30
|
|
|
},
|
|
|
|
|
create: {
|
|
|
|
|
...data.customer.customer_individual,
|
|
|
|
|
customer: {
|
|
|
|
|
create: {
|
|
|
|
|
type: CustomerType.INDIVIDUAL,
|
2026-02-24 12:42:10 +03:30
|
|
|
},
|
2026-03-30 13:16:36 +03:30
|
|
|
},
|
2026-04-30 16:27:46 +03:30
|
|
|
business_activity: {
|
|
|
|
|
connect: {
|
|
|
|
|
id: businessId,
|
|
|
|
|
},
|
2026-03-30 13:16:36 +03:30
|
|
|
},
|
2026-04-30 16:27:46 +03:30
|
|
|
},
|
|
|
|
|
update: { ...rest },
|
|
|
|
|
select: {
|
|
|
|
|
customer_id: true,
|
|
|
|
|
},
|
|
|
|
|
})
|
2026-02-24 12:42:10 +03:30
|
|
|
|
2026-04-30 16:27:46 +03:30
|
|
|
if (!customerIndividual) {
|
|
|
|
|
throw new BadRequestException('متاسفانه مشکلی در اطلاعات مشتری وجود دارد.')
|
|
|
|
|
}
|
2026-03-30 13:16:36 +03:30
|
|
|
|
2026-04-30 16:27:46 +03:30
|
|
|
return customerIndividual.customer_id
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (data.customer_type === CustomerType.LEGAL && data.customer?.customer_legal) {
|
|
|
|
|
const { registration_number, ...rest } = data.customer.customer_legal
|
|
|
|
|
const customerLegal = await tx.customerLegal.upsert({
|
|
|
|
|
where: {
|
|
|
|
|
business_activity_id_economic_code: {
|
|
|
|
|
business_activity_id: businessId,
|
|
|
|
|
economic_code: data.customer.customer_legal.economic_code,
|
2026-02-24 12:42:10 +03:30
|
|
|
},
|
2026-04-30 16:27:46 +03:30
|
|
|
},
|
|
|
|
|
create: {
|
|
|
|
|
...data.customer.customer_legal,
|
|
|
|
|
customer: {
|
|
|
|
|
create: {
|
|
|
|
|
type: CustomerType.LEGAL,
|
2026-02-24 12:42:10 +03:30
|
|
|
},
|
2026-03-30 13:16:36 +03:30
|
|
|
},
|
2026-04-30 16:27:46 +03:30
|
|
|
business_activity: {
|
|
|
|
|
connect: {
|
|
|
|
|
id: businessId,
|
|
|
|
|
},
|
2026-03-30 13:16:36 +03:30
|
|
|
},
|
2026-04-30 16:27:46 +03:30
|
|
|
},
|
|
|
|
|
update: {
|
|
|
|
|
...rest,
|
|
|
|
|
},
|
|
|
|
|
select: {
|
|
|
|
|
customer_id: true,
|
|
|
|
|
},
|
|
|
|
|
})
|
2026-02-24 12:42:10 +03:30
|
|
|
|
2026-04-30 16:27:46 +03:30
|
|
|
if (!customerLegal) {
|
|
|
|
|
throw new BadRequestException('متاسفانه مشکلی در اطلاعات مشتری وجود دارد.')
|
2026-02-24 12:42:10 +03:30
|
|
|
}
|
|
|
|
|
|
2026-04-30 16:27:46 +03:30
|
|
|
return customerLegal.customer_id
|
|
|
|
|
}
|
2026-04-27 22:11:05 +03:30
|
|
|
|
2026-04-30 16:27:46 +03:30
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async getGoodsById(tx: Prisma.TransactionClient, data: CreateSalesInvoiceDto) {
|
|
|
|
|
const itemGoodIds = data.items
|
|
|
|
|
.map(item => item.good_id)
|
|
|
|
|
.filter((goodId): goodId is string => Boolean(goodId))
|
|
|
|
|
|
|
|
|
|
const goods = itemGoodIds.length
|
|
|
|
|
? await tx.good.findMany({
|
|
|
|
|
where: {
|
|
|
|
|
id: {
|
|
|
|
|
in: itemGoodIds,
|
2026-04-27 22:11:05 +03:30
|
|
|
},
|
2026-04-30 16:27:46 +03:30
|
|
|
},
|
|
|
|
|
select: {
|
|
|
|
|
id: true,
|
|
|
|
|
name: true,
|
2026-05-01 19:43:59 +03:30
|
|
|
sku: {
|
|
|
|
|
select: {
|
|
|
|
|
id: true,
|
|
|
|
|
name: true,
|
2026-05-03 16:23:17 +03:30
|
|
|
code: true,
|
|
|
|
|
VAT: true,
|
2026-05-01 19:43:59 +03:30
|
|
|
},
|
|
|
|
|
},
|
2026-04-30 16:27:46 +03:30
|
|
|
local_sku: true,
|
|
|
|
|
barcode: true,
|
|
|
|
|
pricing_model: true,
|
2026-05-01 19:43:59 +03:30
|
|
|
measure_unit: {
|
|
|
|
|
select: {
|
|
|
|
|
id: true,
|
|
|
|
|
name: true,
|
2026-05-03 16:23:17 +03:30
|
|
|
code: true,
|
2026-05-01 19:43:59 +03:30
|
|
|
},
|
|
|
|
|
},
|
2026-04-30 16:27:46 +03:30
|
|
|
base_sale_price: true,
|
|
|
|
|
image_url: true,
|
|
|
|
|
category: {
|
|
|
|
|
select: {
|
|
|
|
|
id: true,
|
|
|
|
|
name: true,
|
2026-04-27 22:11:05 +03:30
|
|
|
},
|
|
|
|
|
},
|
2026-02-24 12:42:10 +03:30
|
|
|
},
|
2026-04-30 16:27:46 +03:30
|
|
|
})
|
|
|
|
|
: []
|
|
|
|
|
|
|
|
|
|
return new Map(goods.map(good => [good.id, good]))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private buildSalesInvoiceData(params: {
|
|
|
|
|
data: CreateSalesInvoiceDto
|
|
|
|
|
normalizedInvoiceDate: Date
|
|
|
|
|
invoiceNumber: number
|
|
|
|
|
consumer_account_id: string
|
2026-05-01 19:43:59 +03:30
|
|
|
businessId: string
|
|
|
|
|
complexId: string
|
2026-04-30 16:27:46 +03:30
|
|
|
pos_id: string
|
|
|
|
|
goodsById: Map<string, any>
|
|
|
|
|
customerId: string | null
|
|
|
|
|
}) {
|
|
|
|
|
const {
|
|
|
|
|
data,
|
|
|
|
|
normalizedInvoiceDate,
|
|
|
|
|
invoiceNumber,
|
|
|
|
|
consumer_account_id,
|
|
|
|
|
pos_id,
|
|
|
|
|
goodsById,
|
|
|
|
|
customerId,
|
2026-05-01 19:43:59 +03:30
|
|
|
businessId,
|
|
|
|
|
complexId,
|
2026-04-30 16:27:46 +03:30
|
|
|
} = params
|
|
|
|
|
const { customer_id, customer_type, customer, payments, ...invoiceData } = data
|
|
|
|
|
|
2026-05-03 16:23:17 +03:30
|
|
|
const salesInvoiceData: SalesInvoiceCreateInput = {
|
2026-04-30 16:27:46 +03:30
|
|
|
...invoiceData,
|
|
|
|
|
invoice_date: normalizedInvoiceDate,
|
|
|
|
|
invoice_number: invoiceNumber,
|
|
|
|
|
total_amount: data.total_amount,
|
2026-05-01 19:43:59 +03:30
|
|
|
code: this.generateInvoiceCode(businessId, complexId, pos_id, invoiceNumber),
|
2026-04-30 16:27:46 +03:30
|
|
|
items: {
|
|
|
|
|
createMany: {
|
|
|
|
|
data: data.items.map(item => ({
|
2026-05-03 16:23:17 +03:30
|
|
|
good_id: item.good_id!,
|
2026-04-30 16:27:46 +03:30
|
|
|
quantity: item.quantity,
|
|
|
|
|
unit_price: item.unit_price,
|
|
|
|
|
total_amount: item.total_amount,
|
2026-05-03 16:23:17 +03:30
|
|
|
measure_unit_text: goodsById.get(item.good_id!)?.measure_unit.name || null,
|
|
|
|
|
measure_unit_code: goodsById.get(item.good_id!)?.measure_unit.code || null,
|
|
|
|
|
sku_code: goodsById.get(item.good_id!)?.sku.code || null,
|
|
|
|
|
sku_vat: goodsById.get(item.good_id!)?.sku.VAT || null,
|
2026-04-30 16:27:46 +03:30
|
|
|
payload: item.payload ? JSON.parse(JSON.stringify(item.payload)) : undefined,
|
|
|
|
|
good_snapshot: item.good_id
|
|
|
|
|
? JSON.parse(
|
|
|
|
|
JSON.stringify({
|
|
|
|
|
good: goodsById.get(item.good_id) || null,
|
|
|
|
|
}),
|
|
|
|
|
)
|
|
|
|
|
: undefined,
|
|
|
|
|
})),
|
2026-03-30 13:16:36 +03:30
|
|
|
},
|
2026-04-30 16:27:46 +03:30
|
|
|
},
|
|
|
|
|
unknown_customer: {},
|
|
|
|
|
consumer_account: {
|
|
|
|
|
connect: {
|
|
|
|
|
id: consumer_account_id,
|
2026-03-30 13:16:36 +03:30
|
|
|
},
|
2026-04-30 16:27:46 +03:30
|
|
|
},
|
|
|
|
|
pos: {
|
|
|
|
|
connect: {
|
|
|
|
|
id: pos_id,
|
2026-03-30 13:16:36 +03:30
|
|
|
},
|
2026-04-30 16:27:46 +03:30
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-03 16:23:17 +03:30
|
|
|
if (data.send_to_tsp) {
|
|
|
|
|
salesInvoiceData.tsp_attempts = {
|
|
|
|
|
create: {
|
|
|
|
|
attempt_no: 1,
|
|
|
|
|
status: TspProviderResponseStatus.QUEUED,
|
|
|
|
|
type: TspProviderRequestType.MAIN,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-30 16:27:46 +03:30
|
|
|
if (customerId) {
|
|
|
|
|
salesInvoiceData.customer = {
|
|
|
|
|
connect: {
|
|
|
|
|
id: customerId,
|
2026-02-24 12:42:10 +03:30
|
|
|
},
|
2026-03-30 13:16:36 +03:30
|
|
|
}
|
2026-04-30 16:27:46 +03:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return salesInvoiceData
|
|
|
|
|
}
|
2026-03-30 13:16:36 +03:30
|
|
|
|
2026-04-30 16:27:46 +03:30
|
|
|
private async getNextInvoiceNumber(tx: Prisma.TransactionClient, businessId: string) {
|
|
|
|
|
const latestInvoice = await tx.salesInvoice.findFirst({
|
|
|
|
|
where: {
|
|
|
|
|
pos: {
|
|
|
|
|
complex: {
|
|
|
|
|
business_activity_id: businessId,
|
2026-03-30 13:16:36 +03:30
|
|
|
},
|
2026-04-30 16:27:46 +03:30
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
orderBy: {
|
|
|
|
|
invoice_number: 'desc',
|
|
|
|
|
},
|
|
|
|
|
select: {
|
|
|
|
|
invoice_number: true,
|
2026-05-03 16:23:17 +03:30
|
|
|
pos: {
|
|
|
|
|
select: {
|
|
|
|
|
complex: {
|
|
|
|
|
select: {
|
|
|
|
|
business_activity: {
|
|
|
|
|
select: {
|
|
|
|
|
invoice_number_sequence: true,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
2026-04-30 16:27:46 +03:30
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
2026-05-03 16:23:17 +03:30
|
|
|
const latestSequence =
|
|
|
|
|
latestInvoice?.pos?.complex?.business_activity?.invoice_number_sequence.toNumber() ||
|
|
|
|
|
0
|
|
|
|
|
|
|
|
|
|
console.log(latestSequence)
|
|
|
|
|
|
|
|
|
|
return Math.max(latestInvoice?.invoice_number || 0, latestSequence) + 1
|
2026-04-30 16:27:46 +03:30
|
|
|
}
|
2026-03-30 13:16:36 +03:30
|
|
|
|
2026-04-30 16:27:46 +03:30
|
|
|
private async createPayments(
|
|
|
|
|
tx: Prisma.TransactionClient,
|
|
|
|
|
invoiceId: string,
|
|
|
|
|
payments: NormalizedPayment[],
|
|
|
|
|
terminalInfo: TerminalPaymentInfo | undefined,
|
|
|
|
|
paidAt: Date,
|
|
|
|
|
) {
|
|
|
|
|
for (const payment of payments) {
|
|
|
|
|
if (payment.amount <= 0) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
const createdPayment = await tx.salesInvoicePayment.create({
|
|
|
|
|
data: {
|
|
|
|
|
invoice_id: invoiceId,
|
|
|
|
|
amount: payment.amount,
|
|
|
|
|
payment_method: payment.method,
|
|
|
|
|
paid_at: paidAt,
|
|
|
|
|
},
|
2026-02-24 12:42:10 +03:30
|
|
|
})
|
|
|
|
|
|
2026-04-30 16:27:46 +03:30
|
|
|
if (payment.method === PaymentMethodType.TERMINAL && terminalInfo) {
|
|
|
|
|
await tx.salesInvoicePaymentTerminalInfo.create({
|
|
|
|
|
data: {
|
|
|
|
|
payment_id: createdPayment.id,
|
|
|
|
|
terminal_id: terminalInfo.terminalId,
|
|
|
|
|
stan: terminalInfo.stan,
|
|
|
|
|
rrn: terminalInfo.rrn,
|
2026-05-03 16:23:17 +03:30
|
|
|
transaction_date_time: new Date(terminalInfo.transactionDateTime || ''),
|
2026-04-30 16:27:46 +03:30
|
|
|
customer_card_no: terminalInfo.customerCardNO || null,
|
|
|
|
|
description: terminalInfo.description || null,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}
|
2026-04-27 22:11:05 +03:30
|
|
|
}
|
2026-01-07 15:25:59 +03:30
|
|
|
}
|
2026-05-01 19:43:59 +03:30
|
|
|
|
|
|
|
|
private generateInvoiceCode(
|
|
|
|
|
business_id: string,
|
|
|
|
|
complex_id: string,
|
|
|
|
|
pos_id: string,
|
|
|
|
|
invoice_number: number,
|
|
|
|
|
) {
|
2026-05-03 16:23:17 +03:30
|
|
|
return `${business_id.substring(business_id.length - 2, business_id.length)}-${complex_id.substring(complex_id.length - 2, complex_id.length)}-${pos_id.substring(pos_id.length - 2, pos_id.length)}-${invoice_number.toString()}`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async checkAccessToInvoice(
|
|
|
|
|
consumer_account_id: string,
|
|
|
|
|
pos_id: string,
|
|
|
|
|
): Promise<string> {
|
|
|
|
|
const consumer = await this.prisma.consumerAccount.findUnique({
|
|
|
|
|
where: {
|
|
|
|
|
id: consumer_account_id,
|
|
|
|
|
OR: [
|
|
|
|
|
{
|
|
|
|
|
pos: {
|
|
|
|
|
id: pos_id,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
role: ConsumerRole.OWNER,
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
select: {
|
|
|
|
|
consumer_id: true,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
if (!consumer) {
|
|
|
|
|
throw new BadRequestException('شما دسترسی لازم برای ارسال فاکتور را ندارید.')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return consumer.consumer_id
|
2026-05-01 19:43:59 +03:30
|
|
|
}
|
2026-01-07 15:25:59 +03:30
|
|
|
}
|